diff --git a/Parse/build.gradle b/Parse/build.gradle index 9c9a12ac3..bb0c7b74e 100644 --- a/Parse/build.gradle +++ b/Parse/build.gradle @@ -1,11 +1,8 @@ -import com.android.builder.core.BuilderConstants - apply plugin: 'com.android.library' apply plugin: 'com.github.kt3k.coveralls' -apply plugin: 'com.jfrog.bintray' group = 'com.parse' -version = '1.16.8-SNAPSHOT' +version = rootProject.ext.commonLibVersion ext { projDescription = 'A library that gives you access to the powerful Parse cloud platform from your Android app.' @@ -13,15 +10,6 @@ ext { projName = 'Parse-Android' gitLink = 'https://github.com/parse-community/Parse-SDK-Android' } -buildscript { - repositories { - jcenter() - } - - dependencies { - classpath 'org.kt3k.gradle.plugin:coveralls-gradle-plugin:2.8.1' - } -} android { compileSdkVersion rootProject.ext.compileSdkVersion @@ -64,111 +52,6 @@ dependencies { testImplementation "com.squareup.okhttp3:mockwebserver:$okhttpVersion" } -android.libraryVariants.all { variant -> - def name = variant.buildType.name - - def javadoc = task("javadoc${variant.name.capitalize()}", type: Javadoc) { - description "Generates Javadoc for $variant.name." - destinationDir = rootProject.file("docs/api") - source = variant.javaCompiler.source - ext.androidJar = "${android.sdkDirectory}/platforms/${android.compileSdkVersion}/android.jar" - doFirst { - classpath = files(variant.javaCompiler.classpath.files) + files(ext.androidJar) - } - options.docletpath = [rootProject.file("./gradle/ExcludeDoclet.jar")] - options.doclet = "me.grantland.doclet.ExcludeDoclet" - - options.linksOffline("http://d.android.com/reference", "${android.sdkDirectory}/docs/reference") - options.links("http://boltsframework.github.io/docs/android/") - - exclude '**/BuildConfig.java' - exclude '**/R.java' - exclude '**/internal/**' - } - - def javadocJar = task("javadocJar${variant.name.capitalize()}", type: Jar, dependsOn: "javadoc${variant.name.capitalize()}") { - classifier = 'javadoc' - from javadoc.destinationDir - } - - if (name.equals(BuilderConstants.RELEASE)) { - artifacts.add('archives', javadocJar); - } -} - -//region Maven - -apply plugin: 'maven' -apply plugin: 'signing' - -def isSnapshot = version.endsWith('-SNAPSHOT') -def ossrhUsername = hasProperty('NEXUS_USERNAME') ? NEXUS_USERNAME : System.getenv('CI_NEXUS_USERNAME') -def ossrhPassword = hasProperty('NEXUS_PASSWORD') ? NEXUS_PASSWORD : System.getenv('CI_NEXUS_PASSWORD') - -def pomConfig = { - licenses { - license { - name 'BSD License' - url 'https://github.com/parse-community/Parse-SDK-Android/blob/master/LICENSE' - distribution 'repo' - } - } - - scm { - connection 'scm:git@github.com:parse-community/Parse-SDK-Android.git' - developerConnection 'scm:git@github.com:parse-community/Parse-SDK-Android.git' - url gitLink - } - - developers { - developer { - id 'parse' - name 'Parse' - } - } -} - - -uploadArchives { - repositories.mavenDeployer { - beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } - - repository(url: "https://oss.sonatype.org/service/local/staging/deploy/maven2/") { - authentication(userName: ossrhUsername, password: ossrhPassword) - } - - snapshotRepository(url: "https://oss.sonatype.org/content/repositories/snapshots/") { - authentication(userName: ossrhUsername, password: ossrhPassword) - } - - def basePom = { - name projName - artifactId = artifact - packaging 'aar' - description projDescription - url gitLink - } - - pom.project basePom << pomConfig - } -} - -signing { - required { !isSnapshot && gradle.taskGraph.hasTask("uploadArchives") } - sign configurations.archives -} - -task androidSourcesJar(type: Jar) { - classifier = 'sources' - from android.sourceSets.main.java.sourceFiles -} - -artifacts { - archives androidSourcesJar -} - -//endregion - //region Code Coverage apply plugin: 'jacoco' @@ -211,73 +94,4 @@ coveralls.jacocoReportPath = "${buildDir}/reports/jacoco/jacocoTestReport/jacoco //endregion -// Requires apply plugin: 'com.jfrog.bintray' - -bintray { - user = System.getenv('BINTRAY_USER') - key = System.getenv('BINTRAY_API_KEY') - - publications = ["mavenAar"] - - publish = true - pkg { - repo = 'maven' - name = 'com.parse:parse-android' - userOrg = 'parse' - licenses = ['BSD License'] - vcsUrl = 'https://github.com/parse-community/Parse-SDK-Android' - version { - name = project.version - desc = projDescription - released = new Date() - vcsTag = project.version - - // Sonatype username/passwrod must be set for this operation to happen - mavenCentralSync { - sync = true - user = ossrhUsername - password = ossrhPassword - close = '1' // release automatically - } - } - } -} - -// Create the publication with the pom configuration: -apply plugin: 'digital.wup.android-maven-publish' - -publishing { - publications { - mavenAar(MavenPublication) { - from components.android - groupId group - // We have to specify it here because otherwise Bintray's plugin will assume the artifact's name is Parse - artifactId artifact - artifacts = [androidSourcesJar, javadocJarRelease, bundleRelease] - version version - - } - - } -} - -// End of Bintray plugin - -apply plugin: "com.jfrog.artifactory" - -artifactory { - contextUrl = 'https://oss.jfrog.org' - publish { - repository { - repoKey = 'oss-snapshot-local' // The Artifactory repository key to publish to - - username = System.getenv('BINTRAY_USER') - password = System.getenv('BINTRAY_API_KEY') - maven = true - } - defaults { - publishArtifacts = true - publications('mavenAar') - } - } -} +apply from: rootProject.file('gradle/gradle-mvn-push.gradle') diff --git a/Parse/src/main/AndroidManifest.xml b/Parse/src/main/AndroidManifest.xml index 255fb581a..e9d286e22 100644 --- a/Parse/src/main/AndroidManifest.xml +++ b/Parse/src/main/AndroidManifest.xml @@ -13,11 +13,6 @@ - - - + diff --git a/Parse/src/main/java/com/parse/GcmBroadcastReceiver.java b/Parse/src/main/java/com/parse/GcmBroadcastReceiver.java deleted file mode 100644 index 7aed1bd6b..000000000 --- a/Parse/src/main/java/com/parse/GcmBroadcastReceiver.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * 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.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.support.annotation.CallSuper; - -/** - * @exclude - */ -public class GcmBroadcastReceiver extends BroadcastReceiver { - @Override - @CallSuper - public void onReceive(Context context, Intent intent) { - PushServiceUtils.runService(context, intent); - } -} diff --git a/Parse/src/main/java/com/parse/GcmPushHandler.java b/Parse/src/main/java/com/parse/GcmPushHandler.java deleted file mode 100644 index 8b7a86e10..000000000 --- a/Parse/src/main/java/com/parse/GcmPushHandler.java +++ /dev/null @@ -1,211 +0,0 @@ -/* - * 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.Service; -import android.content.Context; -import android.content.Intent; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.annotation.WorkerThread; - -import org.json.JSONException; -import org.json.JSONObject; - -import java.lang.ref.WeakReference; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -import bolts.Task; - -/** - * Proxy Service while running in GCM mode. - * - * We use an {@link ExecutorService} so that we can operate like a ghetto - * {@link android.app.IntentService} where all incoming {@link Intent}s will be handled - * sequentially. - */ -/** package */ class GcmPushHandler implements PushHandler { - private static final String TAG = "GcmPushHandler"; - - static final String REGISTER_RESPONSE_ACTION = "com.google.android.c2dm.intent.REGISTRATION"; - static final String RECEIVE_PUSH_ACTION = "com.google.android.c2dm.intent.RECEIVE"; - - GcmPushHandler() {} - - @NonNull - @Override - public SupportLevel isSupported() { - if (!ManifestInfo.isGooglePlayServicesAvailable()) { - return SupportLevel.MISSING_REQUIRED_DECLARATIONS; - } - return getManifestSupportLevel(); - } - - private SupportLevel getManifestSupportLevel() { - Context context = Parse.getApplicationContext(); - String[] requiredPermissions = new String[] { - "android.permission.INTERNET", - "android.permission.ACCESS_NETWORK_STATE", - "android.permission.WAKE_LOCK", - "com.google.android.c2dm.permission.RECEIVE", - context.getPackageName() + ".permission.C2D_MESSAGE" - }; - - if (!ManifestInfo.hasRequestedPermissions(context, requiredPermissions)) { - return SupportLevel.MISSING_REQUIRED_DECLARATIONS; - } - - String packageName = context.getPackageName(); - String rcvrPermission = "com.google.android.c2dm.permission.SEND"; - Intent[] intents = new Intent[] { - new Intent(GcmPushHandler.RECEIVE_PUSH_ACTION) - .setPackage(packageName) - .addCategory(packageName), - new Intent(GcmPushHandler.REGISTER_RESPONSE_ACTION) - .setPackage(packageName) - .addCategory(packageName), - }; - - if (!ManifestInfo.checkReceiver(GcmBroadcastReceiver.class, rcvrPermission, intents)) { - return SupportLevel.MISSING_REQUIRED_DECLARATIONS; - } - - String[] optionalPermissions = new String[] { - "android.permission.VIBRATE" - }; - - if (!ManifestInfo.hasGrantedPermissions(context, optionalPermissions)) { - return SupportLevel.MISSING_OPTIONAL_DECLARATIONS; - } - - return SupportLevel.SUPPORTED; - } - - @Nullable - @Override - public String getWarningMessage(SupportLevel level) { - switch (level) { - case SUPPORTED: return null; - case MISSING_OPTIONAL_DECLARATIONS: return "Using GCM for Parse Push, " + - "but the app manifest is missing some optional " + - "declarations that should be added for maximum reliability. Please " + - getWarningMessage(); - case MISSING_REQUIRED_DECLARATIONS: - if (ManifestInfo.isGooglePlayServicesAvailable()) { - return "Cannot use GCM for push because the app manifest is missing some " + - "required declarations. Please " + getWarningMessage(); - } else { - return "Cannot use GCM for push on this device because Google Play " + - "Services is not available. Install Google Play Services from the Play Store."; - } - } - return null; - } - - static String getWarningMessage() { - String packageName = Parse.getApplicationContext().getPackageName(); - String gcmPackagePermission = packageName + ".permission.C2D_MESSAGE"; - return "make sure that these permissions are declared as children " + - "of the root element:\n" + - "\n" + - "\n" + - "\n" + - "\n" + - "\n" + - "\n" + - "\n" + - "\n" + - "\n" + - "\n" + - "Also, please make sure that these services and broadcast receivers are declared as " + - "children of the element:\n" + - "\n" + - "\n" + - "\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - "\n" + - "\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - ""; - } - - @Override - public Task initialize() { - return GcmRegistrar.getInstance().registerAsync(); - } - - @WorkerThread - @Override - public void handlePush(Intent intent) { - if (intent != null) { - String action = intent.getAction(); - if (REGISTER_RESPONSE_ACTION.equals(action)) { - handleGcmRegistrationIntent(intent); - } else if (RECEIVE_PUSH_ACTION.equals(action)) { - handleGcmPushIntent(intent); - } else { - PLog.e(TAG, "PushService got unknown intent in GCM mode: " + intent); - } - } - } - - @WorkerThread - private void handleGcmRegistrationIntent(Intent intent) { - try { - // Have to block here since we are already in a background thread and as soon as we return, - // PushService may exit. - GcmRegistrar.getInstance().handleRegistrationIntentAsync(intent).waitForCompletion(); - } catch (InterruptedException e) { - // do nothing - } - } - - @WorkerThread - private void handleGcmPushIntent(Intent intent) { - String messageType = intent.getStringExtra("message_type"); - if (messageType != null) { - /* - * The GCM docs reserve the right to use the message_type field for new actions, but haven't - * documented what those new actions are yet. For forwards compatibility, ignore anything - * with a message_type field. - */ - PLog.i(TAG, "Ignored special message type " + messageType + " from GCM via intent " + intent); - } else { - String pushId = intent.getStringExtra("push_id"); - String timestamp = intent.getStringExtra("time"); - String dataString = intent.getStringExtra("data"); - String channel = intent.getStringExtra("channel"); - - JSONObject data = null; - if (dataString != null) { - try { - data = new JSONObject(dataString); - } catch (JSONException e) { - PLog.e(TAG, "Ignoring push because of JSON exception while processing: " + dataString, e); - return; - } - } - - PushRouter.getInstance().handlePush(pushId, timestamp, channel, data); - } - } - -} diff --git a/Parse/src/main/java/com/parse/GcmRegistrar.java b/Parse/src/main/java/com/parse/GcmRegistrar.java deleted file mode 100644 index 596a842a0..000000000 --- a/Parse/src/main/java/com/parse/GcmRegistrar.java +++ /dev/null @@ -1,405 +0,0 @@ -/* - * 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 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 checkTask = installation.getDeviceToken() == null - ? Task.forResult(true) - : isLocalDeviceTokenStaleAsync(); - return checkTask.onSuccessTask(new Continuation>() { - @Override - public Task then(Task 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 sendRegistrationRequestAsync() { - synchronized (lock) { - if (request != null) { - return Task.forResult(null); - } - // Look for an element like this as a child of the element: - // - // - // - // 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 + " 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" + - "\" />"); - return null; - } - - request = Request.createAndSend(context, senderID); - return request.getTask().continueWith(new Continuation() { - @Override - public Void then(Task 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 handleRegistrationIntentAsync(Intent intent) { - List> 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 isLocalDeviceTokenStaleAsync() { - return getLocalDeviceTokenLastModifiedAsync().onSuccessTask(new Continuation>() { - @Override - public Task then(Task task) throws Exception { - long localDeviceTokenLastModified = task.getResult(); - return Task.forResult(localDeviceTokenLastModified != ManifestInfo.getLastModified()); - } - }); - } - - /** package for tests */ Task updateLocalDeviceTokenLastModifiedAsync() { - return Task.call(new Callable() { - @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 getLocalDeviceTokenLastModifiedAsync() { - return Task.call(new Callable() { - @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 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 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); - } - } - } -} diff --git a/Parse/src/main/java/com/parse/ManifestInfo.java b/Parse/src/main/java/com/parse/ManifestInfo.java index a7f1c7329..7ddde25b9 100644 --- a/Parse/src/main/java/com/parse/ManifestInfo.java +++ b/Parse/src/main/java/com/parse/ManifestInfo.java @@ -31,31 +31,14 @@ * A utility class for retrieving app metadata such as the app name, default icon, whether or not * the app declares the correct permissions for push, etc. */ -/** package */ class ManifestInfo { +public class ManifestInfo { private static final String TAG = "com.parse.ManifestInfo"; private static final Object lock = new Object(); - private static long lastModified = -1; /* package */ static int versionCode = -1; /* package */ static String versionName = null; private static int iconId = 0; private static String displayName = null; - private static PushType pushType; - - /** - * Returns the last time this application's APK was modified on disk. This is a proxy for both - * version changes and if the APK has been restored from backup onto a different device. - */ - public static long getLastModified() { - synchronized (lock) { - if (lastModified == -1) { - File apkPath = new File(getContext().getApplicationInfo().sourceDir); - lastModified = apkPath.lastModified(); - } - } - - return lastModified; - } /** * Returns the version code for this app, as specified by the android:versionCode attribute in the @@ -152,68 +135,6 @@ public static int getIconId() { } return list; } - - // Should only be used for tests. - static void setPushType(PushType newPushType) { - synchronized (lock) { - pushType = newPushType; - } - } - - /** - * Inspects the app's manifest and returns whether the manifest contains required declarations to - * be able to use GCM for push. - */ - public static PushType getPushType() { - synchronized (lock) { - if (pushType == null) { - pushType = findPushType(); - PLog.v(TAG, "Using " + pushType + " for push."); - } - } - return pushType; - } - - - private static PushType findPushType() { - if (!ParsePushBroadcastReceiver.isSupported()) { - return PushType.NONE; - } - - if (!PushServiceUtils.isSupported()) { - return PushType.NONE; - } - - // Ordered by preference. - PushType[] types = PushType.types(); - for (PushType type : types) { - PushHandler handler = PushHandler.Factory.create(type); - PushHandler.SupportLevel level = handler.isSupported(); - String message = handler.getWarningMessage(level); - switch (level) { - case MISSING_REQUIRED_DECLARATIONS: // Can't use. notify. - if (message != null) PLog.e(TAG, message); - break; - case MISSING_OPTIONAL_DECLARATIONS: // Using anyway. - if (message != null) PLog.w(TAG, message); - return type; - case SUPPORTED: - return type; - } - } - return PushType.NONE; - } - - /* - * Returns a message that can be written to the system log if an app expects push to be enabled, - * but push isn't actually enabled because the manifest is misconfigured. - */ - static String getPushDisabledMessage() { - return "Push is not configured for this app because the app manifest is missing required " + - "declarations. To configure GCM, please add the following declarations to your app manifest: " + - GcmPushHandler.getWarningMessage(); - } - private static Context getContext() { return Parse.getApplicationContext(); @@ -241,114 +162,4 @@ public static Bundle getApplicationMetadata(Context context) { } return null; } - - private static PackageInfo getPackageInfo(String name) { - PackageInfo info = null; - - try { - info = getPackageManager().getPackageInfo(name, 0); - } catch (NameNotFoundException e) { - // do nothing - } - - return info; - } - - static ServiceInfo getServiceInfo(Class clazz) { - ServiceInfo info = null; - try { - info = getPackageManager().getServiceInfo(new ComponentName(getContext(), clazz), 0); - } catch (NameNotFoundException e) { - // do nothing - } - - return info; - } - - private static ActivityInfo getReceiverInfo(Class clazz) { - ActivityInfo info = null; - try { - info = getPackageManager().getReceiverInfo(new ComponentName(getContext(), clazz), 0); - } catch (NameNotFoundException e) { - // do nothing - } - - return info; - } - - /** - * Returns {@code true} if this package has requested all of the listed permissions. - *

- * Note: This package might have requested all the permissions, but may not - * be granted all of them. - */ - static boolean hasRequestedPermissions(Context context, String... permissions) { - String packageName = context.getPackageName(); - try { - PackageInfo pi = context.getPackageManager().getPackageInfo( - packageName, PackageManager.GET_PERMISSIONS); - if (pi.requestedPermissions == null) { - return false; - } - return Arrays.asList(pi.requestedPermissions).containsAll(Arrays.asList(permissions)); - } catch (NameNotFoundException e) { - PLog.e(TAG, "Couldn't find info about own package", e); - return false; - } - } - - /** - * Returns {@code true} if this package has been granted all of the listed permissions. - *

- * Note: This package might have requested all the permissions, but may not - * be granted all of them. - */ - static boolean hasGrantedPermissions(Context context, String... permissions) { - String packageName = context.getPackageName(); - PackageManager packageManager = context.getPackageManager(); - for (String permission : permissions) { - if (packageManager.checkPermission(permission, packageName) != PackageManager.PERMISSION_GRANTED) { - return false; - } - } - - return true; - } - - private static boolean checkResolveInfo(Class clazz, List infoList, String permission) { - for (ResolveInfo info : infoList) { - if (info.activityInfo != null) { - final Class resolveInfoClass; - try { - resolveInfoClass = Class.forName(info.activityInfo.name); - } catch (ClassNotFoundException e) { - break; - } - if (clazz.isAssignableFrom(resolveInfoClass) && (permission == null || permission.equals(info.activityInfo.permission))) { - return true; - } - } - } - - return false; - } - - static boolean checkReceiver(Class clazz, String permission, Intent[] intents) { - for (Intent intent : intents) { - List receivers = getPackageManager().queryBroadcastReceivers(intent, 0); - if (receivers.isEmpty()) { - return false; - } - - if (!checkResolveInfo(clazz, receivers, permission)) { - return false; - } - } - - return true; - } - - static boolean isGooglePlayServicesAvailable() { - return getPackageInfo("com.google.android.gsf") != null; - } } diff --git a/Parse/src/main/java/com/parse/PLog.java b/Parse/src/main/java/com/parse/PLog.java index 86502eac5..8c7843f21 100644 --- a/Parse/src/main/java/com/parse/PLog.java +++ b/Parse/src/main/java/com/parse/PLog.java @@ -10,7 +10,10 @@ import android.util.Log; -/** package */ class PLog { +/** + * Parse Logger + */ +public class PLog { public static final int LOG_LEVEL_NONE = Integer.MAX_VALUE; private static int logLevel = Integer.MAX_VALUE; @@ -57,7 +60,7 @@ private static void log(int messageLogLevel, String tag, String message, Throwab log(Log.VERBOSE, tag, message, tr); } - /* package */ static void v(String tag, String message) { + public static void v(String tag, String message) { v(tag, message, null); } @@ -73,7 +76,7 @@ private static void log(int messageLogLevel, String tag, String message, Throwab log(Log.INFO, tag, message, tr); } - /* package */ static void i(String tag, String message) { + public static void i(String tag, String message) { i(tag, message, null); } @@ -85,11 +88,11 @@ private static void log(int messageLogLevel, String tag, String message, Throwab w(tag, message, null); } - /* package */ static void e(String tag, String message, Throwable tr) { + public static void e(String tag, String message, Throwable tr) { log(Log.ERROR, tag, message, tr); } - /* package */ static void e(String tag, String message) { + public static void e(String tag, String message) { e(tag, message, null); } } diff --git a/Parse/src/main/java/com/parse/Parse.java b/Parse/src/main/java/com/parse/Parse.java index e525ba25b..9da2cdf83 100644 --- a/Parse/src/main/java/com/parse/Parse.java +++ b/Parse/src/main/java/com/parse/Parse.java @@ -423,15 +423,7 @@ public Void call() throws Exception { "com.parse.push.intent.OPEN, com.parse.push.intent.DELETE"); } - // May need to update GCM registration ID if app version has changed. - // This also primes current installation. - PushServiceUtils.initialize().continueWithTask(new Continuation>() { - @Override - public Task then(Task task) throws Exception { - // Prime current user in the background - return ParseUser.getCurrentUserAsync().makeVoid(); - } - }).continueWith(new Continuation() { + ParseUser.getCurrentUserAsync().makeVoid().continueWith(new Continuation() { @Override public Void then(Task task) throws Exception { // Prime config in the background @@ -469,7 +461,7 @@ static boolean isInitialized() { return ParsePlugins.get() != null; } - static Context getApplicationContext() { + public static Context getApplicationContext() { checkContext(); return ParsePlugins.get().applicationContext(); } @@ -511,7 +503,7 @@ static File getParseCacheDir() { return ParsePlugins.get().getCacheDir(); } - static File getParseCacheDir(String subDir) { + public static File getParseCacheDir(String subDir) { synchronized (MUTEX) { File dir = new File(getParseCacheDir(), subDir); if (!dir.exists()) { diff --git a/Parse/src/main/java/com/parse/ParseFileUtils.java b/Parse/src/main/java/com/parse/ParseFileUtils.java index 7510b1ad5..520ab1238 100644 --- a/Parse/src/main/java/com/parse/ParseFileUtils.java +++ b/Parse/src/main/java/com/parse/ParseFileUtils.java @@ -32,7 +32,7 @@ /** * General file manipulation utilities. */ -/** package */ class ParseFileUtils { +public class ParseFileUtils { /** * The number of bytes in a kilobyte. diff --git a/Parse/src/main/java/com/parse/ParseInstallation.java b/Parse/src/main/java/com/parse/ParseInstallation.java index 5ad0be037..53a379125 100644 --- a/Parse/src/main/java/com/parse/ParseInstallation.java +++ b/Parse/src/main/java/com/parse/ParseInstallation.java @@ -291,11 +291,11 @@ private void updateLocaleIdentifier() { } } - /* package */ PushType getPushType() { - return PushType.fromString(super.getString(KEY_PUSH_TYPE)); + public String getPushType() { + return super.getString(KEY_PUSH_TYPE); } - /* package */ void setPushType(PushType pushType) { + public void setPushType(String pushType) { if (pushType != null) { performPut(KEY_PUSH_TYPE, pushType.toString()); } @@ -305,11 +305,11 @@ private void updateLocaleIdentifier() { performRemove(KEY_PUSH_TYPE); } - /* package */ String getDeviceToken() { + public String getDeviceToken() { return super.getString(KEY_DEVICE_TOKEN); } - /* package */ void setDeviceToken(String deviceToken) { + public void setDeviceToken(String deviceToken) { if (deviceToken != null && deviceToken.length() > 0) { performPut(KEY_DEVICE_TOKEN, deviceToken); } diff --git a/Parse/src/main/java/com/parse/ParsePushChannelsController.java b/Parse/src/main/java/com/parse/ParsePushChannelsController.java index 5698a4832..aef771365 100644 --- a/Parse/src/main/java/com/parse/ParsePushChannelsController.java +++ b/Parse/src/main/java/com/parse/ParsePushChannelsController.java @@ -15,14 +15,12 @@ import bolts.Task; /** package */ class ParsePushChannelsController { - private static final String TAG = "com.parse.ParsePushChannelsController"; private static ParseCurrentInstallationController getCurrentInstallationController() { return ParseCorePlugins.getInstance().getCurrentInstallationController(); } public Task subscribeInBackground(final String channel) { - checkManifestAndLogErrorIfNecessary(); if (channel == null) { throw new IllegalArgumentException("Can't subscribe to null channel."); } @@ -44,7 +42,6 @@ public Task then(Task task) throws Exception { } public Task unsubscribeInBackground(final String channel) { - checkManifestAndLogErrorIfNecessary(); if (channel == null) { throw new IllegalArgumentException("Can't unsubscribe from null channel."); } @@ -63,13 +60,4 @@ public Task then(Task task) throws Exception { } }); } - - private static boolean loggedManifestError = false; - private static void checkManifestAndLogErrorIfNecessary() { - if (!loggedManifestError && ManifestInfo.getPushType() == PushType.NONE) { - loggedManifestError = true; - PLog.e(TAG, "Tried to subscribe or unsubscribe from a channel, but push is not enabled " + - "correctly. " + ManifestInfo.getPushDisabledMessage()); - } - } } diff --git a/Parse/src/main/java/com/parse/PushHandler.java b/Parse/src/main/java/com/parse/PushHandler.java deleted file mode 100644 index 576e003b5..000000000 --- a/Parse/src/main/java/com/parse/PushHandler.java +++ /dev/null @@ -1,125 +0,0 @@ -/* - * 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.content.Intent; -import android.os.IBinder; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.annotation.WorkerThread; - -import org.json.JSONObject; - -import java.util.List; - -import bolts.Task; - -/** - * An interface for for handling push payloads (or any similar events, e.g. registration) - * that woke up the {@link com.parse.PushService}. - * Each subclass represent a certain {@link com.parse.PushType}. - * - * These classes are short-lived, instantiated at the moment of handling a push payload - * or initializing. They should not be 'stateful' in this sense. - */ -/** package */ interface PushHandler { - - // TODO: let someone extend this somehow if we want to publicize handlers - class Factory { - static PushHandler create(PushType type) { - switch (type) { - case GCM: return new GcmPushHandler(); - case NONE: return new FallbackHandler(); - } - return null; - } - } - - enum SupportLevel { - /* - * Manifest has all required and optional declarations necessary to support this push service. - */ - SUPPORTED, - - /* - * Manifest has all required declarations to support this push service, but is missing some - * optional declarations. - */ - MISSING_OPTIONAL_DECLARATIONS, - - /* - * Manifest doesn't have enough required declarations to support this push service. - */ - MISSING_REQUIRED_DECLARATIONS - } - - - /** - * Whether this push handler is supported by the current device and manifest configuration. - * Implementors can parse the manifest file using utilities in {@link ManifestInfo}. - * @return true if supported - */ - @NonNull - SupportLevel isSupported(); - - /** - * Returns a warning message to be shown in logs, depending on the support level returned - * by {@link #isSupported()}. Called on the same instance. - */ - @Nullable - String getWarningMessage(SupportLevel level); - - /** - * If this handler is the default handler for this device, - * initialize is called to let it set up things, launch registrations intents, - * or whatever else is needed. - * - * This method is also responsible to update the current {@link ParseInstallation} - * fields to let parse server work (e.g. device token, push type). - * - * @return a task completing when init completed - */ - Task initialize(); - - /** - * Handle a raw intent. - * This is called in a background thread so can be synchronous. - * Handlers can do checks over the intent and then dispatch the push notification to - * {@link ParsePushBroadcastReceiver}, by calling - * {@link PushRouter#handlePush(String, String, String, JSONObject)}. - */ - @WorkerThread - void handlePush(Intent intent); - - - class FallbackHandler implements PushHandler { - - private FallbackHandler() {}; - - @Nullable - @Override - public String getWarningMessage(SupportLevel level) { - return null; - } - - @NonNull - @Override - public SupportLevel isSupported() { - return SupportLevel.SUPPORTED; - } - - @Override - public Task initialize() { - return Task.forResult(null); - } - - @Override - public void handlePush(Intent intent) {} - } -} diff --git a/Parse/src/main/java/com/parse/PushRouter.java b/Parse/src/main/java/com/parse/PushRouter.java index 3630e8918..2d51fdb87 100644 --- a/Parse/src/main/java/com/parse/PushRouter.java +++ b/Parse/src/main/java/com/parse/PushRouter.java @@ -29,7 +29,7 @@ * registration id for a client (which can result in duplicate pushes while both the old and * new registration id are still valid). */ -/** package */ class PushRouter { +public class PushRouter { private static final String TAG = "com.parse.ParsePushRouter"; private static final String LEGACY_STATE_LOCATION = "pushState"; private static final String STATE_LOCATION = "push"; diff --git a/Parse/src/main/java/com/parse/PushService.java b/Parse/src/main/java/com/parse/PushService.java deleted file mode 100644 index f7de87a66..000000000 --- a/Parse/src/main/java/com/parse/PushService.java +++ /dev/null @@ -1,277 +0,0 @@ -/* - * 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.Service; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.os.IBinder; -import android.os.PowerManager; -import android.util.SparseArray; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -/** - * A service to listen for push notifications. This operates in the same process as the parent - * application. - *

- * The {@code PushService} can listen to pushes from Google Cloud Messaging (GCM). - * To configure the {@code PushService} for GCM, ensure these permission declarations are present in - * your AndroidManifest.xml as children of the <manifest> element: - *

- *

- * <uses-permission android:name="android.permission.INTERNET" />
- * <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
- * <uses-permission android:name="android.permission.VIBRATE" />
- * <uses-permission android:name="android.permission.WAKE_LOCK" />
- * <uses-permission android:name="android.permission.GET_ACCOUNTS" />
- * <uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
- * <permission android:name="YOUR_PACKAGE_NAME.permission.C2D_MESSAGE"
- *   android:protectionLevel="signature" />
- * <uses-permission android:name="YOUR_PACKAGE_NAME.permission.C2D_MESSAGE" />
- * 
- *

- * Replace YOUR_PACKAGE_NAME in the declarations above with your application's package name. Also, - * make sure that {@link GcmBroadcastReceiver}, {@link PushService} and - * {@link ParsePushBroadcastReceiver} are declared as children of the - * <application> element: - *

- *

- * <service android:name="com.parse.PushService" />
- * <receiver android:name="com.parse.GcmBroadcastReceiver"
- *  android:permission="com.google.android.c2dm.permission.SEND">
- *   <intent-filter>
- *     <action android:name="com.google.android.c2dm.intent.RECEIVE" />
- *     <action android:name="com.google.android.c2dm.intent.REGISTRATION" />
- *     <category android:name="YOUR_PACKAGE_NAME" />
- *   </intent-filter>
- * </receiver>
- * <receiver android:name="com.parse.ParsePushBroadcastReceiver" android:exported=false>
- *  <intent-filter>
- *     <action android:name="com.parse.push.intent.RECEIVE" />
- *     <action android:name="com.parse.push.intent.OPEN" />
- *     <action android:name="com.parse.push.intent.DELETE" />
- *   </intent-filter>
- * </receiver>
- * 
- *

- * Again, replace YOUR_PACKAGE_NAME with your application's package name. - * If you want to customize the way your app generates Notifications for your pushes, you - * can register a custom subclass of {@link ParsePushBroadcastReceiver}. - *

- * Once push notifications are configured in the manifest, you can subscribe to a push channel by - * calling: - *

- *

- * ParsePush.subscribeInBackground("the_channel_name");
- * 
- *

- * When the client receives a push message, a notification will appear in the system tray. When the - * user taps the notification, it will broadcast the "com.parse.push.intent.OPEN" intent. - * The {@link ParsePushBroadcastReceiver} listens to this intent to track an app open event and - * launch the app's launcher activity. To customize this behavior override - * {@link ParsePushBroadcastReceiver#onPushOpen(Context, Intent)}. - * - * Starting with Android O, this is replaced by {@link PushServiceApi26}. - */ -public final class PushService extends Service { - private static final String TAG = "com.parse.PushService"; - - //region run and dispose - - private static final String WAKE_LOCK_EXTRA = "parseWakeLockId"; - private static final SparseArray wakeLocks = new SparseArray<>(); - private static int wakeLockId = 0; - - /* - * Same as Context.startService, but acquires a wake lock before starting the service. The wake - * lock must later be released by calling dispose(). - */ - static boolean run(Context context, Intent intent) { - String reason = intent.toString(); - ParseWakeLock wl = ParseWakeLock.acquireNewWakeLock(context, PowerManager.PARTIAL_WAKE_LOCK, reason, 0); - - synchronized (wakeLocks) { - intent.putExtra(WAKE_LOCK_EXTRA, wakeLockId); - wakeLocks.append(wakeLockId, wl); - wakeLockId++; - } - - intent.setClass(context, PushService.class); - ComponentName name = context.startService(intent); - if (name == null) { - PLog.e(TAG, "Could not start the service. Make sure that the XML tag " - + " is in your " - + "AndroidManifest.xml as a child of the element."); - dispose(intent); - return false; - } - return true; - } - - static void dispose(Intent intent) { - if (intent != null && intent.hasExtra(WAKE_LOCK_EXTRA)) { - int id = intent.getIntExtra(WAKE_LOCK_EXTRA, -1); - ParseWakeLock wakeLock; - - synchronized (wakeLocks) { - wakeLock = wakeLocks.get(id); - wakeLocks.remove(id); - } - - if (wakeLock == null) { - PLog.e(TAG, "Got wake lock id of " + id + " in intent, but no such lock found in " + - "global map. Was disposePushService called twice for the same intent?"); - } else { - wakeLock.release(); - } - } - } - - //region ServiceLifecycleCallbacks used for testing - - private static List serviceLifecycleCallbacks = null; - - /* package */ interface ServiceLifecycleCallbacks { - void onServiceCreated(Service service); - void onServiceDestroyed(Service service); - } - - /* package */ static void registerServiceLifecycleCallbacks(ServiceLifecycleCallbacks callbacks) { - synchronized (PushService.class) { - if (serviceLifecycleCallbacks == null) { - serviceLifecycleCallbacks = new ArrayList<>(); - } - serviceLifecycleCallbacks.add(callbacks); - } - } - - /* package */ static void unregisterServiceLifecycleCallbacks(ServiceLifecycleCallbacks callbacks) { - synchronized (PushService.class) { - serviceLifecycleCallbacks.remove(callbacks); - } - } - - private static void dispatchOnServiceCreated(Service service) { - if (serviceLifecycleCallbacks != null) { - for (ServiceLifecycleCallbacks callback : serviceLifecycleCallbacks) { - callback.onServiceCreated(service); - } - } - } - - private static void dispatchOnServiceDestroyed(Service service) { - if (serviceLifecycleCallbacks != null) { - for (ServiceLifecycleCallbacks callback : serviceLifecycleCallbacks) { - callback.onServiceDestroyed(service); - } - } - } - - //endregion - - // We delegate the intent to a PushHandler running in a streamlined executor. - private ExecutorService executor; - private PushHandler handler; - - /** - * Client code should not construct a PushService directly. - */ - public PushService() { - super(); - } - - // For tests - void setPushHandler(PushHandler handler) { - this.handler = handler; - } - - /** - * Called at startup at the moment of parsing the manifest, to see - * if it was correctly set-up. - */ - static boolean isSupported() { - return ManifestInfo.getServiceInfo(PushService.class) != null; - } - - - /** - * Client code should not call {@code onCreate} directly. - */ - @Override - public void onCreate() { - super.onCreate(); - if (ParsePlugins.get() == null) { - PLog.e(TAG, "The Parse push service cannot start because Parse.initialize " - + "has not yet been called. If you call Parse.initialize from " - + "an Activity's onCreate, that call should instead be in the " - + "Application.onCreate. Be sure your Application class is registered " - + "in your AndroidManifest.xml with the android:name property of your " - + " tag."); - stopSelf(); - return; - } - - executor = Executors.newSingleThreadExecutor(); - handler = PushServiceUtils.createPushHandler(); - dispatchOnServiceCreated(this); - } - - /** - * Client code should not call {@code onStartCommand} directly. - */ - @Override - public int onStartCommand(final Intent intent, int flags, final int startId) { - if (ManifestInfo.getPushType() == PushType.NONE) { - PLog.e(TAG, "Started push service even though no push service is enabled: " + intent); - } - - executor.execute(new Runnable() { - @Override - public void run() { - try { - handler.handlePush(intent); - } finally { - dispose(intent); - stopSelf(startId); - } - } - }); - - return START_NOT_STICKY; - } - - /** - * Client code should not call {@code onBind} directly. - */ - @Override - public IBinder onBind(Intent intent) { - throw new IllegalArgumentException("You cannot bind directly to the PushService. " - + "Use PushService.subscribe instead."); - } - - /** - * Client code should not call {@code onDestroy} directly. - */ - @Override - public void onDestroy() { - if (executor != null) { - executor.shutdown(); - executor = null; - handler = null; - } - - dispatchOnServiceDestroyed(this); - super.onDestroy(); - } -} diff --git a/Parse/src/main/java/com/parse/PushServiceApi26.java b/Parse/src/main/java/com/parse/PushServiceApi26.java deleted file mode 100644 index cf4633e65..000000000 --- a/Parse/src/main/java/com/parse/PushServiceApi26.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * 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.annotation.TargetApi; -import android.app.job.JobInfo; -import android.app.job.JobParameters; -import android.app.job.JobScheduler; -import android.app.job.JobService; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.os.Build; -import android.os.Bundle; - -import java.util.concurrent.Executor; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -/** - * A JobService that is triggered by push notifications on Oreo+. - * Read {@link PushServiceUtils} and {@link PushService} for info and docs. - * This is already set-up in our own manifest. - */ -@TargetApi(Build.VERSION_CODES.O) -public final class PushServiceApi26 extends JobService { - private static final String TAG = PushServiceApi26.class.getSimpleName(); - private static final String INTENT_KEY = "intent"; - private static final int JOB_SERVICE_ID = 999; - - static boolean run(Context context, Intent intent) { - JobScheduler scheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE); - // Execute in the next second. - Bundle extra = new Bundle(1); - extra.putParcelable(INTENT_KEY, intent); - ComponentName component = new ComponentName(context, PushServiceApi26.class); - int did = scheduler.schedule(new JobInfo.Builder(JOB_SERVICE_ID, component) - .setMinimumLatency(1L) - .setOverrideDeadline(1000L) - .setRequiresCharging(false) - .setRequiresBatteryNotLow(false) - .setRequiresStorageNotLow(false) - .setTransientExtras(extra) - .build()); - return did == JobScheduler.RESULT_SUCCESS; - } - - // We delegate the intent to a PushHandler running in a streamlined executor. - private ExecutorService executor; - private PushHandler handler; - private int jobsCount; - - // Our manifest file is OK. - static boolean isSupported() { - return true; - } - - @Override - public boolean onStartJob(final JobParameters jobParameters) { - if (ParsePlugins.get() == null) { - PLog.e(TAG, "The Parse push service cannot start because Parse.initialize " - + "has not yet been called. If you call Parse.initialize from " - + "an Activity's onCreate, that call should instead be in the " - + "Application.onCreate. Be sure your Application class is registered " - + "in your AndroidManifest.xml with the android:name property of your " - + " tag."); - return false; - } - - final Bundle params = jobParameters.getTransientExtras(); - final Intent intent = params.getParcelable(INTENT_KEY); - jobsCount++; - getExecutor().execute(new Runnable() { - @Override - public void run() { - try { - getHandler().handlePush(intent); - } finally { - jobFinished(jobParameters, false); - jobsCount--; - if (jobsCount == 0) { - tearDown(); - } - } - } - }); - return true; - } - - @Override - public boolean onStopJob(JobParameters jobParameters) { - // Something went wrong before jobFinished(). Try rescheduling. - return true; - } - - private Executor getExecutor() { - if (executor == null) executor = Executors.newSingleThreadExecutor(); - return executor; - } - - private PushHandler getHandler() { - if (handler == null) handler = PushServiceUtils.createPushHandler(); - return handler; - } - - private void tearDown() { - if (executor != null) executor.shutdown(); - executor = null; - handler = null; - } -} diff --git a/Parse/src/main/java/com/parse/PushServiceUtils.java b/Parse/src/main/java/com/parse/PushServiceUtils.java deleted file mode 100644 index 5b8b5d15c..000000000 --- a/Parse/src/main/java/com/parse/PushServiceUtils.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * 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.content.Context; -import android.content.Intent; -import android.os.Build; -import android.support.annotation.NonNull; - -import bolts.Task; - - -/** - * Helper class mostly used to access and wake the push dispatching class, plus some other utilities. - * - * Android O introduces limitations over Context.startService. If the app is currently considered - * in background, the call will result in a crash. The only reliable solutions are either using - * Context.startServiceInForeground, which does not fit our case, or move to the JobScheduler - * engine, which is what we do here for Oreo, launching {@link PushServiceApi26}. - * - * Pre-oreo, we just launch {@link PushService}. - * - * See: - * https://developer.android.com/about/versions/oreo/background.html - * https://developer.android.com/reference/android/support/v4/content/WakefulBroadcastReceiver.html - */ -abstract class PushServiceUtils { - private static final boolean USE_JOBS = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O; - - /** - * Wakes the PushService class by running it either as a Service or as a scheduled job - * depending on API level. - * - * @param context calling context - * @param intent non-null intent to be passed to the PushHandlers - * @return true if service could be launched - */ - public static boolean runService(Context context, @NonNull Intent intent) { - if (USE_JOBS) { - return PushServiceApi26.run(context, intent); - } else { - return PushService.run(context, intent); - } - } - - // Checks the manifest file. - static boolean isSupported() { - if (USE_JOBS) { - return PushServiceApi26.isSupported(); - } else { - return PushService.isSupported(); - } - } - - // Some handlers might need initialization. - static Task initialize() { - return createPushHandler().initialize(); - } - - static PushHandler createPushHandler() { - return PushHandler.Factory.create(ManifestInfo.getPushType()); - } -} diff --git a/Parse/src/main/java/com/parse/PushType.java b/Parse/src/main/java/com/parse/PushType.java deleted file mode 100644 index 7edcacd51..000000000 --- a/Parse/src/main/java/com/parse/PushType.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * 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 java.util.List; - -/** package */ enum PushType { - NONE("none"), - GCM("gcm"); - - private final String pushType; - - PushType(String pushType) { - this.pushType = pushType; - } - - static PushType fromString(String pushType) { - if ("none".equals(pushType)) { - return PushType.NONE; - } else if ("gcm".equals(pushType)) { - return PushType.GCM; - } else { - return null; - } - } - - @Override - public String toString() { - return pushType; - } - - // Preference ordered list. - // TODO: let someone inject here if we want public handlers - static PushType[] types() { - return new PushType[]{ GCM, NONE }; - } -} diff --git a/Parse/src/test/java/com/parse/ParseInstallationTest.java b/Parse/src/test/java/com/parse/ParseInstallationTest.java index a5617f023..a8a8a4233 100644 --- a/Parse/src/test/java/com/parse/ParseInstallationTest.java +++ b/Parse/src/test/java/com/parse/ParseInstallationTest.java @@ -285,32 +285,6 @@ public void testUpdateBeforeSave() throws Exception { // TODO(mengyan): Add other testUpdateBeforeSave cases to cover all branches - @Test - public void testPushType() throws Exception { - ParseInstallation installation = new ParseInstallation(); - installation.setPushType(PushType.GCM); - - assertEquals(PushType.GCM, installation.getPushType()); - - installation.removePushType(); - - assertNull(installation.getPushType()); - // Make sure we add the pushType to operationSetQueue instead of serverData - assertEquals(1, installation.operationSetQueue.getLast().size()); - } - - @Test - public void testPushTypeWithNullPushType() throws Exception { - ParseInstallation installation = new ParseInstallation(); - installation.setPushType(PushType.GCM); - - assertEquals(PushType.GCM, installation.getPushType()); - - installation.setPushType(null); - - assertEquals(PushType.GCM, installation.getPushType()); - } - @Test public void testDeviceToken() throws Exception { ParseInstallation installation = new ParseInstallation(); diff --git a/Parse/src/test/java/com/parse/PushHandlerTest.java b/Parse/src/test/java/com/parse/PushHandlerTest.java deleted file mode 100644 index eccdf6f69..000000000 --- a/Parse/src/test/java/com/parse/PushHandlerTest.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.parse; - - -import org.junit.Test; - -import static org.junit.Assert.*; - -public class PushHandlerTest { - - @Test - public void testFactory() { - PushHandler handler = PushHandler.Factory.create(PushType.NONE); - assertTrue(handler instanceof PushHandler.FallbackHandler); - - handler = PushHandler.Factory.create(PushType.GCM); - assertTrue(handler instanceof GcmPushHandler); - } - - @Test - public void testFallbackHandler() { - PushHandler handler = PushHandler.Factory.create(PushType.NONE); - assertNull(handler.getWarningMessage(PushHandler.SupportLevel.SUPPORTED)); - assertNull(handler.getWarningMessage(PushHandler.SupportLevel.MISSING_OPTIONAL_DECLARATIONS)); - assertNull(handler.getWarningMessage(PushHandler.SupportLevel.MISSING_REQUIRED_DECLARATIONS)); - assertTrue(handler.initialize().isCompleted()); - assertEquals(handler.isSupported(), PushHandler.SupportLevel.SUPPORTED); - } -} diff --git a/Parse/src/test/java/com/parse/PushServiceTest.java b/Parse/src/test/java/com/parse/PushServiceTest.java deleted file mode 100644 index 2f2615d71..000000000 --- a/Parse/src/test/java/com/parse/PushServiceTest.java +++ /dev/null @@ -1,105 +0,0 @@ -package com.parse; - - -import android.content.Intent; - -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; -import org.robolectric.Robolectric; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.android.controller.ServiceController; -import org.robolectric.annotation.Config; - -import bolts.Task; -import bolts.TaskCompletionSource; - -import static org.mockito.Matchers.any; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - - -@RunWith(RobolectricTestRunner.class) -@Config(constants = BuildConfig.class, sdk = TestHelper.ROBOLECTRIC_SDK_VERSION) -public class PushServiceTest extends ResetPluginsParseTest { - - private PushService service; - private ServiceController controller; - private PushHandler handler; - private PushService.ServiceLifecycleCallbacks callbacks; - - @Before - public void setUp() throws Exception { - super.setUp(); - callbacks = mock(PushService.ServiceLifecycleCallbacks.class); - PushService.registerServiceLifecycleCallbacks(callbacks); - - controller = Robolectric.buildService(PushService.class); - service = controller.get(); - handler = mock(PushHandler.class); - service.setPushHandler(handler); - - Parse.Configuration.Builder builder = new Parse.Configuration.Builder(service); - ParsePlugins.initialize(service, builder.build()); - } - - @After - public void tearDown() throws Exception { - super.tearDown(); - PushService.unregisterServiceLifecycleCallbacks(callbacks); - } - - @Test - public void testOnCreateWithoutInit() { - ParsePlugins.reset(); - controller.create(); - verify(callbacks, never()).onServiceCreated(service); - } - - @Test - public void testOnCreate() { - controller.create(); - verify(callbacks, times(1)).onServiceCreated(service); - } - - @Test(expected = IllegalArgumentException.class) - public void testCannotBind() { - controller.create().bind(); - } - - @Test - public void testStartCommand() throws Exception { - controller.create(); - service.setPushHandler(handler); // reset handler to our mock - - final TaskCompletionSource tcs = new TaskCompletionSource<>(); - final Task handleTask = tcs.getTask(); - doAnswer(new Answer() { - @Override - public Object answer(InvocationOnMock invocation) throws Throwable { - tcs.setResult(null); - return null; - } - }).when(handler).handlePush(any(Intent.class)); - - controller.startCommand(0, 0); - handleTask.waitForCompletion(); - - verify(callbacks, times(1)).onServiceCreated(service); - verify(handler, times(1)).handlePush(any(Intent.class)); - } - - @Test - public void testDestroy() { - controller.create(); - controller.startCommand(0, 0); - controller.destroy(); - verify(callbacks, times(1)).onServiceDestroyed(service); - } -} diff --git a/Parse/src/test/java/com/parse/PushServiceUtilsTest.java b/Parse/src/test/java/com/parse/PushServiceUtilsTest.java deleted file mode 100644 index 0d8b22efc..000000000 --- a/Parse/src/test/java/com/parse/PushServiceUtilsTest.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.parse; - - -import android.content.Intent; - -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; -import org.robolectric.Robolectric; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.android.controller.ServiceController; -import org.robolectric.annotation.Config; - -import bolts.Task; -import bolts.TaskCompletionSource; - -import static org.junit.Assert.assertTrue; -import static org.mockito.Matchers.any; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - - -public class PushServiceUtilsTest { - - @Test - public void testDefaultHandler() { - ManifestInfo.setPushType(PushType.NONE); - PushHandler handler = PushServiceUtils.createPushHandler(); - assertTrue(handler instanceof PushHandler.FallbackHandler); - - ManifestInfo.setPushType(PushType.GCM); - handler = PushServiceUtils.createPushHandler(); - assertTrue(handler instanceof GcmPushHandler); - } - -} diff --git a/build.gradle b/build.gradle index c0333ade0..1b255b0d6 100644 --- a/build.gradle +++ b/build.gradle @@ -5,6 +5,7 @@ buildscript { } dependencies { classpath 'com.android.tools.build:gradle:3.0.0' + classpath 'org.kt3k.gradle.plugin:coveralls-gradle-plugin:2.8.2' } } @@ -24,9 +25,12 @@ allprojects { ext { compileSdkVersion = 27 - buildToolsVersion = "27.0.0" - supportLibVersion = '27.0.1' + commonLibVersion = "1.17.0-SNAPSHOT" + + supportLibVersion = '27.1.0' + googleLibVersion = '12.0.1' + firebaseJobdispatcherVersion = '0.8.5' minSdkVersion = 14 targetSdkVersion = 27 diff --git a/fcm/.gitignore b/fcm/.gitignore new file mode 100644 index 000000000..796b96d1c --- /dev/null +++ b/fcm/.gitignore @@ -0,0 +1 @@ +/build diff --git a/fcm/README.md b/fcm/README.md new file mode 100644 index 000000000..b1f5983e9 --- /dev/null +++ b/fcm/README.md @@ -0,0 +1,67 @@ +# Parse SDK Android FCM +FCM support for Parse Android apps + +## Setup + +### Installation + +Add dependency to the application level `build.gradle` file. + +```groovy +dependencies { + implementation 'com.parse:parse-android:latest.version.here' + implementation 'com.parse:parse-android-fcm:latest.version.here' +} +``` +Then, follow Google's docs for [setting up an Firebase app](https://firebase.google.com/docs/android/setup). Although the steps are different for setting up FCM with Parse, it is also a good idea to read over the [Firebase FCM Setup](https://firebase.google.com/docs/cloud-messaging/android/client). + +You will then need to register some things in your manifest, specifically: +```xml + + + + + +``` +where `MyFirebaseInstanceIdService` is your own custom class which extends `ParseFirebaseInstanceIdService`. + +Additional, you will register: + +```xml + + + + + +``` +where `MyFirebaseMessagingService` extends `ParseFirebaseMessagingService` + +After these services are registered in the Manifest, you then need to register your push broadcast receiver: +```xml + + + + + + + +``` + +## Custom Notifications +If you need to customize the notification that is sent out from a push, you can do so easily by extending `ParsePushBroadcastReceiver` with your own class and registering it instead in the Manifest. + +## Instance ID Service +If you need to store the FCM token elsewhere outside of Parse, you can create your own implementation of the `FirebaseInstanceIdService`, just make sure you are either extending `ParseFirebaseInstanceIdService` or are calling `ParseFCM.scheduleTokenUpload(getApplicationContext());` in the `onTokenRefresh` method. + +## License + 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. \ No newline at end of file diff --git a/fcm/build.gradle b/fcm/build.gradle new file mode 100644 index 000000000..99b3e917e --- /dev/null +++ b/fcm/build.gradle @@ -0,0 +1,49 @@ +apply plugin: 'com.android.library' + +version = rootProject.ext.commonLibVersion +group = 'com.parse' + +ext { + projDescription = 'Parse Android FCM support.' + artifact = 'parse-fcm-android' + projName = 'Parse-Android' + gitLink = 'https://github.com/parse-community/Parse-SDK-Android' +} + +android { + compileSdkVersion rootProject.ext.compileSdkVersion + + defaultConfig { + minSdkVersion rootProject.ext.minSdkVersion + targetSdkVersion rootProject.ext.targetSdkVersion + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + + } + + packagingOptions { + exclude '**/BuildConfig.class' + } + + lintOptions { + abortOnError false + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + +} + +dependencies { + api "com.google.firebase:firebase-messaging:$googleLibVersion" + api "com.firebase:firebase-jobdispatcher:$firebaseJobdispatcherVersion" + implementation project(':Parse') +} + +apply from: rootProject.file('gradle/gradle-mvn-push.gradle') diff --git a/fcm/proguard-rules.pro b/fcm/proguard-rules.pro new file mode 100644 index 000000000..f1b424510 --- /dev/null +++ b/fcm/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/fcm/src/main/AndroidManifest.xml b/fcm/src/main/AndroidManifest.xml new file mode 100644 index 000000000..d7d51629c --- /dev/null +++ b/fcm/src/main/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + diff --git a/fcm/src/main/java/com/parse/fcm/ParseFCM.java b/fcm/src/main/java/com/parse/fcm/ParseFCM.java new file mode 100644 index 000000000..c6e296737 --- /dev/null +++ b/fcm/src/main/java/com/parse/fcm/ParseFCM.java @@ -0,0 +1,46 @@ +/* + * 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.fcm; + +import android.content.Context; + +import com.firebase.jobdispatcher.Constraint; +import com.firebase.jobdispatcher.FirebaseJobDispatcher; +import com.firebase.jobdispatcher.GooglePlayDriver; +import com.firebase.jobdispatcher.Job; +import com.firebase.jobdispatcher.RetryStrategy; + +public class ParseFCM { + + static final String TAG = "ParseFCM"; + + private static final String JOB_TAG_UPLOAD_TOKEN = "upload-token"; + + /** + * You can call this manually if you are overriding the {@link com.google.firebase.iid.FirebaseInstanceIdService} + * @param context context + */ + public static void scheduleTokenUpload(Context context) { + FirebaseJobDispatcher dispatcher = new FirebaseJobDispatcher(new GooglePlayDriver(context.getApplicationContext())); + Job job = dispatcher.newJobBuilder() + .setRecurring(false) + .setReplaceCurrent(true) + // retry with exponential backoff + .setRetryStrategy(RetryStrategy.DEFAULT_EXPONENTIAL) + .setConstraints( + // only run on a network + Constraint.ON_ANY_NETWORK + ) + .setService(ParseFirebaseJobService.class) // the JobService that will be called + .setTag(JOB_TAG_UPLOAD_TOKEN) // uniquely identifies the job + .build(); + + dispatcher.mustSchedule(job); + } +} diff --git a/fcm/src/main/java/com/parse/fcm/ParseFirebaseInstanceIdService.java b/fcm/src/main/java/com/parse/fcm/ParseFirebaseInstanceIdService.java new file mode 100644 index 000000000..d068b5ce1 --- /dev/null +++ b/fcm/src/main/java/com/parse/fcm/ParseFirebaseInstanceIdService.java @@ -0,0 +1,28 @@ +/* + * 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.fcm; + +import android.support.annotation.CallSuper; + +import com.google.firebase.iid.FirebaseInstanceIdService; +import com.parse.ParseInstallation; + +/** + * Assures the {@link ParseInstallation#getDeviceToken()} stays up to date. If you need to do custom things with the token, make sure you extend this + * class and call super. + */ +public class ParseFirebaseInstanceIdService extends FirebaseInstanceIdService { + + @CallSuper + @Override + public void onTokenRefresh() { + super.onTokenRefresh(); + ParseFCM.scheduleTokenUpload(getApplicationContext()); + } +} diff --git a/fcm/src/main/java/com/parse/fcm/ParseFirebaseJobService.java b/fcm/src/main/java/com/parse/fcm/ParseFirebaseJobService.java new file mode 100644 index 000000000..02b15db38 --- /dev/null +++ b/fcm/src/main/java/com/parse/fcm/ParseFirebaseJobService.java @@ -0,0 +1,54 @@ +/* + * 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.fcm; + +import com.firebase.jobdispatcher.JobParameters; +import com.firebase.jobdispatcher.JobService; +import com.google.firebase.iid.FirebaseInstanceId; +import com.parse.PLog; +import com.parse.ParseException; +import com.parse.ParseInstallation; +import com.parse.SaveCallback; + +/** + * Handles saving the FCM token to the {@link ParseInstallation} in the background + */ +public class ParseFirebaseJobService extends JobService { + + @Override + public boolean onStartJob(final JobParameters job) { + PLog.v(ParseFCM.TAG, "Updating FCM token"); + ParseInstallation installation = ParseInstallation.getCurrentInstallation(); + String token = FirebaseInstanceId.getInstance().getToken(); + if (installation != null && token != null) { + installation.setDeviceToken(token); + //even though this is FCM, calling it gcm will work on the backend + installation.setPushType("gcm"); + installation.saveInBackground(new SaveCallback() { + @Override + public void done(ParseException e) { + if (e == null) { + PLog.v(ParseFCM.TAG, "FCM token saved to installation"); + jobFinished(job, false); + } else { + PLog.e(ParseFCM.TAG, "FCM token upload failed", e); + jobFinished(job, true); + } + } + }); + return true; + } + return false; // Answers the question: "Is there still work going on?" + } + + @Override + public boolean onStopJob(JobParameters job) { + return true; // Answers the question: "Should this job be retried?" + } +} diff --git a/fcm/src/main/java/com/parse/fcm/ParseFirebaseMessagingService.java b/fcm/src/main/java/com/parse/fcm/ParseFirebaseMessagingService.java new file mode 100644 index 000000000..cbf94924b --- /dev/null +++ b/fcm/src/main/java/com/parse/fcm/ParseFirebaseMessagingService.java @@ -0,0 +1,35 @@ +package com.parse.fcm; + +import com.google.firebase.messaging.FirebaseMessagingService; +import com.google.firebase.messaging.RemoteMessage; +import com.parse.PLog; +import com.parse.PushRouter; + +import org.json.JSONException; +import org.json.JSONObject; + +public class ParseFirebaseMessagingService extends FirebaseMessagingService { + + @Override + public void onMessageReceived(RemoteMessage remoteMessage) { + super.onMessageReceived(remoteMessage); + PLog.v(ParseFCM.TAG, "onMessageReceived"); + + String pushId = remoteMessage.getData().get("push_id"); + String timestamp = remoteMessage.getData().get("time"); + String dataString = remoteMessage.getData().get("data"); + String channel = remoteMessage.getData().get("channel"); + + JSONObject data = null; + if (dataString != null) { + try { + data = new JSONObject(dataString); + } catch (JSONException e) { + PLog.e(ParseFCM.TAG, "Ignoring push because of JSON exception while processing: " + dataString, e); + return; + } + } + + PushRouter.getInstance().handlePush(pushId, timestamp, channel, data); + } +} diff --git a/gcm/.gitignore b/gcm/.gitignore new file mode 100644 index 000000000..796b96d1c --- /dev/null +++ b/gcm/.gitignore @@ -0,0 +1 @@ +/build diff --git a/gcm/README.md b/gcm/README.md new file mode 100644 index 000000000..6aceccf5b --- /dev/null +++ b/gcm/README.md @@ -0,0 +1,96 @@ +# Parse SDK Android GCM +GCM support for Parse Android apps + +## Deprecated +Please note that GCM is deprecated in favor of FCM. This module exists as a backwards compatible solution for projects already using GCM. New apps should instead use FCM. + +## Setup + +### Installation + +Add dependency to the application level `build.gradle` file. + +```groovy +dependencies { + implementation 'com.parse:parse-android:latest.version.here' + implementation 'com.parse:parse-android-gcm:latest.version.here' +} +``` +You will then need to register some things in your manifest, firstly, the GCM sender ID: +```xml + +``` +The sender ID should be all numbers. Make sure you are keeping the `id:` in the front + +Next: +```xml + + + + + + +``` +And to listen for the pushes from GCM: +```xml + + + + + +``` +And finally, to register the device for GCM pushes: +```xml + + + + + +``` + +After these services are registered in the Manifest, you then need to register your push broadcast receiver: +```xml + + + + + + + +``` +After all this, you will need to register GCM in your `Application.onCreate()` like so: +```java +@Override +public void onCreate() { + super.onCreate(); + Parse.Configuration configuration = new Parse.Configuration.Builder(this) + //... + .build(); + Parse.initialize(configuration); + ParseGCM.register(this); +} +``` + +After this, you are all set. + +## Custom Notifications +If you need to customize the notification that is sent out from a push, you can do so easily by extending `ParsePushBroadcastReceiver` with your own class and registering it instead in the Manifest. + +## License + 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. \ No newline at end of file diff --git a/gcm/build.gradle b/gcm/build.gradle new file mode 100644 index 000000000..6834ab90f --- /dev/null +++ b/gcm/build.gradle @@ -0,0 +1,49 @@ +apply plugin: 'com.android.library' + +version = rootProject.ext.commonLibVersion +group = 'com.parse' + +ext { + projDescription = 'Parse Android GCM support.' + artifact = 'parse-gcm-android' + projName = 'Parse-Android' + gitLink = 'https://github.com/parse-community/Parse-SDK-Android' +} + +android { + compileSdkVersion rootProject.ext.compileSdkVersion + + defaultConfig { + minSdkVersion rootProject.ext.minSdkVersion + targetSdkVersion rootProject.ext.targetSdkVersion + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + + } + + packagingOptions { + exclude '**/BuildConfig.class' + } + + lintOptions { + abortOnError false + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + +} + +dependencies { + api "com.google.android.gms:play-services-gcm:$googleLibVersion" + api "com.firebase:firebase-jobdispatcher:$firebaseJobdispatcherVersion" + implementation project(':Parse') +} + +apply from: rootProject.file('gradle/gradle-mvn-push.gradle') diff --git a/gcm/proguard-rules.pro b/gcm/proguard-rules.pro new file mode 100644 index 000000000..f1b424510 --- /dev/null +++ b/gcm/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/gcm/src/main/AndroidManifest.xml b/gcm/src/main/AndroidManifest.xml new file mode 100644 index 000000000..2c9bcc51c --- /dev/null +++ b/gcm/src/main/AndroidManifest.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + diff --git a/gcm/src/main/java/com/parse/gcm/ParseGCM.java b/gcm/src/main/java/com/parse/gcm/ParseGCM.java new file mode 100644 index 000000000..6ee36eca6 --- /dev/null +++ b/gcm/src/main/java/com/parse/gcm/ParseGCM.java @@ -0,0 +1,112 @@ +/* + * 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.gcm; + +import android.content.Context; +import android.os.Bundle; +import android.support.annotation.Nullable; + +import com.firebase.jobdispatcher.Constraint; +import com.firebase.jobdispatcher.FirebaseJobDispatcher; +import com.firebase.jobdispatcher.GooglePlayDriver; +import com.firebase.jobdispatcher.Job; +import com.firebase.jobdispatcher.RetryStrategy; +import com.parse.ManifestInfo; +import com.parse.PLog; + +/** + * Entry point into setting up Parse GCM Push + */ +public class ParseGCM { + + private static final String SENDER_ID_EXTRA = "com.parse.push.gcm_sender_id"; + + static final String TAG = "ParseGCM"; + + private static final String JOB_TAG_REGISTER = "register"; + + /** + * Register your app to start receiving GCM pushes + * + * @param context context + */ + public static void register(Context context) { + //kicks off the background job + PLog.v(TAG, "Scheduling job to register Parse GCM"); + FirebaseJobDispatcher dispatcher = new FirebaseJobDispatcher(new GooglePlayDriver(context.getApplicationContext())); + Job job = dispatcher.newJobBuilder() + .setRecurring(false) + .setReplaceCurrent(true) + // retry with exponential backoff + .setRetryStrategy(RetryStrategy.DEFAULT_EXPONENTIAL) + .setConstraints( + // only run on a network + Constraint.ON_ANY_NETWORK + ) + .setService(ParseGCMJobService.class) // the JobService that will be called + .setTag(JOB_TAG_REGISTER) // uniquely identifies the job + .build(); + + dispatcher.mustSchedule(job); + } + + @Nullable + static String gcmSenderFromManifest(Context context) { + // Look for an element like this as a child of the element: + // + // + // + // 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 + " 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" + + "\" />"); + return null; + } + return senderID; + } + + 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); + } +} diff --git a/gcm/src/main/java/com/parse/gcm/ParseGCMInstanceIDListenerService.java b/gcm/src/main/java/com/parse/gcm/ParseGCMInstanceIDListenerService.java new file mode 100644 index 000000000..b4d2ad17a --- /dev/null +++ b/gcm/src/main/java/com/parse/gcm/ParseGCMInstanceIDListenerService.java @@ -0,0 +1,21 @@ +package com.parse.gcm; + +import com.firebase.jobdispatcher.Constraint; +import com.firebase.jobdispatcher.FirebaseJobDispatcher; +import com.firebase.jobdispatcher.GooglePlayDriver; +import com.firebase.jobdispatcher.Job; +import com.firebase.jobdispatcher.RetryStrategy; +import com.google.android.gms.iid.InstanceIDListenerService; + +/** + * Listens for GCM token refreshes and kicks off a background job to save the token + */ +public class ParseGCMInstanceIDListenerService extends InstanceIDListenerService { + + @Override + public void onTokenRefresh() { + super.onTokenRefresh(); + + ParseGCM.register(getApplicationContext()); + } +} diff --git a/gcm/src/main/java/com/parse/gcm/ParseGCMJobService.java b/gcm/src/main/java/com/parse/gcm/ParseGCMJobService.java new file mode 100644 index 000000000..7138e83ca --- /dev/null +++ b/gcm/src/main/java/com/parse/gcm/ParseGCMJobService.java @@ -0,0 +1,59 @@ +/* + * 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.gcm; + +import com.firebase.jobdispatcher.JobParameters; +import com.firebase.jobdispatcher.JobService; +import com.google.android.gms.gcm.GoogleCloudMessaging; +import com.google.android.gms.iid.InstanceID; +import com.parse.PLog; +import com.parse.ParseInstallation; + +import java.util.concurrent.Callable; + +import bolts.Task; + +/** + * Handles saving the GCM token to the Parse Installation + */ +public class ParseGCMJobService extends JobService { + + @Override + public boolean onStartJob(final JobParameters job) { + PLog.v(ParseGCM.TAG, "Updating GCM token"); + + Task.callInBackground(new Callable() { + @Override + public Void call() throws Exception { + try { + InstanceID instanceID = InstanceID.getInstance(getApplicationContext()); + String senderId = ParseGCM.gcmSenderFromManifest(getApplicationContext()); + String token = instanceID.getToken(senderId, + GoogleCloudMessaging.INSTANCE_ID_SCOPE, null); + ParseInstallation installation = ParseInstallation.getCurrentInstallation(); + installation.setDeviceToken(token); + //even though this is FCM, calling it gcm will work on the backend + installation.setPushType("gcm"); + installation.save(); + PLog.v(ParseGCM.TAG, "GCM registration success"); + } catch (Exception e) { + PLog.e(ParseGCM.TAG, "GCM registration failed", e); + jobFinished(job, true); + } + return null; + } + }); + return true; // Answers the question: "Is there still work going on?" + } + + @Override + public boolean onStopJob(JobParameters job) { + return true; // Answers the question: "Should this job be retried?" + } +} diff --git a/gcm/src/main/java/com/parse/gcm/ParseGCMListenerService.java b/gcm/src/main/java/com/parse/gcm/ParseGCMListenerService.java new file mode 100644 index 000000000..e379a531e --- /dev/null +++ b/gcm/src/main/java/com/parse/gcm/ParseGCMListenerService.java @@ -0,0 +1,34 @@ +package com.parse.gcm; + +import android.os.Bundle; + +import com.google.android.gms.gcm.GcmListenerService; +import com.parse.PLog; +import com.parse.PushRouter; + +import org.json.JSONException; +import org.json.JSONObject; + +public class ParseGCMListenerService extends GcmListenerService { + + @Override + public void onMessageReceived(String s, Bundle bundle) { + super.onMessageReceived(s, bundle); + String pushId = bundle.getString("push_id"); + String timestamp = bundle.getString("time"); + String dataString = bundle.getString("data"); + String channel = bundle.getString("channel"); + + JSONObject data = null; + if (dataString != null) { + try { + data = new JSONObject(dataString); + } catch (JSONException e) { + PLog.e(ParseGCM.TAG, "Ignoring push because of JSON exception while processing: " + dataString, e); + return; + } + } + + PushRouter.getInstance().handlePush(pushId, timestamp, channel, data); + } +} diff --git a/gradle/gradle-mvn-push.gradle b/gradle/gradle-mvn-push.gradle new file mode 100644 index 000000000..44d0550ec --- /dev/null +++ b/gradle/gradle-mvn-push.gradle @@ -0,0 +1,175 @@ +apply plugin: 'com.jfrog.bintray' + +android.libraryVariants.all { variant -> + def name = variant.buildType.name + + def javadoc = task("javadoc${variant.name.capitalize()}", type: Javadoc) { + description "Generates Javadoc for $variant.name." + destinationDir = rootProject.file("docs/api") + source = variant.javaCompiler.source + ext.androidJar = "${android.sdkDirectory}/platforms/${android.compileSdkVersion}/android.jar" + doFirst { + classpath = files(variant.javaCompiler.classpath.files) + files(ext.androidJar) + } + options.docletpath = [rootProject.file("./gradle/ExcludeDoclet.jar")] + options.doclet = "me.grantland.doclet.ExcludeDoclet" + + options.linksOffline("http://d.android.com/reference", "${android.sdkDirectory}/docs/reference") + options.links("http://boltsframework.github.io/docs/android/") + + exclude '**/BuildConfig.java' + exclude '**/R.java' + exclude '**/internal/**' + } + + def javadocJar = task("javadocJar${variant.name.capitalize()}", type: Jar, dependsOn: "javadoc${variant.name.capitalize()}") { + classifier = 'javadoc' + from javadoc.destinationDir + } + + artifacts.add('archives', javadocJar) +} + +//region Maven + +apply plugin: 'maven' +apply plugin: 'signing' + +def isSnapshot = version.endsWith('-SNAPSHOT') +def ossrhUsername = hasProperty('NEXUS_USERNAME') ? NEXUS_USERNAME : System.getenv('CI_NEXUS_USERNAME') +def ossrhPassword = hasProperty('NEXUS_PASSWORD') ? NEXUS_PASSWORD : System.getenv('CI_NEXUS_PASSWORD') + +def pomConfig = { + licenses { + license { + name 'BSD License' + url 'https://github.com/parse-community/Parse-SDK-Android/blob/master/LICENSE' + distribution 'repo' + } + } + + scm { + connection 'scm:git@github.com:parse-community/Parse-SDK-Android.git' + developerConnection 'scm:git@github.com:parse-community/Parse-SDK-Android.git' + url gitLink + } + + developers { + developer { + id 'parse' + name 'Parse' + } + } +} + + +uploadArchives { + repositories.mavenDeployer { + beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } + + repository(url: "https://oss.sonatype.org/service/local/staging/deploy/maven2/") { + authentication(userName: ossrhUsername, password: ossrhPassword) + } + + snapshotRepository(url: "https://oss.sonatype.org/content/repositories/snapshots/") { + authentication(userName: ossrhUsername, password: ossrhPassword) + } + + def basePom = { + name projName + artifactId = artifact + packaging 'aar' + description projDescription + url gitLink + } + + pom.project basePom << pomConfig + } +} + +signing { + required { !isSnapshot && gradle.taskGraph.hasTask("uploadArchives") } + sign configurations.archives +} + +task androidSourcesJar(type: Jar) { + classifier = 'sources' + from android.sourceSets.main.java.sourceFiles +} + +artifacts { + archives androidSourcesJar +} + +//endregion + +// Requires apply plugin: 'com.jfrog.bintray' + +bintray { + user = System.getenv('BINTRAY_USER') + key = System.getenv('BINTRAY_API_KEY') + + publications = ["mavenAar"] + + publish = true + pkg { + repo = 'maven' + name = 'com.parse:parse-android' + userOrg = 'parse' + licenses = ['BSD License'] + vcsUrl = 'https://github.com/parse-community/Parse-SDK-Android' + version { + name = project.version + desc = projDescription + released = new Date() + vcsTag = project.version + + // Sonatype username/passwrod must be set for this operation to happen + mavenCentralSync { + sync = true + user = ossrhUsername + password = ossrhPassword + close = '1' // release automatically + } + } + } +} + +// Create the publication with the pom configuration: +apply plugin: 'digital.wup.android-maven-publish' + +publishing { + publications { + mavenAar(MavenPublication) { + from components.android + groupId group + // We have to specify it here because otherwise Bintray's plugin will assume the artifact's name is Parse + artifactId artifact + artifacts = [androidSourcesJar, javadocJarRelease, bundleRelease] + version version + + } + + } +} + +// End of Bintray plugin + +apply plugin: "com.jfrog.artifactory" + +artifactory { + contextUrl = 'https://oss.jfrog.org' + publish { + repository { + repoKey = 'oss-snapshot-local' // The Artifactory repository key to publish to + + username = System.getenv('BINTRAY_USER') + password = System.getenv('BINTRAY_API_KEY') + maven = true + } + defaults { + publishArtifacts = true + publications('mavenAar') + } + } +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 7cd3dd471..85490d96a 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1,2 @@ -include ':Parse' +include ':Parse', ':fcm', ':gcm'