ParseApplication/ExternalLibs/Parse-SDK-Android/Parse/src/main/java/com/parse/GcmRegistrar.java

406 lines
15 KiB
Java

/*
* Copyright (c) 2015-present, Parse, LLC.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.parse;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.os.SystemClock;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.Callable;
import java.util.concurrent.atomic.AtomicInteger;
import bolts.Continuation;
import bolts.Task;
import bolts.TaskCompletionSource;
/**
* A class that manages registering for GCM and updating the registration if it is out of date,
* used by {@link com.parse.GcmPushHandler}.
*/
/** package */ class GcmRegistrar {
private static final String TAG = "com.parse.GcmRegistrar";
private static final String REGISTRATION_ID_EXTRA = "registration_id";
private static final String ERROR_EXTRA = "error";
private static final String SENDER_ID_EXTRA = "com.parse.push.gcm_sender_id";
public static final String REGISTER_ACTION = "com.google.android.c2dm.intent.REGISTER";
private static final String FILENAME_DEVICE_TOKEN_LAST_MODIFIED = "deviceTokenLastModified";
private long localDeviceTokenLastModified;
private final Object localDeviceTokenLastModifiedMutex = new Object();
public static GcmRegistrar getInstance() {
return Singleton.INSTANCE;
}
private static class Singleton {
public static final GcmRegistrar INSTANCE = new GcmRegistrar(Parse.getApplicationContext());
}
private static String actualSenderIDFromExtra(Object senderIDExtra) {
if (!(senderIDExtra instanceof String)) {
return null;
}
String senderID = (String)senderIDExtra;
if (!senderID.startsWith("id:")) {
return null;
}
return senderID.substring(3);
}
private final Object lock = new Object();
private Request request = null;
private Context context = null;
// This a package-level constructor for unit testing only. Otherwise, use getInstance().
/* package */ GcmRegistrar(Context context) {
this.context = context;
}
/**
* Does nothing if the client already has a valid GCM registration id. Otherwise, sends out a
* GCM registration request and saves the resulting registration id to the server via
* ParseInstallation.
*/
public Task<Void> registerAsync() {
if (ManifestInfo.getPushType() != PushType.GCM) {
return Task.forResult(null);
}
synchronized (lock) {
/*
* If we don't yet have a device token, mark this installation as wanting to use GCM by
* setting its pushType to GCM. If the registration does not succeed (because the device
* is offline, for instance), then update() will re-register for a GCM device token at
* next app initialize time.
*/
final ParseInstallation installation = ParseInstallation.getCurrentInstallation();
// Check whether we need to send registration request, if installation does not
// have device token or local device token is stale, we need to send request.
Task<Boolean> checkTask = installation.getDeviceToken() == null
? Task.forResult(true)
: isLocalDeviceTokenStaleAsync();
return checkTask.onSuccessTask(new Continuation<Boolean, Task<Void>>() {
@Override
public Task<Void> then(Task<Boolean> task) throws Exception {
if (!task.getResult()) {
return Task.forResult(null);
}
if (installation.getPushType() != PushType.GCM) {
installation.setPushType(PushType.GCM);
}
// We do not need to wait sendRegistrationRequestAsync, since this task will finish
// after we get the response from GCM, if we wait for this task, it will block our test.
sendRegistrationRequestAsync();
return Task.forResult(null);
}
});
}
}
private Task<Void> sendRegistrationRequestAsync() {
synchronized (lock) {
if (request != null) {
return Task.forResult(null);
}
// Look for an element like this as a child of the <application> element:
//
// <meta-data android:name="com.parse.push.gcm_sender_id"
// android:value="id:567327206255" />
//
// The reason why the "id:" prefix is necessary is because Android treats any metadata value
// that is a string of digits as an integer. So the call to Bundle.getString() will actually
// return null for `android:value="567327206255"`. Additionally, Bundle.getInteger() returns
// a 32-bit integer. For `android:value="567327206255"`, this returns a truncated integer
// because 567327206255 is larger than the largest 32-bit integer.
Bundle metaData = ManifestInfo.getApplicationMetadata(context);
String senderID = null;
if (metaData != null) {
Object senderIDExtra = metaData.get(SENDER_ID_EXTRA);
if (senderIDExtra != null) {
senderID = actualSenderIDFromExtra(senderIDExtra);
if (senderID == null) {
PLog.e(TAG, "Found " + SENDER_ID_EXTRA + " <meta-data> element with value \"" +
senderIDExtra.toString() + "\", but the value is missing the expected \"id:\" " +
"prefix.");
return null;
}
}
}
if (senderID == null) {
PLog.e(TAG, "You must provide " + SENDER_ID_EXTRA + " in your AndroidManifest.xml\n" +
"Make sure to prefix with the value with id:\n\n" +
"<meta-data\n" +
" android:name=\"com.parse.push.gcm_sender_id\"\n" +
" android:value=\"id:<YOUR_GCM_SENDER_ID>\" />");
return null;
}
request = Request.createAndSend(context, senderID);
return request.getTask().continueWith(new Continuation<String, Void>() {
@Override
public Void then(Task<String> task) {
Exception e = task.getError();
if (e != null) {
PLog.e(TAG, "Got error when trying to register for GCM push", e);
}
synchronized (lock) {
request = null;
}
return null;
}
});
}
}
/**
* Should be called by a broadcast receiver or service to handle the GCM registration response
* intent (com.google.android.c2dm.intent.REGISTRATION).
*/
Task<Void> handleRegistrationIntentAsync(Intent intent) {
List<Task<Void>> tasks = new ArrayList<>();
/*
* We have to parse the response here because GCM may send us a new registration_id
* out-of-band without a request in flight.
*/
String registrationId = intent.getStringExtra(REGISTRATION_ID_EXTRA);
if (registrationId != null && registrationId.length() > 0) {
PLog.v(TAG, "Received deviceToken <" + registrationId + "> from GCM.");
ParseInstallation installation = ParseInstallation.getCurrentInstallation();
// Compare the new deviceToken with the old deviceToken, we only update the
// deviceToken if the new one is different from the old one. This does not follow google
// guide strictly. But we find most of the time if user just update the app, the
// registrationId does not change so there is no need to save it again.
if (!registrationId.equals(installation.getDeviceToken())) {
installation.setPushType(PushType.GCM);
installation.setDeviceToken(registrationId);
tasks.add(installation.saveInBackground());
}
// We need to update the last modified even the deviceToken is the same. Otherwise when the
// app is opened again, isDeviceTokenStale() will always return false so we will send
// request to GCM every time.
tasks.add(updateLocalDeviceTokenLastModifiedAsync());
}
synchronized (lock) {
if (request != null) {
request.onReceiveResponseIntent(intent);
}
}
return Task.whenAll(tasks);
}
// Only used by tests.
/* package */ int getRequestIdentifier() {
synchronized (lock) {
return request != null ? request.identifier : 0;
}
}
/** package for tests */ Task<Boolean> isLocalDeviceTokenStaleAsync() {
return getLocalDeviceTokenLastModifiedAsync().onSuccessTask(new Continuation<Long, Task<Boolean>>() {
@Override
public Task<Boolean> then(Task<Long> task) throws Exception {
long localDeviceTokenLastModified = task.getResult();
return Task.forResult(localDeviceTokenLastModified != ManifestInfo.getLastModified());
}
});
}
/** package for tests */ Task<Void> updateLocalDeviceTokenLastModifiedAsync() {
return Task.call(new Callable<Void>() {
@Override
public Void call() throws Exception {
synchronized (localDeviceTokenLastModifiedMutex) {
localDeviceTokenLastModified = ManifestInfo.getLastModified();
final String localDeviceTokenLastModifiedStr =
String.valueOf(localDeviceTokenLastModified);
try {
ParseFileUtils.writeStringToFile(getLocalDeviceTokenLastModifiedFile(),
localDeviceTokenLastModifiedStr, "UTF-8");
} catch (IOException e) {
// do nothing
}
}
return null;
}
}, Task.BACKGROUND_EXECUTOR);
}
private Task<Long> getLocalDeviceTokenLastModifiedAsync() {
return Task.call(new Callable<Long>() {
@Override
public Long call() throws Exception {
synchronized (localDeviceTokenLastModifiedMutex) {
if (localDeviceTokenLastModified == 0) {
try {
String localDeviceTokenLastModifiedStr = ParseFileUtils.readFileToString(
getLocalDeviceTokenLastModifiedFile(), "UTF-8");
localDeviceTokenLastModified = Long.valueOf(localDeviceTokenLastModifiedStr);
} catch (IOException e) {
localDeviceTokenLastModified = 0;
}
}
return localDeviceTokenLastModified;
}
}
}, Task.BACKGROUND_EXECUTOR);
}
/** package for tests */ static File getLocalDeviceTokenLastModifiedFile() {
File dir = Parse.getParseCacheDir("GCMRegistrar");
return new File(dir, FILENAME_DEVICE_TOKEN_LAST_MODIFIED);
}
/** package for tests */ static void deleteLocalDeviceTokenLastModifiedFile() {
ParseFileUtils.deleteQuietly(getLocalDeviceTokenLastModifiedFile());
}
/**
* Encapsulates the a GCM registration request-response, potentially using AlarmManager to
* schedule retries if the GCM service is not available.
*/
private static class Request {
private static final String RETRY_ACTION = "com.parse.RetryGcmRegistration";
private static final int MAX_RETRIES = 5;
private static final int BACKOFF_INTERVAL_MS = 3000;
final private Context context;
final private String senderId;
final private Random random;
final private int identifier;
final private TaskCompletionSource<String> tcs;
final private PendingIntent appIntent;
final private AtomicInteger tries;
final private PendingIntent retryIntent;
final private BroadcastReceiver retryReceiver;
public static Request createAndSend(Context context, String senderId) {
Request request = new Request(context, senderId);
request.send();
return request;
}
private Request(Context context, String senderId) {
this.context = context;
this.senderId = senderId;
this.random = new Random();
this.identifier = this.random.nextInt();
this.tcs = new TaskCompletionSource<>();
this.appIntent = PendingIntent.getBroadcast(this.context, identifier, new Intent(), 0);
this.tries = new AtomicInteger(0);
String packageName = this.context.getPackageName();
Intent intent = new Intent(RETRY_ACTION).setPackage(packageName);
intent.addCategory(packageName);
intent.putExtra("random", identifier);
this.retryIntent = PendingIntent.getBroadcast(this.context, identifier, intent, 0);
this.retryReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (intent != null && intent.getIntExtra("random", 0) == identifier) {
send();
}
}
};
IntentFilter filter = new IntentFilter();
filter.addAction(RETRY_ACTION);
filter.addCategory(packageName);
context.registerReceiver(this.retryReceiver, filter);
}
public Task<String> getTask() {
return tcs.getTask();
}
private void send() {
Intent intent = new Intent(REGISTER_ACTION);
intent.setPackage("com.google.android.gsf");
intent.putExtra("sender", senderId);
intent.putExtra("app", appIntent);
ComponentName name = null;
try {
name = context.startService(intent);
} catch (SecurityException exception) {
// do nothing
}
if (name == null) {
finish(null, "GSF_PACKAGE_NOT_AVAILABLE");
}
tries.incrementAndGet();
PLog.v(TAG, "Sending GCM registration intent");
}
public void onReceiveResponseIntent(Intent intent) {
String registrationId = intent.getStringExtra(REGISTRATION_ID_EXTRA);
String error = intent.getStringExtra(ERROR_EXTRA);
if (registrationId == null && error == null) {
PLog.e(TAG, "Got no registration info in GCM onReceiveResponseIntent");
return;
}
// Retry with exponential backoff if GCM isn't available.
if ("SERVICE_NOT_AVAILABLE".equals(error) && tries.get() < MAX_RETRIES) {
AlarmManager manager = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
int alarmType = AlarmManager.ELAPSED_REALTIME_WAKEUP;
long delay = (1 << tries.get()) * BACKOFF_INTERVAL_MS + random.nextInt(BACKOFF_INTERVAL_MS);
long start = SystemClock.elapsedRealtime() + delay;
manager.set(alarmType, start, retryIntent);
} else {
finish(registrationId, error);
}
}
private void finish(String registrationId, String error) {
boolean didSetResult;
if (registrationId != null) {
didSetResult = tcs.trySetResult(registrationId);
} else {
didSetResult = tcs.trySetError(new Exception("GCM registration error: " + error));
}
if (didSetResult) {
appIntent.cancel();
retryIntent.cancel();
context.unregisterReceiver(this.retryReceiver);
}
}
}
}