1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 package org.chromium.components.gcm_driver;
7 import android.content.Context;
8 import android.content.SharedPreferences;
9 import android.os.AsyncTask;
10 import android.os.Bundle;
11 import android.preference.PreferenceManager;
12 import android.util.Log;
14 import com.google.android.gcm.GCMRegistrar;
16 import org.chromium.base.CalledByNative;
17 import org.chromium.base.JNINamespace;
18 import org.chromium.base.ThreadUtils;
19 import org.chromium.base.library_loader.ProcessInitException;
20 import org.chromium.content.browser.BrowserStartupController;
22 import java.util.ArrayList;
23 import java.util.List;
26 * This class is the Java counterpart to the C++ GCMDriverAndroid class.
27 * It uses Android's Java GCM APIs to implements GCM registration etc, and
28 * sends back GCM messages over JNI.
30 * Threading model: all calls to/from C++ happen on the UI thread.
33 public class GCMDriver {
34 private static final String TAG = "GCMDriver";
36 private static final String LAST_GCM_APP_ID_KEY = "last_gcm_app_id";
38 // The instance of GCMDriver currently owned by a C++ GCMDriverAndroid, if any.
39 private static GCMDriver sInstance = null;
41 private long mNativeGCMDriverAndroid;
42 private final Context mContext;
44 private GCMDriver(long nativeGCMDriverAndroid, Context context) {
45 mNativeGCMDriverAndroid = nativeGCMDriverAndroid;
50 * Create a GCMDriver object, which is owned by GCMDriverAndroid
53 * @param nativeGCMDriverAndroid The C++ object that owns us.
54 * @param context The app context.
57 private static GCMDriver create(long nativeGCMDriverAndroid,
59 if (sInstance != null) {
60 throw new IllegalStateException("Already instantiated");
62 sInstance = new GCMDriver(nativeGCMDriverAndroid, context);
67 * Called when our C++ counterpart is deleted. Clear the handle to our
68 * native C++ object, ensuring it's never called.
71 private void destroy() {
72 assert sInstance == this;
74 mNativeGCMDriverAndroid = 0;
78 private void register(final String appId, final String[] senderIds) {
80 new AsyncTask<Void, Void, String>() {
82 protected String doInBackground(Void... voids) {
84 GCMRegistrar.checkDevice(mContext);
85 } catch (UnsupportedOperationException ex) {
86 return ""; // Indicates failure.
88 // TODO(johnme): Move checkManifest call to a test instead.
89 GCMRegistrar.checkManifest(mContext);
90 String existingRegistrationId = GCMRegistrar.getRegistrationId(mContext);
91 if (existingRegistrationId.equals("")) {
92 // TODO(johnme): Migrate from GCMRegistrar to GoogleCloudMessaging API, both
93 // here and elsewhere in Chromium.
94 // TODO(johnme): Pass appId to GCM.
95 GCMRegistrar.register(mContext, senderIds);
96 return null; // Indicates pending result.
98 Log.i(TAG, "Re-using existing registration ID");
99 return existingRegistrationId;
103 protected void onPostExecute(String registrationId) {
104 if (registrationId == null) {
105 return; // Wait for {@link #onRegisterFinished} to be called.
107 nativeOnRegisterFinished(mNativeGCMDriverAndroid, appId, registrationId,
108 !registrationId.isEmpty());
113 private enum UnregisterResult { SUCCESS, FAILED, PENDING }
116 private void unregister(final String appId) {
117 new AsyncTask<Void, Void, UnregisterResult>() {
119 protected UnregisterResult doInBackground(Void... voids) {
121 GCMRegistrar.checkDevice(mContext);
122 } catch (UnsupportedOperationException ex) {
123 return UnregisterResult.FAILED;
125 if (!GCMRegistrar.isRegistered(mContext)) {
126 return UnregisterResult.SUCCESS;
128 // TODO(johnme): Pass appId to GCM.
129 GCMRegistrar.unregister(mContext);
130 return UnregisterResult.PENDING;
134 protected void onPostExecute(UnregisterResult result) {
135 if (result == UnregisterResult.PENDING) {
136 return; // Wait for {@link #onUnregisterFinished} to be called.
138 nativeOnUnregisterFinished(mNativeGCMDriverAndroid, appId,
139 result == UnregisterResult.SUCCESS);
144 static void onRegisterFinished(String appId, String registrationId) {
145 ThreadUtils.assertOnUiThread();
146 // TODO(johnme): If this gets called, did it definitely succeed?
147 // TODO(johnme): Update registrations cache?
148 if (sInstance != null) {
149 sInstance.nativeOnRegisterFinished(sInstance.mNativeGCMDriverAndroid, getLastAppId(),
150 registrationId, true);
154 static void onUnregisterFinished(String appId) {
155 ThreadUtils.assertOnUiThread();
156 // TODO(johnme): If this gets called, did it definitely succeed?
157 // TODO(johnme): Update registrations cache?
158 if (sInstance != null) {
159 sInstance.nativeOnUnregisterFinished(sInstance.mNativeGCMDriverAndroid, getLastAppId(),
164 static void onMessageReceived(Context context, final String appId, final Bundle extras) {
165 final String pushApiDataKey = "data";
166 if (!extras.containsKey(pushApiDataKey)) {
167 // For now on Android only the Push API uses GCMDriver. To avoid double-handling of
168 // messages already handled in Java by other implementations of MultiplexingGcmListener,
169 // and unnecessarily waking up the browser processes for all existing GCM messages that
170 // are received by Chrome on Android, we currently discard messages unless they are
171 // destined for the Push API.
172 // TODO(johnme): Find a better way of distinguishing messages that should be delivered
173 // to native from messages that have already been delivered to Java, for example by
174 // refactoring other implementations of MultiplexingGcmListener to instead register with
175 // this class, and distinguish them based on appId (which also requires GCM to start
176 // sending us the app IDs).
180 // TODO(johnme): Store message and redeliver later if Chrome is killed before delivery.
181 ThreadUtils.assertOnUiThread();
182 launchNativeThen(context, new Runnable() {
183 @Override public void run() {
184 final String bundleSenderId = "from";
185 final String bundleCollapseKey = "collapse_key";
186 final String bundleGcmplex = "com.google.ipc.invalidation.gcmmplex.";
188 String senderId = extras.getString(bundleSenderId);
189 String collapseKey = extras.getString(bundleCollapseKey);
191 List<String> dataKeysAndValues = new ArrayList<String>();
192 for (String key : extras.keySet()) {
193 // TODO(johnme): Check there aren't other keys that we need to exclude.
194 if (key == bundleSenderId || key == bundleCollapseKey ||
195 key.startsWith(bundleGcmplex))
197 dataKeysAndValues.add(key);
198 dataKeysAndValues.add(extras.getString(key));
201 sInstance.nativeOnMessageReceived(sInstance.mNativeGCMDriverAndroid,
202 getLastAppId(), senderId, collapseKey,
203 dataKeysAndValues.toArray(new String[dataKeysAndValues.size()]));
208 static void onMessagesDeleted(Context context, final String appId) {
209 // TODO(johnme): Store event and redeliver later if Chrome is killed before delivery.
210 ThreadUtils.assertOnUiThread();
211 launchNativeThen(context, new Runnable() {
212 @Override public void run() {
213 sInstance.nativeOnMessagesDeleted(sInstance.mNativeGCMDriverAndroid,
219 private native void nativeOnRegisterFinished(long nativeGCMDriverAndroid, String appId,
220 String registrationId, boolean success);
221 private native void nativeOnUnregisterFinished(long nativeGCMDriverAndroid, String appId,
223 private native void nativeOnMessageReceived(long nativeGCMDriverAndroid, String appId,
224 String senderId, String collapseKey, String[] dataKeysAndValues);
225 private native void nativeOnMessagesDeleted(long nativeGCMDriverAndroid, String appId);
227 // TODO(johnme): This and setLastAppId are just temporary (crbug.com/350383).
228 private static String getLastAppId() {
229 SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(
231 return settings.getString(LAST_GCM_APP_ID_KEY, "push#unknown_app_id#0");
234 private static void setLastAppId(String appId) {
235 SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(
237 SharedPreferences.Editor editor = settings.edit();
238 editor.putString(LAST_GCM_APP_ID_KEY, appId);
242 private static void launchNativeThen(Context context, Runnable task) {
243 if (sInstance != null) {
248 // TODO(johnme): Call ChromeMobileApplication.initCommandLine(context) or
249 // ChromeShellApplication.initCommandLine() as appropriate.
252 BrowserStartupController.get(context).startBrowserProcessesSync(false);
253 if (sInstance != null) {
256 Log.e(TAG, "Started browser process, but failed to instantiate GCMDriver.");
258 } catch (ProcessInitException e) {
259 Log.e(TAG, "Failed to start browser process.", e);
263 // TODO(johnme): Now we should probably exit?