blob: 579f181fd7d59bbc80a40e8be165f60d3be6da19 [file] [log] [blame]
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.components.gcm_driver;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.AsyncTask;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.util.Log;
import com.google.android.gcm.GCMRegistrar;
import org.chromium.base.CalledByNative;
import org.chromium.base.JNINamespace;
import org.chromium.base.ThreadUtils;
import org.chromium.base.library_loader.ProcessInitException;
import org.chromium.content.browser.BrowserStartupController;
import java.util.ArrayList;
import java.util.List;
/**
* This class is the Java counterpart to the C++ GCMDriverAndroid class.
* It uses Android's Java GCM APIs to implements GCM registration etc, and
* sends back GCM messages over JNI.
*
* Threading model: all calls to/from C++ happen on the UI thread.
*/
@JNINamespace("gcm")
public class GCMDriver {
private static final String TAG = "GCMDriver";
private static final String LAST_GCM_APP_ID_KEY = "last_gcm_app_id";
// The instance of GCMDriver currently owned by a C++ GCMDriverAndroid, if any.
private static GCMDriver sInstance = null;
private long mNativeGCMDriverAndroid;
private final Context mContext;
private GCMDriver(long nativeGCMDriverAndroid, Context context) {
mNativeGCMDriverAndroid = nativeGCMDriverAndroid;
mContext = context;
}
/**
* Create a GCMDriver object, which is owned by GCMDriverAndroid
* on the C++ side.
*
* @param nativeGCMDriverAndroid The C++ object that owns us.
* @param context The app context.
*/
@CalledByNative
private static GCMDriver create(long nativeGCMDriverAndroid,
Context context) {
if (sInstance != null) {
throw new IllegalStateException("Already instantiated");
}
sInstance = new GCMDriver(nativeGCMDriverAndroid, context);
return sInstance;
}
/**
* Called when our C++ counterpart is deleted. Clear the handle to our
* native C++ object, ensuring it's never called.
*/
@CalledByNative
private void destroy() {
assert sInstance == this;
sInstance = null;
mNativeGCMDriverAndroid = 0;
}
@CalledByNative
private void register(final String appId, final String[] senderIds) {
setLastAppId(appId);
new AsyncTask<Void, Void, String>() {
@Override
protected String doInBackground(Void... voids) {
try {
GCMRegistrar.checkDevice(mContext);
} catch (UnsupportedOperationException ex) {
return ""; // Indicates failure.
}
// TODO(johnme): Move checkManifest call to a test instead.
GCMRegistrar.checkManifest(mContext);
String existingRegistrationId = GCMRegistrar.getRegistrationId(mContext);
if (existingRegistrationId.equals("")) {
// TODO(johnme): Migrate from GCMRegistrar to GoogleCloudMessaging API, both
// here and elsewhere in Chromium.
// TODO(johnme): Pass appId to GCM.
GCMRegistrar.register(mContext, senderIds);
return null; // Indicates pending result.
} else {
Log.i(TAG, "Re-using existing registration ID");
return existingRegistrationId;
}
}
@Override
protected void onPostExecute(String registrationId) {
if (registrationId == null) {
return; // Wait for {@link #onRegisterFinished} to be called.
}
nativeOnRegisterFinished(mNativeGCMDriverAndroid, appId, registrationId,
!registrationId.isEmpty());
}
}.execute();
}
private enum UnregisterResult { SUCCESS, FAILED, PENDING }
@CalledByNative
private void unregister(final String appId) {
new AsyncTask<Void, Void, UnregisterResult>() {
@Override
protected UnregisterResult doInBackground(Void... voids) {
try {
GCMRegistrar.checkDevice(mContext);
} catch (UnsupportedOperationException ex) {
return UnregisterResult.FAILED;
}
if (!GCMRegistrar.isRegistered(mContext)) {
return UnregisterResult.SUCCESS;
}
// TODO(johnme): Pass appId to GCM.
GCMRegistrar.unregister(mContext);
return UnregisterResult.PENDING;
}
@Override
protected void onPostExecute(UnregisterResult result) {
if (result == UnregisterResult.PENDING) {
return; // Wait for {@link #onUnregisterFinished} to be called.
}
nativeOnUnregisterFinished(mNativeGCMDriverAndroid, appId,
result == UnregisterResult.SUCCESS);
}
}.execute();
}
static void onRegisterFinished(String appId, String registrationId) {
ThreadUtils.assertOnUiThread();
// TODO(johnme): If this gets called, did it definitely succeed?
// TODO(johnme): Update registrations cache?
if (sInstance != null) {
sInstance.nativeOnRegisterFinished(sInstance.mNativeGCMDriverAndroid, getLastAppId(),
registrationId, true);
}
}
static void onUnregisterFinished(String appId) {
ThreadUtils.assertOnUiThread();
// TODO(johnme): If this gets called, did it definitely succeed?
// TODO(johnme): Update registrations cache?
if (sInstance != null) {
sInstance.nativeOnUnregisterFinished(sInstance.mNativeGCMDriverAndroid, getLastAppId(),
true);
}
}
static void onMessageReceived(Context context, final String appId, final Bundle extras) {
final String PUSH_API_DATA_KEY = "data";
if (!extras.containsKey(PUSH_API_DATA_KEY)) {
// For now on Android only the Push API uses GCMDriver. To avoid double-handling of
// messages already handled in Java by other implementations of MultiplexingGcmListener,
// and unnecessarily waking up the browser processes for all existing GCM messages that
// are received by Chrome on Android, we currently discard messages unless they are
// destined for the Push API.
// TODO(johnme): Find a better way of distinguishing messages that should be delivered
// to native from messages that have already been delivered to Java, for example by
// refactoring other implementations of MultiplexingGcmListener to instead register with
// this class, and distinguish them based on appId (which also requires GCM to start
// sending us the app IDs).
return;
}
// TODO(johnme): Store message and redeliver later if Chrome is killed before delivery.
ThreadUtils.assertOnUiThread();
launchNativeThen(context, new Runnable() {
@Override public void run() {
final String BUNDLE_SENDER_ID = "from";
final String BUNDLE_COLLAPSE_KEY = "collapse_key";
final String BUNDLE_GCMMPLEX = "com.google.ipc.invalidation.gcmmplex.";
String senderId = extras.getString(BUNDLE_SENDER_ID);
String collapseKey = extras.getString(BUNDLE_COLLAPSE_KEY);
List<String> dataKeysAndValues = new ArrayList<String>();
for (String key : extras.keySet()) {
// TODO(johnme): Check there aren't other keys that we need to exclude.
if (key == BUNDLE_SENDER_ID || key == BUNDLE_COLLAPSE_KEY ||
key.startsWith(BUNDLE_GCMMPLEX))
continue;
dataKeysAndValues.add(key);
dataKeysAndValues.add(extras.getString(key));
}
sInstance.nativeOnMessageReceived(sInstance.mNativeGCMDriverAndroid,
getLastAppId(), senderId, collapseKey,
dataKeysAndValues.toArray(new String[dataKeysAndValues.size()]));
}
});
}
static void onMessagesDeleted(Context context, final String appId) {
// TODO(johnme): Store event and redeliver later if Chrome is killed before delivery.
ThreadUtils.assertOnUiThread();
launchNativeThen(context, new Runnable() {
@Override public void run() {
sInstance.nativeOnMessagesDeleted(sInstance.mNativeGCMDriverAndroid,
getLastAppId());
}
});
}
private native void nativeOnRegisterFinished(long nativeGCMDriverAndroid, String appId,
String registrationId, boolean success);
private native void nativeOnUnregisterFinished(long nativeGCMDriverAndroid, String appId,
boolean success);
private native void nativeOnMessageReceived(long nativeGCMDriverAndroid, String appId,
String senderId, String collapseKey, String[] dataKeysAndValues);
private native void nativeOnMessagesDeleted(long nativeGCMDriverAndroid, String appId);
// TODO(johnme): This and setLastAppId are just temporary (crbug.com/350383).
private static String getLastAppId() {
SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(
sInstance.mContext);
return settings.getString(LAST_GCM_APP_ID_KEY, "push#unknown_app_id#0");
}
private static void setLastAppId(String appId) {
SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(
sInstance.mContext);
SharedPreferences.Editor editor = settings.edit();
editor.putString(LAST_GCM_APP_ID_KEY, appId);
editor.commit();
}
private static void launchNativeThen(Context context, Runnable task) {
if (sInstance != null) {
task.run();
return;
}
// TODO(johnme): Call ChromeMobileApplication.initCommandLine(context) or
// ChromeShellApplication.initCommandLine() as appropriate.
try {
final int MAX_RENDERERS = 1;
BrowserStartupController.get(context).startBrowserProcessesSync(MAX_RENDERERS);
if (sInstance != null) {
task.run();
} else {
Log.e(TAG, "Started browser process, but failed to instantiate GCMDriver.");
}
} catch (ProcessInitException e) {
Log.e(TAG, "Failed to start browser process.", e);
System.exit(-1);
}
// TODO(johnme): Now we should probably exit?
}
}