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