diff --git a/sky/services/gcm/BUILD.gn b/sky/services/gcm/BUILD.gn new file mode 100644 index 0000000000000..dfc8236b5b6d7 --- /dev/null +++ b/sky/services/gcm/BUILD.gn @@ -0,0 +1,46 @@ +# Copyright 2015 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. + +# TODO(mpcomplete): move this to a separate repo. Doesn't belong in the main +# Flutter repo. + +import("//mojo/public/tools/bindings/mojom.gni") + +group("gcm") { + deps = [ + ":interfaces", + ] + + if (is_android) { + deps += [ ":gcm_lib" ] + } +} + +mojom("interfaces") { + sources = [ + "gcm.mojom", + ] +} + +if (is_android) { + import("//build/config/android/config.gni") + import("//build/config/android/rules.gni") + + android_library("gcm_lib") { + java_files = [ + "src/org/domokit/gcm/GcmListenerService.java", + "src/org/domokit/gcm/InstanceIDListenerService.java", + "src/org/domokit/gcm/RegistrationIntentService.java", + ] + + deps = [ + "//base:base_java", + "//mojo/java", + "//mojo/public/java:bindings", + "//mojo/public/java:system", + ":interfaces_java", + google_play_services_library, + ] + } +} diff --git a/sky/services/gcm/gcm.mojom b/sky/services/gcm/gcm.mojom new file mode 100644 index 0000000000000..ba04c228663e2 --- /dev/null +++ b/sky/services/gcm/gcm.mojom @@ -0,0 +1,21 @@ +// Copyright 2015 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. + +[DartPackage="sky_services"] +module gcm; + +[ServiceName="gcm::GcmListener"] +interface GcmListener { + OnMessageReceived(string from, string jsonMessage); +}; + +[ServiceName="gcm::GcmService"] +interface GcmService { + // senderId is the project_number from google-services.json. + // token should be saved and remembered - you only need to register your app + // once. I don't understand how this token is used, though. + Register(string senderId, GcmListener listener) => (string token); + SubscribeTopics(string token, array topics); + UnsubscribeTopics(string token, array topics); +}; diff --git a/sky/services/gcm/src/org/domokit/gcm/GcmListenerService.java b/sky/services/gcm/src/org/domokit/gcm/GcmListenerService.java new file mode 100644 index 0000000000000..d8e752ab288d8 --- /dev/null +++ b/sky/services/gcm/src/org/domokit/gcm/GcmListenerService.java @@ -0,0 +1,32 @@ +// Copyright 2015 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.domokit.gcm; + +import android.os.Bundle; +import android.util.Log; + +import java.util.Set; +import org.json.JSONObject; +import org.json.JSONException; + +public class GcmListenerService extends com.google.android.gms.gcm.GcmListenerService { + private static final String TAG = "GcmListenerService"; + + @Override + public void onMessageReceived(String from, Bundle data) { + // Convert the data Bundle to JSON. + JSONObject json = new JSONObject(); + Set keys = data.keySet(); + for (String key : keys) { + try { + json.put(key, JSONObject.wrap(data.get(key))); + } catch(JSONException e) { + Log.d(TAG, "Failed to convert GCM message to JSON: " + e); + } + } + + RegistrationIntentService.notifyMessageReceived(from, json.toString()); + } +} diff --git a/sky/services/gcm/src/org/domokit/gcm/InstanceIDListenerService.java b/sky/services/gcm/src/org/domokit/gcm/InstanceIDListenerService.java new file mode 100644 index 0000000000000..a022371338910 --- /dev/null +++ b/sky/services/gcm/src/org/domokit/gcm/InstanceIDListenerService.java @@ -0,0 +1,27 @@ +// Copyright 2015 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.domokit.gcm; + +import android.content.Intent; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; +import android.util.Log; + +import com.google.android.gms.iid.InstanceID; + +public class InstanceIDListenerService extends com.google.android.gms.iid.InstanceIDListenerService { + /** + * Called if InstanceID token is updated. This may occur if the security of + * the previous token had been compromised. This call is initiated by the + * InstanceID provider. + */ + @Override + public void onTokenRefresh() { + // TODO(mpcomplete): need to notify Dart app? + // Fetch updated Instance ID token and notify our app's server of any changes (if applicable). + Intent intent = new Intent(this, RegistrationIntentService.class); + startService(intent); + } +} diff --git a/sky/services/gcm/src/org/domokit/gcm/RegistrationIntentService.java b/sky/services/gcm/src/org/domokit/gcm/RegistrationIntentService.java new file mode 100644 index 0000000000000..3bf8904bc9af5 --- /dev/null +++ b/sky/services/gcm/src/org/domokit/gcm/RegistrationIntentService.java @@ -0,0 +1,149 @@ +// Copyright 2015 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.domokit.gcm; + +import android.app.IntentService; +import android.content.Context; +import android.content.Intent; +import android.util.Log; + +import com.google.android.gms.gcm.GcmPubSub; +import com.google.android.gms.gcm.GoogleCloudMessaging; +import com.google.android.gms.iid.InstanceID; + +import java.io.IOException; + +import org.chromium.base.ThreadUtils; +import org.chromium.mojo.system.MojoException; +import org.chromium.mojom.gcm.GcmListener; +import org.chromium.mojom.gcm.GcmService; + +public class RegistrationIntentService extends IntentService { + private static final String TAG = "RegistrationIntentService"; + private static final String REGISTER_EXTRA = "register"; + private static final String TOKEN_EXTRA = "token"; + private static final String SUBSCRIBE_EXTRA = "subscribe"; + private static final String UNSUBSCRIBE_EXTRA = "unsubscribe"; + + private static GcmListener sListener; + private static GcmService.RegisterResponse sRegisterResponse; + + public static class MojoService implements GcmService { + private Context context; + + public MojoService(Context context) { + this.context = context; + } + + @Override + public void close() {} + + @Override + public void onConnectionError(MojoException e) {} + + @Override + public void register(String senderId, GcmListener listener, RegisterResponse response) { + if (checkPlayServices()) { + // Start IntentService to register this application with GCM. + RegistrationIntentService.sListener = listener; + RegistrationIntentService.sRegisterResponse = response; + Intent intent = new Intent(context, RegistrationIntentService.class); + intent.putExtra(REGISTER_EXTRA, senderId); + context.startService(intent); + } + } + + public void subscribeTopics(String token, String[] topics) { + Intent intent = new Intent(context, RegistrationIntentService.class); + intent.putExtra(TOKEN_EXTRA, token); + intent.putExtra(SUBSCRIBE_EXTRA, topics); + context.startService(intent); + } + + public void unsubscribeTopics(String token, String[] topics) { + Intent intent = new Intent(context, RegistrationIntentService.class); + intent.putExtra(TOKEN_EXTRA, token); + intent.putExtra(UNSUBSCRIBE_EXTRA, topics); + context.startService(intent); + } + + private boolean checkPlayServices() { + // TODO(mpcomplete): implement? This would check if the user has the Google Play Services + // library installed, and if not, prompt them to download it. + return true; + } + } + + public RegistrationIntentService() { + super(TAG); + } + + public static void notifyMessageReceived(final String from, final String message) { + ThreadUtils.runOnUiThread(new Runnable() { + @Override + public void run() { + if (sListener != null) + sListener.onMessageReceived(from, message); + } + }); + } + + public static void notifyRegistered(final String token) { + ThreadUtils.runOnUiThread(new Runnable() { + @Override + public void run() { + if (sRegisterResponse != null) + sRegisterResponse.call(token); + } + }); + } + + @Override + protected void onHandleIntent(Intent intent) { + try { + if (intent.hasExtra(REGISTER_EXTRA)) { + register(intent.getStringExtra(REGISTER_EXTRA)); + } else if (intent.hasExtra(SUBSCRIBE_EXTRA)) { + subscribeTopics( + intent.getStringExtra(TOKEN_EXTRA), + intent.getStringArrayExtra(SUBSCRIBE_EXTRA)); + } else if (intent.hasExtra(UNSUBSCRIBE_EXTRA)) { + unsubscribeTopics( + intent.getStringExtra(TOKEN_EXTRA), + intent.getStringArrayExtra(UNSUBSCRIBE_EXTRA)); + } else { + Log.d(TAG, "Unexpected intent."); + } + } catch (Exception e) { + Log.d(TAG, "Failed to process GCM request", e); + } + } + + private void register(String senderId) { + try { + InstanceID instanceID = InstanceID.getInstance(this); + String token = instanceID.getToken(senderId, + GoogleCloudMessaging.INSTANCE_ID_SCOPE, null); + notifyRegistered(token); + } catch (Exception e) { + Log.d(TAG, "Failed to complete token refresh", e); + // TODO(mpcomplete): callback error code. + } + } + + private void subscribeTopics(String token, String[] topics) throws IOException { + GcmPubSub pubSub = GcmPubSub.getInstance(this); + for (String topic : topics) { + pubSub.subscribe(token, "/topics/" + topic, null); + } + } + + private void unsubscribeTopics(String token, String[] topics) throws IOException { + GcmPubSub pubSub = GcmPubSub.getInstance(this); + for (String topic : topics) { + pubSub.unsubscribe(token, "/topics/" + topic); + } + } +}