mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
Bug 1214338 - Implement Android GCM-based PushService protocol. r=rnewman r=kitcambridge
MozReview-Commit-ID: 1KV7CZBuosx
This commit is contained in:
parent
6a26062006
commit
99ade81c39
@ -10,19 +10,26 @@ const Ci = Components.interfaces;
|
||||
const Cu = Components.utils;
|
||||
const Cr = Components.results;
|
||||
|
||||
const {PushDB} = Cu.import("resource://gre/modules/PushDB.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/Timer.jsm");
|
||||
Cu.import("resource://gre/modules/AppConstants.jsm");
|
||||
Cu.import("resource://gre/modules/Preferences.jsm");
|
||||
Cu.import("resource://gre/modules/Promise.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/Timer.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
||||
const {PushServiceWebSocket} = Cu.import("resource://gre/modules/PushServiceWebSocket.jsm");
|
||||
const {PushServiceHttp2} = Cu.import("resource://gre/modules/PushServiceHttp2.jsm");
|
||||
const {PushCrypto} = Cu.import("resource://gre/modules/PushCrypto.jsm");
|
||||
const {PushDB} = Cu.import("resource://gre/modules/PushDB.jsm");
|
||||
|
||||
// Currently supported protocols: WebSocket.
|
||||
const CONNECTION_PROTOCOLS = [PushServiceWebSocket, PushServiceHttp2];
|
||||
const CONNECTION_PROTOCOLS = (function() {
|
||||
if ('android' != AppConstants.MOZ_WIDGET_TOOLKIT) {
|
||||
const {PushServiceWebSocket} = Cu.import("resource://gre/modules/PushServiceWebSocket.jsm");
|
||||
const {PushServiceHttp2} = Cu.import("resource://gre/modules/PushServiceHttp2.jsm");
|
||||
return [PushServiceWebSocket, PushServiceHttp2];
|
||||
} else {
|
||||
const {PushServiceAndroidGCM} = Cu.import("resource://gre/modules/PushServiceAndroidGCM.jsm");
|
||||
return [PushServiceAndroidGCM];
|
||||
}
|
||||
})();
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "AlarmService",
|
||||
"resource://gre/modules/AlarmService.jsm");
|
||||
|
255
dom/push/PushServiceAndroidGCM.jsm
Normal file
255
dom/push/PushServiceAndroidGCM.jsm
Normal file
@ -0,0 +1,255 @@
|
||||
/* jshint moz: true, esnext: true */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
const Cc = Components.classes;
|
||||
const Ci = Components.interfaces;
|
||||
const Cu = Components.utils;
|
||||
const Cr = Components.results;
|
||||
|
||||
const {PushDB} = Cu.import("resource://gre/modules/PushDB.jsm");
|
||||
const {PushRecord} = Cu.import("resource://gre/modules/PushRecord.jsm");
|
||||
Cu.import("resource://gre/modules/Messaging.jsm"); /*global: Services */
|
||||
Cu.import("resource://gre/modules/Services.jsm"); /*global: Services */
|
||||
Cu.import("resource://gre/modules/Preferences.jsm"); /*global: Preferences */
|
||||
Cu.import("resource://gre/modules/Promise.jsm"); /*global: Promise */
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm"); /*global: XPCOMUtils */
|
||||
|
||||
const Log = Cu.import("resource://gre/modules/AndroidLog.jsm", {}).AndroidLog.bind("Push");
|
||||
|
||||
const {
|
||||
PushCrypto,
|
||||
base64UrlDecode,
|
||||
concatArray,
|
||||
getCryptoParams,
|
||||
} = Cu.import("resource://gre/modules/PushCrypto.jsm");
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["PushServiceAndroidGCM"];
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, "console", () => {
|
||||
let {ConsoleAPI} = Cu.import("resource://gre/modules/Console.jsm", {});
|
||||
return new ConsoleAPI({
|
||||
dump: Log.i,
|
||||
maxLogLevelPref: "dom.push.loglevel",
|
||||
prefix: "PushServiceAndroidGCM",
|
||||
});
|
||||
});
|
||||
|
||||
const kPUSHANDROIDGCMDB_DB_NAME = "pushAndroidGCM";
|
||||
const kPUSHANDROIDGCMDB_DB_VERSION = 5; // Change this if the IndexedDB format changes
|
||||
const kPUSHANDROIDGCMDB_STORE_NAME = "pushAndroidGCM";
|
||||
|
||||
const prefs = new Preferences("dom.push.");
|
||||
|
||||
/**
|
||||
* The implementation of WebPush push backed by Android's GCM
|
||||
* delivery.
|
||||
*/
|
||||
this.PushServiceAndroidGCM = {
|
||||
_mainPushService: null,
|
||||
_serverURI: null,
|
||||
|
||||
newPushDB: function() {
|
||||
return new PushDB(kPUSHANDROIDGCMDB_DB_NAME,
|
||||
kPUSHANDROIDGCMDB_DB_VERSION,
|
||||
kPUSHANDROIDGCMDB_STORE_NAME,
|
||||
"channelID",
|
||||
PushRecordAndroidGCM);
|
||||
},
|
||||
|
||||
serviceType: function() {
|
||||
return "AndroidGCM";
|
||||
},
|
||||
|
||||
validServerURI: function(serverURI) {
|
||||
if (!serverURI) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (serverURI.scheme == "https") {
|
||||
return true;
|
||||
}
|
||||
if (prefs.get("debug") && serverURI.scheme == "http") {
|
||||
// Accept HTTP endpoints when debugging.
|
||||
return true;
|
||||
}
|
||||
console.info("Unsupported Android GCM dom.push.serverURL scheme", serverURI.scheme);
|
||||
return false;
|
||||
},
|
||||
|
||||
observe: function(subject, topic, data) {
|
||||
if (topic == "nsPref:changed") {
|
||||
if (data == "dom.push.debug") {
|
||||
// Reconfigure.
|
||||
let debug = !!prefs.get("debug");
|
||||
console.info("Debug parameter changed; updating configuration with new debug", debug);
|
||||
this._configure(this._serverURI, debug);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (topic == "PushServiceAndroidGCM:ReceivedPushMessage") {
|
||||
// TODO: Use Messaging.jsm for this.
|
||||
if (this._mainPushService == null) {
|
||||
// Shouldn't ever happen, but let's be careful.
|
||||
console.error("No main PushService! Dropping message.");
|
||||
return;
|
||||
}
|
||||
if (!data) {
|
||||
console.error("No data from Java! Dropping message.");
|
||||
return;
|
||||
}
|
||||
data = JSON.parse(data);
|
||||
console.debug("ReceivedPushMessage with data", data);
|
||||
|
||||
// Default is no data (and no encryption).
|
||||
let message = null;
|
||||
let cryptoParams = null;
|
||||
|
||||
if (data.message && data.enc && (data.enckey || data.cryptokey)) {
|
||||
let headers = {
|
||||
encryption_key: data.enckey,
|
||||
crypto_key: data.cryptokey,
|
||||
encryption: data.enc,
|
||||
};
|
||||
cryptoParams = getCryptoParams(headers);
|
||||
// Ciphertext is (urlsafe) Base 64 encoded.
|
||||
message = base64UrlDecode(data.message);
|
||||
}
|
||||
|
||||
console.debug("Delivering message to main PushService:", message, cryptoParams);
|
||||
this._mainPushService.receivedPushMessage(
|
||||
data.channelID, message, cryptoParams, (record) => {
|
||||
// Always update the stored record.
|
||||
return record;
|
||||
});
|
||||
return;
|
||||
}
|
||||
},
|
||||
|
||||
_configure: function(serverURL, debug) {
|
||||
return Messaging.sendRequestForResult({
|
||||
type: "PushServiceAndroidGCM:Configure",
|
||||
endpoint: serverURL.spec,
|
||||
debug: debug,
|
||||
});
|
||||
},
|
||||
|
||||
init: function(options, mainPushService, serverURL) {
|
||||
console.debug("init()");
|
||||
this._mainPushService = mainPushService;
|
||||
this._serverURI = serverURL;
|
||||
|
||||
prefs.observe("debug", this);
|
||||
Services.obs.addObserver(this, "PushServiceAndroidGCM:ReceivedPushMessage", false);
|
||||
|
||||
return this._configure(serverURL, !!prefs.get("debug"));
|
||||
},
|
||||
|
||||
uninit: function() {
|
||||
console.debug("uninit()");
|
||||
this._mainPushService = null;
|
||||
Services.obs.removeObserver(this, "PushServiceAndroidGCM:ReceivedPushMessage");
|
||||
prefs.ignore("debug", this);
|
||||
},
|
||||
|
||||
onAlarmFired: function() {
|
||||
// No action required.
|
||||
},
|
||||
|
||||
connect: function(records) {
|
||||
console.debug("connect:", records);
|
||||
// It's possible for the registration or subscriptions backing the
|
||||
// PushService to not be registered with the underlying AndroidPushService.
|
||||
// Expire those that are unrecognized.
|
||||
return Messaging.sendRequestForResult({
|
||||
type: "PushServiceAndroidGCM:DumpSubscriptions",
|
||||
})
|
||||
.then(subscriptions => {
|
||||
console.debug("connect:", subscriptions);
|
||||
// subscriptions maps chid => subscription data.
|
||||
return Promise.all(records.map(record => {
|
||||
if (subscriptions.hasOwnProperty(record.keyID)) {
|
||||
console.debug("connect:", "hasOwnProperty", record.keyID);
|
||||
return Promise.resolve();
|
||||
}
|
||||
console.debug("connect:", "!hasOwnProperty", record.keyID);
|
||||
// Subscription is known to PushService.jsm but not to AndroidPushService. Drop it.
|
||||
return this._mainPushService.dropRegistrationAndNotifyApp(record.keyID)
|
||||
.catch(error => {
|
||||
console.error("connect: Error dropping registration", record.keyID, error);
|
||||
});
|
||||
}));
|
||||
});
|
||||
},
|
||||
|
||||
isConnected: function() {
|
||||
return this._mainPushService != null;
|
||||
},
|
||||
|
||||
disconnect: function() {
|
||||
console.debug("disconnect");
|
||||
},
|
||||
|
||||
request: function(action, record) {
|
||||
switch (action) {
|
||||
case "register":
|
||||
console.debug("register:", record);
|
||||
return this._register(record);
|
||||
case "unregister":
|
||||
console.debug("unregister: ", record);
|
||||
return this._unregister(record);
|
||||
default:
|
||||
console.debug("Ignoring unrecognized request action:", action);
|
||||
}
|
||||
},
|
||||
|
||||
_register: function(record) {
|
||||
let ctime = Date.now();
|
||||
// Caller handles errors.
|
||||
return Messaging.sendRequestForResult({
|
||||
type: "PushServiceAndroidGCM:SubscribeChannel",
|
||||
}).then(data => {
|
||||
console.debug("Got data:", data);
|
||||
return PushCrypto.generateKeys()
|
||||
.then(exportedKeys =>
|
||||
new PushRecordAndroidGCM({
|
||||
// Straight from autopush.
|
||||
channelID: data.channelID,
|
||||
pushEndpoint: data.endpoint,
|
||||
// Common to all PushRecord implementations.
|
||||
scope: record.scope,
|
||||
originAttributes: record.originAttributes,
|
||||
ctime: ctime,
|
||||
// Cryptography!
|
||||
p256dhPublicKey: exportedKeys[0],
|
||||
p256dhPrivateKey: exportedKeys[1],
|
||||
authenticationSecret: PushCrypto.generateAuthenticationSecret(),
|
||||
})
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
_unregister: function(record) {
|
||||
return Messaging.sendRequestForResult({
|
||||
type: "PushServiceAndroidGCM:UnsubscribeChannel",
|
||||
channelID: record.keyID,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
function PushRecordAndroidGCM(record) {
|
||||
PushRecord.call(this, record);
|
||||
this.channelID = record.channelID;
|
||||
}
|
||||
|
||||
PushRecordAndroidGCM.prototype = Object.create(PushRecord.prototype, {
|
||||
keyID: {
|
||||
get() {
|
||||
return this.channelID;
|
||||
},
|
||||
},
|
||||
});
|
@ -14,10 +14,20 @@ EXTRA_JS_MODULES += [
|
||||
'PushDB.jsm',
|
||||
'PushRecord.jsm',
|
||||
'PushService.jsm',
|
||||
'PushServiceHttp2.jsm',
|
||||
'PushServiceWebSocket.jsm',
|
||||
]
|
||||
|
||||
if CONFIG['MOZ_BUILD_APP'] != 'mobile/android':
|
||||
# Everything but Fennec.
|
||||
EXTRA_JS_MODULES += [
|
||||
'PushServiceHttp2.jsm',
|
||||
'PushServiceWebSocket.jsm',
|
||||
]
|
||||
else:
|
||||
# Fennec only.
|
||||
EXTRA_JS_MODULES += [
|
||||
'PushServiceAndroidGCM.jsm',
|
||||
]
|
||||
|
||||
MOCHITEST_MANIFESTS += [
|
||||
'test/mochitest.ini',
|
||||
]
|
||||
|
@ -175,6 +175,7 @@ public class GeckoApplication extends Application
|
||||
// network access, so is naturally asynchronous. This, of course, races against Gecko page load of
|
||||
// content requiring GCM-backed services, like Web Push. There's nothing to be done here.
|
||||
PushService.createInstance(context);
|
||||
PushService.registerGeckoEventListener();
|
||||
|
||||
try {
|
||||
PushService.getInstance().onStartup();
|
||||
|
@ -34,13 +34,23 @@ import java.util.Map;
|
||||
* <p/>
|
||||
* It's worth noting that we allow the DOM push API in restricted profiles.
|
||||
*/
|
||||
public class PushService {
|
||||
public class PushService implements BundleEventListener {
|
||||
private static final String LOG_TAG = "GeckoPushService";
|
||||
|
||||
public static final String SERVICE_WEBPUSH = "webpush";
|
||||
|
||||
private static PushService sInstance;
|
||||
|
||||
private static final String[] GECKO_EVENTS = new String[]{
|
||||
"PushServiceAndroidGCM:Configure",
|
||||
"PushServiceAndroidGCM:DumpRegistration",
|
||||
"PushServiceAndroidGCM:DumpSubscriptions",
|
||||
"PushServiceAndroidGCM:RegisterUserAgent",
|
||||
"PushServiceAndroidGCM:UnregisterUserAgent",
|
||||
"PushServiceAndroidGCM:SubscribeChannel",
|
||||
"PushServiceAndroidGCM:UnsubscribeChannel",
|
||||
};
|
||||
|
||||
public static synchronized PushService getInstance() {
|
||||
if (sInstance == null) {
|
||||
throw new IllegalStateException("PushService not yet created!");
|
||||
@ -87,7 +97,7 @@ public class PushService {
|
||||
try {
|
||||
pushManager.startup(System.currentTimeMillis());
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "Got exception during startup; ignoring.", e);
|
||||
Log.e(LOG_TAG, "Got exception during refresh; ignoring.", e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -119,11 +129,169 @@ public class PushService {
|
||||
Log.i(LOG_TAG, "Message directed to service: " + subscription.service);
|
||||
|
||||
if (SERVICE_WEBPUSH.equals(subscription.service)) {
|
||||
// Nothing yet.
|
||||
Log.i(LOG_TAG, "Message directed to unimplemented service; ignoring: " + subscription.service);
|
||||
return;
|
||||
if (subscription.serviceData == null) {
|
||||
Log.e(LOG_TAG, "No serviceData found for chid: " + chid + "; ignoring dom/push message.");
|
||||
return;
|
||||
}
|
||||
|
||||
final String profileName = subscription.serviceData.optString("profileName", null);
|
||||
final String profilePath = subscription.serviceData.optString("profilePath", null);
|
||||
if (profileName == null || profilePath == null) {
|
||||
Log.e(LOG_TAG, "Corrupt serviceData found for chid: " + chid + "; ignoring dom/push message.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!GeckoThread.isRunning()) {
|
||||
Log.w(LOG_TAG, "dom/push message received but no Gecko thread is running; ignoring message.");
|
||||
return;
|
||||
}
|
||||
|
||||
final GeckoAppShell.GeckoInterface geckoInterface = GeckoAppShell.getGeckoInterface();
|
||||
if (geckoInterface == null) {
|
||||
Log.w(LOG_TAG, "dom/push message received but no Gecko interface is registered; ignoring message.");
|
||||
return;
|
||||
}
|
||||
|
||||
final GeckoProfile profile = geckoInterface.getProfile();
|
||||
if (profile == null || !profileName.equals(profile.getName()) || !profilePath.equals(profile.getDir().getAbsolutePath())) {
|
||||
Log.w(LOG_TAG, "dom/push message received but Gecko is running with the wrong profile name or path; ignoring message.");
|
||||
return;
|
||||
}
|
||||
|
||||
// DELIVERANCE!
|
||||
final JSONObject data = new JSONObject();
|
||||
try {
|
||||
data.put("channelID", chid);
|
||||
data.put("enc", bundle.getString("enc"));
|
||||
// Only one of cryptokey (newer) and enckey (deprecated) should be set, but the
|
||||
// Gecko handler will verify this.
|
||||
data.put("cryptokey", bundle.getString("cryptokey"));
|
||||
data.put("enckey", bundle.getString("enckey"));
|
||||
data.put("message", bundle.getString("body"));
|
||||
} catch (JSONException e) {
|
||||
Log.e(LOG_TAG, "Got exception delivering dom/push message to Gecko!", e);
|
||||
return;
|
||||
}
|
||||
|
||||
Log.i(LOG_TAG, "Delivering dom/push message to Gecko!");
|
||||
GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("PushServiceAndroidGCM:ReceivedPushMessage", data.toString()));
|
||||
} else {
|
||||
Log.e(LOG_TAG, "Message directed to unknown service; dropping: " + subscription.service);
|
||||
}
|
||||
}
|
||||
|
||||
public static void registerGeckoEventListener() {
|
||||
Log.d(LOG_TAG, "Registered Gecko event listener.");
|
||||
EventDispatcher.getInstance().registerBackgroundThreadListener(getInstance(), GECKO_EVENTS);
|
||||
}
|
||||
|
||||
public static void unregisterGeckoEventListener() {
|
||||
Log.d(LOG_TAG, "Unregistered Gecko event listener.");
|
||||
EventDispatcher.getInstance().unregisterBackgroundThreadListener(getInstance(), GECKO_EVENTS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(final String event, final Bundle message, final EventCallback callback) {
|
||||
Log.i(LOG_TAG, "Handling event: " + event);
|
||||
ThreadUtils.assertOnBackgroundThread();
|
||||
|
||||
// We're invoked in response to a Gecko message on a background thread. We should always
|
||||
// be able to safely retrieve the current Gecko profile.
|
||||
final GeckoProfile geckoProfile = GeckoProfile.get(GeckoAppShell.getApplicationContext());
|
||||
|
||||
if (callback == null) {
|
||||
Log.e(LOG_TAG, "callback must not be null in " + event);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if ("PushServiceAndroidGCM:Configure".equals(event)) {
|
||||
final String endpoint = message.getString("endpoint");
|
||||
if (endpoint == null) {
|
||||
Log.e(LOG_TAG, "endpoint must not be null in " + event);
|
||||
return;
|
||||
}
|
||||
final boolean debug = message.getBoolean("debug", false);
|
||||
pushManager.configure(geckoProfile.getName(), endpoint, debug, System.currentTimeMillis()); // For side effects.
|
||||
callback.sendSuccess(null);
|
||||
return;
|
||||
}
|
||||
if ("PushServiceAndroidGCM:DumpRegistration".equals(event)) {
|
||||
callback.sendError("Not yet implemented!");
|
||||
return;
|
||||
}
|
||||
if ("PushServiceAndroidGCM:DumpSubscriptions".equals(event)) {
|
||||
try {
|
||||
final Map<String, PushSubscription> result = pushManager.allSubscriptionsForProfile(geckoProfile.getName());
|
||||
|
||||
final JSONObject json = new JSONObject();
|
||||
for (Map.Entry<String, PushSubscription> entry : result.entrySet()) {
|
||||
json.put(entry.getKey(), entry.getValue().toJSONObject());
|
||||
}
|
||||
callback.sendSuccess(json);
|
||||
} catch (JSONException e) {
|
||||
callback.sendError("Got exception handling message [" + event + "]: " + e.toString());
|
||||
}
|
||||
return;
|
||||
}
|
||||
if ("PushServiceAndroidGCM:RegisterUserAgent".equals(event)) {
|
||||
try {
|
||||
pushManager.registerUserAgent(geckoProfile.getName(), System.currentTimeMillis()); // For side-effects.
|
||||
callback.sendSuccess(null);
|
||||
} catch (PushManager.ProfileNeedsConfigurationException | AutopushClientException | PushClient.LocalException | IOException e) {
|
||||
Log.e(LOG_TAG, "Got exception in " + event, e);
|
||||
callback.sendError("Got exception handling message [" + event + "]: " + e.toString());
|
||||
}
|
||||
return;
|
||||
}
|
||||
if ("PushServiceAndroidGCM:UnregisterUserAgent".equals(event)) {
|
||||
callback.sendError("Not yet implemented!");
|
||||
return;
|
||||
}
|
||||
if ("PushServiceAndroidGCM:SubscribeChannel".equals(event)) {
|
||||
final String service = SERVICE_WEBPUSH;
|
||||
final JSONObject serviceData;
|
||||
try {
|
||||
serviceData = new JSONObject();
|
||||
serviceData.put("profileName", geckoProfile.getName());
|
||||
serviceData.put("profilePath", geckoProfile.getDir().getAbsolutePath());
|
||||
} catch (JSONException e) {
|
||||
Log.e(LOG_TAG, "Got exception in " + event, e);
|
||||
callback.sendError("Got exception handling message [" + event + "]: " + e.toString());
|
||||
return;
|
||||
}
|
||||
|
||||
final PushSubscription subscription;
|
||||
try {
|
||||
subscription = pushManager.subscribeChannel(geckoProfile.getName(), service, serviceData, System.currentTimeMillis());
|
||||
} catch (PushManager.ProfileNeedsConfigurationException | AutopushClientException | PushClient.LocalException | IOException e) {
|
||||
Log.e(LOG_TAG, "Got exception in " + event, e);
|
||||
callback.sendError("Got exception handling message [" + event + "]: " + e.toString());
|
||||
return;
|
||||
}
|
||||
|
||||
final JSONObject json = new JSONObject();
|
||||
try {
|
||||
json.put("channelID", subscription.chid);
|
||||
json.put("endpoint", subscription.webpushEndpoint);
|
||||
} catch (JSONException e) {
|
||||
Log.e(LOG_TAG, "Got exception in " + event, e);
|
||||
callback.sendError("Got exception handling message [" + event + "]: " + e.toString());
|
||||
return;
|
||||
}
|
||||
callback.sendSuccess(json);
|
||||
return;
|
||||
}
|
||||
if ("PushServiceAndroidGCM:UnsubscribeChannel".equals(event)) {
|
||||
callback.sendError("Not yet implemented!");
|
||||
return;
|
||||
}
|
||||
} catch (GcmTokenClient.NeedsGooglePlayServicesException e) {
|
||||
// TODO: improve this. Can we find a point where the user is *definitely* interacting
|
||||
// with the WebPush? Perhaps we can show a dialog when interacting with the Push
|
||||
// permissions, and then be more aggressive showing this notification when we have
|
||||
// registrations and subscriptions that can't be advanced.
|
||||
callback.sendError("To handle event [" + event + "], user interaction is needed to enable Google Play Services.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user