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. 4 5package org.chromium.components.gcm_driver; 6 7import android.content.Context; 8import android.content.SharedPreferences; 9import android.os.AsyncTask; 10import android.os.Bundle; 11import android.preference.PreferenceManager; 12import android.util.Log; 13 14import com.google.android.gcm.GCMRegistrar; 15 16import org.chromium.base.CalledByNative; 17import org.chromium.base.JNINamespace; 18import org.chromium.base.ThreadUtils; 19import org.chromium.base.library_loader.ProcessInitException; 20import org.chromium.content.browser.BrowserStartupController; 21 22import java.util.ArrayList; 23import java.util.List; 24 25/** 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. 29 * 30 * Threading model: all calls to/from C++ happen on the UI thread. 31 */ 32@JNINamespace("gcm") 33public class GCMDriver { 34 private static final String TAG = "GCMDriver"; 35 36 private static final String LAST_GCM_APP_ID_KEY = "last_gcm_app_id"; 37 38 // The instance of GCMDriver currently owned by a C++ GCMDriverAndroid, if any. 39 private static GCMDriver sInstance = null; 40 41 private long mNativeGCMDriverAndroid; 42 private final Context mContext; 43 44 private GCMDriver(long nativeGCMDriverAndroid, Context context) { 45 mNativeGCMDriverAndroid = nativeGCMDriverAndroid; 46 mContext = context; 47 } 48 49 /** 50 * Create a GCMDriver object, which is owned by GCMDriverAndroid 51 * on the C++ side. 52 * 53 * @param nativeGCMDriverAndroid The C++ object that owns us. 54 * @param context The app context. 55 */ 56 @CalledByNative 57 private static GCMDriver create(long nativeGCMDriverAndroid, 58 Context context) { 59 if (sInstance != null) { 60 throw new IllegalStateException("Already instantiated"); 61 } 62 sInstance = new GCMDriver(nativeGCMDriverAndroid, context); 63 return sInstance; 64 } 65 66 /** 67 * Called when our C++ counterpart is deleted. Clear the handle to our 68 * native C++ object, ensuring it's never called. 69 */ 70 @CalledByNative 71 private void destroy() { 72 assert sInstance == this; 73 sInstance = null; 74 mNativeGCMDriverAndroid = 0; 75 } 76 77 @CalledByNative 78 private void register(final String appId, final String[] senderIds) { 79 setLastAppId(appId); 80 new AsyncTask<Void, Void, String>() { 81 @Override 82 protected String doInBackground(Void... voids) { 83 try { 84 GCMRegistrar.checkDevice(mContext); 85 } catch (UnsupportedOperationException ex) { 86 return ""; // Indicates failure. 87 } 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. 97 } else { 98 Log.i(TAG, "Re-using existing registration ID"); 99 return existingRegistrationId; 100 } 101 } 102 @Override 103 protected void onPostExecute(String registrationId) { 104 if (registrationId == null) { 105 return; // Wait for {@link #onRegisterFinished} to be called. 106 } 107 nativeOnRegisterFinished(mNativeGCMDriverAndroid, appId, registrationId, 108 !registrationId.isEmpty()); 109 } 110 }.execute(); 111 } 112 113 private enum UnregisterResult { SUCCESS, FAILED, PENDING } 114 115 @CalledByNative 116 private void unregister(final String appId) { 117 new AsyncTask<Void, Void, UnregisterResult>() { 118 @Override 119 protected UnregisterResult doInBackground(Void... voids) { 120 try { 121 GCMRegistrar.checkDevice(mContext); 122 } catch (UnsupportedOperationException ex) { 123 return UnregisterResult.FAILED; 124 } 125 if (!GCMRegistrar.isRegistered(mContext)) { 126 return UnregisterResult.SUCCESS; 127 } 128 // TODO(johnme): Pass appId to GCM. 129 GCMRegistrar.unregister(mContext); 130 return UnregisterResult.PENDING; 131 } 132 133 @Override 134 protected void onPostExecute(UnregisterResult result) { 135 if (result == UnregisterResult.PENDING) { 136 return; // Wait for {@link #onUnregisterFinished} to be called. 137 } 138 nativeOnUnregisterFinished(mNativeGCMDriverAndroid, appId, 139 result == UnregisterResult.SUCCESS); 140 } 141 }.execute(); 142 } 143 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); 151 } 152 } 153 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(), 160 true); 161 } 162 } 163 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). 177 return; 178 } 179 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."; 187 188 String senderId = extras.getString(bundleSenderId); 189 String collapseKey = extras.getString(bundleCollapseKey); 190 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)) 196 continue; 197 dataKeysAndValues.add(key); 198 dataKeysAndValues.add(extras.getString(key)); 199 } 200 201 sInstance.nativeOnMessageReceived(sInstance.mNativeGCMDriverAndroid, 202 getLastAppId(), senderId, collapseKey, 203 dataKeysAndValues.toArray(new String[dataKeysAndValues.size()])); 204 } 205 }); 206 } 207 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, 214 getLastAppId()); 215 } 216 }); 217 } 218 219 private native void nativeOnRegisterFinished(long nativeGCMDriverAndroid, String appId, 220 String registrationId, boolean success); 221 private native void nativeOnUnregisterFinished(long nativeGCMDriverAndroid, String appId, 222 boolean success); 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); 226 227 // TODO(johnme): This and setLastAppId are just temporary (crbug.com/350383). 228 private static String getLastAppId() { 229 SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences( 230 sInstance.mContext); 231 return settings.getString(LAST_GCM_APP_ID_KEY, "push#unknown_app_id#0"); 232 } 233 234 private static void setLastAppId(String appId) { 235 SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences( 236 sInstance.mContext); 237 SharedPreferences.Editor editor = settings.edit(); 238 editor.putString(LAST_GCM_APP_ID_KEY, appId); 239 editor.commit(); 240 } 241 242 private static void launchNativeThen(Context context, Runnable task) { 243 if (sInstance != null) { 244 task.run(); 245 return; 246 } 247 248 // TODO(johnme): Call ChromeMobileApplication.initCommandLine(context) or 249 // ChromeShellApplication.initCommandLine() as appropriate. 250 251 try { 252 BrowserStartupController.get(context).startBrowserProcessesSync(false); 253 if (sInstance != null) { 254 task.run(); 255 } else { 256 Log.e(TAG, "Started browser process, but failed to instantiate GCMDriver."); 257 } 258 } catch (ProcessInitException e) { 259 Log.e(TAG, "Failed to start browser process.", e); 260 System.exit(-1); 261 } 262 263 // TODO(johnme): Now we should probably exit? 264 } 265} 266