diff --git a/play-services-base/build.gradle b/play-services-base/build.gradle
new file mode 100644
index 00000000..9c397627
--- /dev/null
+++ b/play-services-base/build.gradle
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2013-2015 microG Project Team
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+apply plugin: 'com.android.library'
+
+String getMyVersionName() {
+ def stdout = new ByteArrayOutputStream()
+ if (rootProject.file("gradlew").exists())
+ exec { commandLine 'git', 'describe', '--tags', '--always', '--dirty'; standardOutput = stdout }
+ else // automatic build system, don't tag dirty
+ exec { commandLine 'git', 'describe', '--tags', '--always'; standardOutput = stdout }
+ return stdout.toString().trim().substring(1)
+}
+
+android {
+ compileSdkVersion androidCompileSdk()
+ buildToolsVersion "$androidBuildVersionTools"
+
+ defaultConfig {
+ versionName getMyVersionName()
+ minSdkVersion androidMinSdk()
+ targetSdkVersion androidTargetSdk()
+ }
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+}
+
+dependencies {
+ api project(':play-services-basement')
+ api project(':play-services-tasks')
+
+ implementation "androidx.fragment:fragment:$fragmentVersion"
+}
diff --git a/play-services-base/gradle.properties b/play-services-base/gradle.properties
new file mode 100644
index 00000000..b31a23e5
--- /dev/null
+++ b/play-services-base/gradle.properties
@@ -0,0 +1,34 @@
+#
+# Copyright 2013-2016 microG Project Team
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+POM_NAME=Play Services Library Base
+POM_DESCRIPTION=Base classes used by all Play Services Library modules
+
+POM_PACKAGING=aar
+
+POM_URL=https://github.com/microg/android_external_GmsLib
+
+POM_SCM_URL=https://github.com/microg/android_external_GmsLib
+POM_SCM_CONNECTION=scm:git@github.com:microg/android_external_GmsLib.git
+POM_SCM_DEV_CONNECTION=scm:git@github.com:microg/android_external_GmsLib.git
+
+POM_LICENCE_NAME=The Apache Software License, Version 2.0
+POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt
+POM_LICENCE_DIST=repo
+
+POM_DEVELOPER_ID=mar-v-in
+POM_DEVELOPER_NAME=Marvin W
+
diff --git a/play-services-base/src/main/AndroidManifest.xml b/play-services-base/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..559a33ba
--- /dev/null
+++ b/play-services-base/src/main/AndroidManifest.xml
@@ -0,0 +1,31 @@
+
+
+
+
+ *+ */ +@PublicApi +public abstract class GcmListenerService extends Service { + private static final String TAG = "GcmListenerService"; + + private final Object lock = new Object(); + private int startId; + private int counter = 0; + + public final IBinder onBind(Intent intent) { + return null; + } + + /** + * Called when GCM server deletes pending messages due to exceeded + * storage limits, for example, when the device cannot be reached + * for an extended period of time. + * + * It is recommended to retrieve any missing messages directly from the + * app server. + */ + public void onDeletedMessages() { + // To be overwritten + } + + /** + * Called when a message is received. + * + * @param from describes message sender. + * @param data message data as String key/value pairs. + */ + public void onMessageReceived(String from, Bundle data) { + // To be overwritten + } + + /** + * Called when an upstream message has been successfully sent to the + * GCM connection server. + * + * @param msgId of the upstream message sent using + * {@link com.google.android.gms.gcm.GoogleCloudMessaging#send(java.lang.String, java.lang.String, android.os.Bundle)}. + */ + public void onMessageSent(String msgId) { + // To be overwritten + } + + /** + * Called when there was an error sending an upstream message. + * + * @param msgId of the upstream message sent using + * {@link com.google.android.gms.gcm.GoogleCloudMessaging#send(java.lang.String, java.lang.String, android.os.Bundle)}. + * @param error description of the error. + */ + public void onSendError(String msgId, String error) { + // To be overwritten + } + + public final int onStartCommand(final Intent intent, int flags, int startId) { + synchronized (lock) { + this.startId = startId; + this.counter++; + } + + if (intent != null) { + if (ACTION_NOTIFICATION_OPEN.equals(intent.getAction())) { + handlePendingNotification(intent); + finishCounter(); + GcmReceiver.completeWakefulIntent(intent); + } else if (ACTION_C2DM_RECEIVE.equals(intent.getAction())) { + new AsyncTask+ * + * + *+ *
+ *+ * An execution contains the tag identifier which your client app provides. This identifies + * one "task", or piece of work, that you mean to perform. Consider the tag to be the key to which + * your task logic is paired. + *+ * + *+ * + *+ *
+ * // Schedule a task to occur between five and fifteen seconds from now: + * OneoffTask myTask = new OneoffTask.Builder() + * .setService(MyGcmTaskService.class) + * .setExecutionWindow( + * 5 * DateUtil.MINUTE_IN_SECONDS, 15 * DateUtil.MINUTE_IN_SECONDS) + * .setTag("test-upload") + * .build(); + * GcmNetworkManager.get(this).schedule(myTask); + * ... + * // Implement service logic to be notified when the task elapses: + * MyUploadService extends GcmTaskService { + * + * @Override public int onRunTask(TaskParams params) { + * // Do some upload work. + * return GcmNetworkManager.RESULT_SUCCESS; + * } + * } + *+ * To help in debugging your tasks, run + *
adb shell dumpsys activity service GcmService --endpoints [...]
+ * If you want to execute your task immediately (for debugging) you can execute tasks from the
+ * command line via:
+ * adb shell am broadcast -a "com.google.android.gms.gcm.ACTION_TRIGGER_TASK" \
+ * -e component -e tag
+ * Where COMPONENT_NAME: The full
+ * {@link ComponentName#flattenToString()} returned for your implementation of
+ * {@link com.google.android.gms.gcm.GcmTaskService}.
+ * TAG: the tag you want to have land in
+ * {@link com.google.android.gms.gcm.GcmTaskService#onRunTask(com.google.android.gms.gcm.TaskParams)}
+ * Example usage for the gradle target GcmTestProxy service:
+ * adb shell am broadcast -a "com.google.android.gms.gcm.ACTION_TRIGGER_TASK" \
+ * -e component "com.google.android.gms.gcm.test.proxy/.internal.nstest.TestNetworkTaskService" \
+ * -e tag "upload"
+ * This is only available if the device is a test-keys build. This will replace any
+ * previously scheduled task with the same tag! This will have especially awkward effects
+ * if your original task was a periodic, because the debug task is scheduled as a one-off.
+ */
+public class GcmNetworkManager {
+ /**
+ * Indicates a task has failed, but not to reschedule.
+ */
+ public static final int RESULT_FAILURE = 2;
+
+ /**
+ * Indicates a task has failed to execute, and must be retried with back-off.
+ */
+ public static final int RESULT_RESCHEDULE = 1;
+
+ /**
+ * Indicates a task has successfully been executed, and can be removed from the queue.
+ */
+ public static final int RESULT_SUCCESS = 0;
+
+ private static GcmNetworkManager INSTANCE;
+
+ private final Context context;
+
+ private GcmNetworkManager(Context context) {
+ this.context = context;
+ }
+
+ /**
+ * Cancels all tasks previously scheduled against the provided GcmTaskService. Note that a
+ * cancel will have no effect on an in-flight task.
+ *
+ * @param gcmTaskService The endpoint for which you want to cancel all outstanding tasks.
+ */
+ public void cancelAllTasks(Class extends GcmTaskService> gcmTaskService) {
+ validateService(gcmTaskService.getName());
+ Intent scheduleIntent = createScheduleIntent();
+ if (scheduleIntent != null) {
+ scheduleIntent.putExtra(EXTRA_SCHEDULER_ACTION, SCHEDULER_ACTION_CANCEL_ALL);
+ scheduleIntent.putExtra(EXTRA_COMPONENT, new ComponentName(context, gcmTaskService));
+ context.sendBroadcast(scheduleIntent);
+ }
+ }
+
+ /**
+ * Cancel a task, specified by tag. Note that a cancel will have no effect on an in-flight
+ * task.
+ *
+ * @param tag The tag to uniquely identify this task on this endpoint.
+ * @param gcmTaskService The endpoint for which you want to cancel all outstanding tasks.
+ */
+ public void cancelTask(String tag, Class extends GcmTaskService> gcmTaskService) {
+ if (TextUtils.isEmpty(tag) || tag.length() < 100) throw new IllegalArgumentException("tag invalid");
+ validateService(gcmTaskService.getName());
+ Intent scheduleIntent = createScheduleIntent();
+ if (scheduleIntent != null) {
+ scheduleIntent.putExtra(EXTRA_SCHEDULER_ACTION, SCHEDULER_ACTION_CANCEL);
+ scheduleIntent.putExtra(EXTRA_TAG, tag);
+ scheduleIntent.putExtra(EXTRA_COMPONENT, new ComponentName(context, gcmTaskService));
+ context.sendBroadcast(scheduleIntent);
+ }
+ }
+
+ /**
+ * Use this function to access the GcmNetworkManager API.
+ *
+ * @param context Context of the calling app.
+ * @return GcmNetworkManager object.
+ */
+ public static GcmNetworkManager getInstance(Context context) {
+ synchronized (GcmNetworkManager.class) {
+ if (INSTANCE == null) {
+ INSTANCE = new GcmNetworkManager(context);
+ }
+ return INSTANCE;
+ }
+ }
+
+ /**
+ * Entry point to schedule a task with the network manager.
+ *
+ * If GooglePlayServices is unavailable (upgrading, missing, etc). This call will fail silently.
+ * You should wrap it in a call to {@link com.google.android.gms.common.GooglePlayServicesUtil#isGooglePlayServicesAvailable(android.content.Context)}
+ *
+ * @param task Task constructed using {@link com.google.android.gms.gcm.Task.Builder}. Can be
+ * an instance of {@link com.google.android.gms.gcm.PeriodicTask} or
+ * {@link com.google.android.gms.gcm.OneoffTask}.
+ */
+ public void schedule(Task task) {
+ validateService(task.getServiceName());
+ Intent scheduleIntent = createScheduleIntent();
+ if (scheduleIntent != null) {
+ Bundle extras = scheduleIntent.getExtras();
+ extras.putString(EXTRA_SCHEDULER_ACTION, SCHEDULER_ACTION_SCHEDULE);
+ task.toBundle(extras);
+ scheduleIntent.putExtras(extras);
+ context.sendBroadcast(scheduleIntent);
+ }
+ }
+
+ private Intent createScheduleIntent() {
+ if (!packageExists(GMS_PACKAGE_NAME)) return null;
+ Intent scheduleIntent = new Intent(ACTION_SCHEDULE);
+ scheduleIntent.setPackage(GMS_PACKAGE_NAME);
+ scheduleIntent.putExtra("app", PendingIntent.getBroadcast(context, 0, new Intent(), 0));
+ return scheduleIntent;
+ }
+
+ private boolean packageExists(String packageName) {
+ try {
+ PackageManager pm = context.getPackageManager();
+ pm.getPackageInfo(packageName, 0);
+ return true;
+ } catch (PackageManager.NameNotFoundException e) {
+ return false;
+ }
+ }
+
+ private void validateService(String serviceName) {
+ if (serviceName == null) throw new NullPointerException("No service provided");
+ Intent taskIntent = new Intent(ACTION_TASK_READY);
+ taskIntent.setPackage(context.getPackageName());
+ PackageManager pm = context.getPackageManager();
+ List+ * String topic = "/topics/myTopic"; + * String registrationToken = InstanceID.getInstance(context) + * .getToken(SENDER_ID, GoogleCloudMessaging.INSTANCE_ID_SCOPE, null); + * GcmPubSub.getInstance(context).subscribe(registrationToken, topic, null); + * // Messages published to the topic will be received as regular GCM messages + * // with 'from' set to "/topics/myTopic"+ * To publish to a topic, see + * GCM server documentation. + */ +public class GcmPubSub { + + private static final Pattern topicPattern = Pattern.compile("/topics/[a-zA-Z0-9-_.~%]{1,900}"); + private static GcmPubSub INSTANCE; + + private final InstanceID instanceId; + + public GcmPubSub(Context context) { + this.instanceId = InstanceID.getInstance(context); + } + + /** + * Returns an instance of GCM PubSub. + * + * @return GcmPubSub instance + */ + public static synchronized GcmPubSub getInstance(Context context) { + if (INSTANCE == null) { + INSTANCE = new GcmPubSub(context); + } + return INSTANCE; + } + + /** + * Subscribes an app instance to a topic, enabling it to receive messages + * sent to that topic. + * + * The topic sender must be authorized to send messages to the + * app instance. To authorize it, call {@link com.google.android.gms.iid.InstanceID#getToken(java.lang.String, java.lang.String)} + * with the sender ID and {@link com.google.android.gms.gcm.GoogleCloudMessaging#INSTANCE_ID_SCOPE} + * + * Do not call this function on the main thread. + * + * @param registrationToken {@link com.google.android.gms.iid.InstanceID} token that authorizes topic + * sender to send messages to the app instance. + * @param topic developer defined topic name. + * Must match the following regular expression: + * "/topics/[a-zA-Z0-9-_.~%]{1,900}". + * @param extras (optional) additional information. + * @throws IOException if the request fails. + */ + public void subscribe(String registrationToken, String topic, Bundle extras) throws IOException { + if (TextUtils.isEmpty(registrationToken)) + throw new IllegalArgumentException("No registration token!"); + if (TextUtils.isEmpty(topic) || !topicPattern.matcher(topic).matches()) + throw new IllegalArgumentException("Invalid topic: " + topic); + + if (extras == null) extras = new Bundle(); + extras.putString(EXTRA_TOPIC, topic); + instanceId.getToken(registrationToken, topic, extras); + } + + /** + * Unsubscribes an app instance from a topic, stopping it from receiving + * any further messages sent to that topic. + * + * Do not call this function on the main thread. + * + * @param registrationToken {@link com.google.android.gms.iid.InstanceID} token + * for the same sender and scope that was previously + * used for subscribing to the topic. + * @param topic from which to stop receiving messages. + * @throws IOException if the request fails. + */ + public void unsubscribe(String registrationToken, String topic) throws IOException { + if (TextUtils.isEmpty(topic) || !topicPattern.matcher(topic).matches()) + throw new IllegalArgumentException("Invalid topic: " + topic); + + Bundle extras = new Bundle(); + extras.putString(EXTRA_TOPIC, topic); + instanceId.deleteToken(registrationToken, topic, extras); + } + +} \ No newline at end of file diff --git a/play-services-gcm/src/main/java/com/google/android/gms/gcm/GcmReceiver.java b/play-services-gcm/src/main/java/com/google/android/gms/gcm/GcmReceiver.java new file mode 100644 index 00000000..d160f42b --- /dev/null +++ b/play-services-gcm/src/main/java/com/google/android/gms/gcm/GcmReceiver.java @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2013-2017 microG Project Team + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.gms.gcm; + +import android.Manifest; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; +import android.os.Build; +import android.support.v4.content.WakefulBroadcastReceiver; +import android.util.Base64; +import android.util.Log; + +import static org.microg.gms.gcm.GcmConstants.ACTION_C2DM_REGISTRATION; +import static org.microg.gms.gcm.GcmConstants.ACTION_INSTANCE_ID; +import static org.microg.gms.gcm.GcmConstants.EXTRA_FROM; +import static org.microg.gms.gcm.GcmConstants.EXTRA_RAWDATA; +import static org.microg.gms.gcm.GcmConstants.EXTRA_RAWDATA_BASE64; +import static org.microg.gms.gcm.GcmConstants.GCMID_INSTANCE_ID; +import static org.microg.gms.gcm.GcmConstants.GCMID_REFRESH; + +/** + *
WakefulBroadcastReceiver
that receives GCM messages and delivers them to an
+ * application-specific {@link com.google.android.gms.gcm.GcmListenerService} subclass.
+ *
+ * This receiver should be declared in your application's manifest file as follows:
+ *
+ * + *+ * The+ * + * + *+ * + * + *
com.google.android.c2dm.permission.SEND
permission is held by Google Play
+ * services. This prevents other apps from invoking the broadcast receiver.
+ */
+public class GcmReceiver extends WakefulBroadcastReceiver {
+ private static final String TAG = "GcmReceiver";
+
+ public void onReceive(Context context, Intent intent) {
+ sanitizeIntent(context, intent);
+ enforceIntentClassName(context, intent);
+ sendIntent(context, intent);
+ if (getResultCode() == 0) setResultCodeIfOrdered(-1);
+ }
+
+ private void sanitizeIntent(Context context, Intent intent) {
+ intent.setComponent(null);
+ intent.setPackage(context.getPackageName());
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
+ intent.removeCategory(context.getPackageName());
+ }
+ String from = intent.getStringExtra(EXTRA_FROM);
+ if (ACTION_C2DM_REGISTRATION.equals(intent.getAction()) || GCMID_INSTANCE_ID.equals(from) || GCMID_REFRESH.equals(from)) {
+ intent.setAction(ACTION_INSTANCE_ID);
+ }
+ String base64encoded = intent.getStringExtra(EXTRA_RAWDATA_BASE64);
+ if (base64encoded != null) {
+ intent.putExtra(EXTRA_RAWDATA, Base64.decode(base64encoded, Base64.DEFAULT));
+ intent.removeExtra(EXTRA_RAWDATA_BASE64);
+ }
+ }
+
+ private void enforceIntentClassName(Context context, Intent intent) {
+ ResolveInfo resolveInfo = context.getPackageManager().resolveService(intent, 0);
+ if (resolveInfo == null || resolveInfo.serviceInfo == null) {
+ Log.e(TAG, "Failed to resolve target intent service, skipping classname enforcement");
+ return;
+ }
+ ServiceInfo serviceInfo = resolveInfo.serviceInfo;
+ if (!context.getPackageName().equals(serviceInfo.packageName) || serviceInfo.name == null) {
+ Log.e(TAG, "Error resolving target intent service, skipping classname enforcement. Resolved service was: " + serviceInfo.packageName + "/" + serviceInfo.name);
+ return;
+ }
+ intent.setClassName(context, serviceInfo.name.startsWith(".") ? (context.getPackageName() + serviceInfo.name) : serviceInfo.name);
+ }
+
+ private void sendIntent(Context context, Intent intent) {
+ setResultCodeIfOrdered(500);
+ try {
+ ComponentName startedComponent;
+ if (context.checkCallingOrSelfPermission(Manifest.permission.WAKE_LOCK) == PackageManager.PERMISSION_GRANTED) {
+ startedComponent = startWakefulService(context, intent);
+ } else {
+ Log.d(TAG, "Missing wake lock permission, service start may be delayed");
+ startedComponent = context.startService(intent);
+ }
+ if (startedComponent == null) {
+ Log.e(TAG, "Error while delivering the message: ServiceIntent not found.");
+ setResultCodeIfOrdered(404);
+ } else {
+ setResultCodeIfOrdered(-1);
+ }
+ } catch (SecurityException e) {
+ Log.e(TAG, "Error while delivering the message to the serviceIntent", e);
+ setResultCodeIfOrdered(401);
+ }
+ }
+
+ private void setResultCodeIfOrdered(int code) {
+ if (isOrderedBroadcast()) {
+ setResultCode(code);
+ }
+ }
+}
\ No newline at end of file
diff --git a/play-services-gcm/src/main/java/com/google/android/gms/gcm/GcmTaskService.java b/play-services-gcm/src/main/java/com/google/android/gms/gcm/GcmTaskService.java
new file mode 100644
index 00000000..0d790589
--- /dev/null
+++ b/play-services-gcm/src/main/java/com/google/android/gms/gcm/GcmTaskService.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2013-2017 microG Project Team
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.gms.gcm;
+
+import android.app.Service;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.Parcelable;
+import android.os.PowerManager;
+import android.util.Log;
+
+import org.microg.gms.common.PublicApi;
+import org.microg.gms.gcm.GcmConstants;
+
+/**
+ * Implemented by the client application to provide an endpoint for the {@link com.google.android.gms.gcm.GcmNetworkManager}
+ * to call back to when a task is ready to be executed.
+ *
+ * Clients must add this service to their manifest and implement
+ * {@link com.google.android.gms.gcm.GcmTaskService#onRunTask(com.google.android.gms.gcm.TaskParams)}.
+ * This service must provide an {@link IntentFilter} on the action
+ * {@link com.google.android.gms.gcm.GcmTaskService#SERVICE_ACTION_EXECUTE_TASK}. Here's an example:
+ * + *+ * The return value of onRunTask(TaskParams) will determine what the manager does with subsequent + * executions of this task. Specifically you can return {@link com.google.android.gms.gcm.GcmNetworkManager#RESULT_RESCHEDULE} + * to have this task be re-executed again shortly subject to exponential back-off. Returning + * {@link com.google.android.gms.gcm.GcmNetworkManager#RESULT_FAILURE} for a periodic task will only affect the executing + * instance of the task, and future tasks will be executed as normal. + * + * Once a task is running it will not be cancelled, however a newly scheduled task with the same + * tag will not be executed until the active task has completed. This newly scheduled task will + * replace the previous task, regardless of whether the previous task returned + * {@link com.google.android.gms.gcm.GcmNetworkManager#RESULT_RESCHEDULE}. + * + * Bear in mind that your service may receive multiple calls from the scheduler at once + * (specifically if you've made multiple schedule requests that overlap). If this is the case, your + * implementation of {@link com.google.android.gms.gcm.GcmTaskService#onRunTask(com.google.android.gms.gcm.TaskParams)} must be thread-safe. + * + * The scheduler will hold a {@link PowerManager.WakeLock} for your service, however + * after three minutes of execution if your task has not returned it will be considered to + * have timed out, and the wakelock will be released. Rescheduling your task at this point + * will have no effect. + * If you suspect your task will run longer than this you should start your own service + * explicitly or use some other mechanism; this API is intended for relatively quick network + * operations. + * + * Your task will run at priority Process.THREAD_PRIORITY_BACKGROUND. If this + * is not appropriate, you should start your own service with suitably + * conditioned threads. + */ +@PublicApi +public abstract class GcmTaskService extends Service { + private static final String TAG = "GcmTaskService"; + + /** + * Action broadcast by the GcmNetworkManager to the requesting package when + * a scheduled task is ready for execution. + */ + public static final String SERVICE_ACTION_EXECUTE_TASK = GcmConstants.ACTION_TASK_READY; + + /** + * Action that a {@link com.google.android.gms.gcm.GcmTaskService} is started with when the service needs to initialize + * its tasks. + */ + public static final String SERVICE_ACTION_INITIALIZE = GcmConstants.ACTION_TASK_INITIALZE; + + /** + * You must protect your service with this permission to avoid being bound to by an + * application other than Google Play Services. + */ + public static final String SERVICE_PERMISSION = GcmConstants.PERMISSION_NETWORK_TASK; + + public IBinder onBind(Intent intent) { + return null; + } + + /** + * When your package is removed or updated, all of its network tasks are cleared by the + * GcmNetworkManager. You can override this method to reschedule them in the case of an + * updated package. This is not called when your application is first installed. + * + * This is called on your application's main thread. + */ + public void onInitializeTasks() { + // To be overwritten + } + + /** + * Override this function to provide the logic for your task execution. + * + * @param params Parameters provided at schedule time with + * {@link com.google.android.gms.gcm.OneoffTask.Builder#setTag(java.lang.String)} + * @return One of {@link com.google.android.gms.gcm.GcmNetworkManager#RESULT_SUCCESS}, + * {@link com.google.android.gms.gcm.GcmNetworkManager#RESULT_RESCHEDULE}, or + * {@link com.google.android.gms.gcm.GcmNetworkManager#RESULT_FAILURE}. + */ + public abstract int onRunTask(TaskParams params); + + /** + * Receives the command to begin doing work, for which it spawns another thread. + */ + public int onStartCommand(Intent intent, int flags, int startId) { + intent.setExtrasClassLoader(PendingCallback.class.getClassLoader()); + if (SERVICE_ACTION_EXECUTE_TASK.equals(intent.getAction())) { + String tag = intent.getStringExtra("tag"); + Parcelable callback = intent.getParcelableExtra("callback"); + Bundle extras = intent.getBundleExtra("extras"); + if (callback == null || !(callback instanceof PendingCallback)) { + Log.w(TAG, tag + ": Invalid callback!"); + return START_NOT_STICKY; + } + + // TODO ensure single instance + + // TODO run task in new thread + } else if (SERVICE_ACTION_INITIALIZE.equals(intent.getAction())) { + this.onInitializeTasks(); + + // TODO ensure single instance + } + + return START_NOT_STICKY; + } + +} diff --git a/play-services-gcm/src/main/java/com/google/android/gms/gcm/GoogleCloudMessaging.java b/play-services-gcm/src/main/java/com/google/android/gms/gcm/GoogleCloudMessaging.java new file mode 100644 index 00000000..619a39ff --- /dev/null +++ b/play-services-gcm/src/main/java/com/google/android/gms/gcm/GoogleCloudMessaging.java @@ -0,0 +1,322 @@ +/* + * Copyright (C) 2013-2017 microG Project Team + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.gms.gcm; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.os.Looper; +import android.text.TextUtils; + +import com.google.android.gms.iid.InstanceID; + +import org.microg.gms.common.PublicApi; +import org.microg.gms.gcm.CloudMessagingRpc; +import org.microg.gms.gcm.GcmConstants; + +import java.io.IOException; + +import static org.microg.gms.common.Constants.GMS_PACKAGE_NAME; +import static org.microg.gms.gcm.GcmConstants.ACTION_C2DM_RECEIVE; +import static org.microg.gms.gcm.GcmConstants.EXTRA_DELAY; +import static org.microg.gms.gcm.GcmConstants.EXTRA_ERROR; +import static org.microg.gms.gcm.GcmConstants.EXTRA_MESSAGE_ID; +import static org.microg.gms.gcm.GcmConstants.EXTRA_MESSAGE_TYPE; +import static org.microg.gms.gcm.GcmConstants.EXTRA_REGISTRATION_ID; +import static org.microg.gms.gcm.GcmConstants.EXTRA_SENDER; +import static org.microg.gms.gcm.GcmConstants.EXTRA_SENDER_LEGACY; +import static org.microg.gms.gcm.GcmConstants.EXTRA_SEND_FROM; +import static org.microg.gms.gcm.GcmConstants.EXTRA_SEND_TO; +import static org.microg.gms.gcm.GcmConstants.EXTRA_TTL; + +/** + * GoogleCloudMessaging (GCM) enables apps to communicate with their app servers + * using simple messages. + * + * To send or receive messages, the app must get a + * registrationToken from {@link com.google.android.gms.iid.InstanceID#getToken(java.lang.String, java.lang.String)}, which authorizes an + * app server to send messages to an app instance. Pass sender ID and + * {@link com.google.android.gms.gcm.GoogleCloudMessaging#INSTANCE_ID_SCOPE} as parameters to the method. + * A sender ID is a project number acquired from the API console, as described in + * Getting Started. + * + * In order to receive GCM messages, declare {@link com.google.android.gms.gcm.GcmReceiver} + * and an implementation of {@link com.google.android.gms.gcm.GcmListenerService} in the app manifest. + * {@link com.google.android.gms.gcm.GcmReceiver} will pass the incoming messages to the implementation + * of {@link com.google.android.gms.gcm.GcmListenerService}. To process messages, override base class + * methods to handle any events required by the application. + * + * Client apps can send upstream messages back to the app server using the XMPP-based + * Cloud Connection Server, + * For example: + * + * gcm.send(SENDER_ID + "@gcm.googleapis.com", id, data); + * See Implementing GCM Client on Android for more details. + */ +@PublicApi +public class GoogleCloudMessaging { + /** + * The GCM {@link com.google.android.gms.gcm.GoogleCloudMessaging#register(java.lang.String...)} and {@link com.google.android.gms.gcm.GoogleCloudMessaging#unregister()} methods are + * blocking. Blocking methods must not be called on the main thread. + */ + public static final String ERROR_MAIN_THREAD = "MAIN_THREAD"; + + /** + * The device can't read the response, or there was a 500/503 from the + * server that can be retried later. The application should use exponential + * back off and retry. + */ + public static final String ERROR_SERVICE_NOT_AVAILABLE = GcmConstants.ERROR_SERVICE_NOT_AVAILABLE; + + /** + * Specifies scope used in obtaining GCM registrationToken when calling + * {@link com.google.android.gms.iid.InstanceID#getToken(java.lang.String, java.lang.String)} + */ + public static final String INSTANCE_ID_SCOPE = GcmConstants.INSTANCE_ID_SCOPE_GCM; + + /** + * Returned by {@link com.google.android.gms.gcm.GoogleCloudMessaging#getMessageType(android.content.Intent)} to indicate that the server deleted + * some pending messages because they exceeded the storage limits. The + * application should contact the server to retrieve the discarded messages. + * + * @deprecated Instead implement {@link com.google.android.gms.gcm.GcmListenerService#onDeletedMessages()} + */ + @Deprecated + public static final String MESSAGE_TYPE_DELETED = GcmConstants.MESSAGE_TYPE_DELETED_MESSAGE; + + /** + * Returned by {@link com.google.android.gms.gcm.GoogleCloudMessaging#getMessageType(android.content.Intent)} to indicate a regular message. + * + * @deprecated Instead implement {@link com.google.android.gms.gcm.GcmListenerService#onMessageReceived(java.lang.String, android.os.Bundle)} + */ + @Deprecated + public static final String MESSAGE_TYPE_MESSAGE = GcmConstants.MESSAGE_TYPE_GCM; + + /** + * Returned by {@link com.google.android.gms.gcm.GoogleCloudMessaging#getMessageType(android.content.Intent)} to indicate a send error. + * The intent includes the message ID of the message and an error code. + * + * @deprecated Instead implement {@link com.google.android.gms.gcm.GcmListenerService#onSendError(java.lang.String, java.lang.String)} + */ + @Deprecated + public static final String MESSAGE_TYPE_SEND_ERROR = GcmConstants.MESSAGE_TYPE_SEND_ERROR; + + /** + * Returned by {@link com.google.android.gms.gcm.GoogleCloudMessaging#getMessageType(android.content.Intent)} to indicate a sent message has been received by the GCM + * server. The intent includes the message ID of the message. + * + * @deprecated Instead implement {@link com.google.android.gms.gcm.GcmListenerService#onMessageSent(java.lang.String)} + */ + @Deprecated + public static final String MESSAGE_TYPE_SEND_EVENT = GcmConstants.MESSAGE_TYPE_SEND_EVENT; + + private static GoogleCloudMessaging instance; + + private CloudMessagingRpc rpc; + private Context context; + + public GoogleCloudMessaging() { + } + + /** + * Must be called when your application is done using GCM, to release + * internal resources. + */ + public synchronized void close() { + instance = null; + rpc.close(); + } + + /** + * Return the singleton instance of GCM. + */ + public static GoogleCloudMessaging getInstance(Context context) { + if (instance == null) { + instance = new GoogleCloudMessaging(); + instance.context = context.getApplicationContext(); + instance.rpc = new CloudMessagingRpc(instance.context); + } + return instance; + } + + /** + * Return the message type from an intent passed into a client app's broadcast receiver. There + * are two general categories of messages passed from the server: regular GCM messages, + * and special GCM status messages. + * + * The possible types are: + * {@link #MESSAGE_TYPE_MESSAGE}, {@link #MESSAGE_TYPE_DELETED}, {@link #MESSAGE_TYPE_SEND_EVENT} and {@link #MESSAGE_TYPE_SEND_ERROR} + * + * You can use this method to filter based on message type. Since it is likely that GCM will + * be extended in the future with new message types, just ignore any message types you're not + * interested in, or that you don't recognize. + * + * @return The message type or null if the intent is not a GCM intent + */ + public String getMessageType(Intent intent) throws IOException { + if (intent == null || !ACTION_C2DM_RECEIVE.equals(intent.getAction())) return null; + if (!intent.hasExtra(EXTRA_MESSAGE_TYPE)) return MESSAGE_TYPE_MESSAGE; + return intent.getStringExtra(EXTRA_MESSAGE_TYPE); + } + + /** + * Register the application for GCM and return the registration ID. You must call this once, + * when your application is installed, and send the returned registration ID to the server. + * + * Repeated calls to this method will return the original registration ID. + * + * If you want to modify the list of senders, you must call+ * + *+ * + *+ *
unregister()
first.
+ *
+ * Most applications use a single sender ID. You may use multiple senders if different
+ * servers may send messages to the app or for testing.
+ *
+ * @param senderIds list of project numbers or Google accounts identifying who is allowed to
+ * send messages to this application.
+ * @return registration id
+ * @throws IOException
+ * @deprecated Instead, for GCM registration, use
+ * {@link com.google.android.gms.iid.InstanceID#getToken(java.lang.String, java.lang.String)}.
+ * Set authorizedEntity to a sender ID and scope to {@link com.google.android.gms.gcm.GoogleCloudMessaging#INSTANCE_ID_SCOPE}.
+ */
+ @Deprecated
+ public String register(String... senderIds) throws IOException {
+ if (Looper.getMainLooper() == Looper.myLooper()) throw new IOException(ERROR_MAIN_THREAD);
+
+ if (senderIds == null || senderIds.length == 0) throw new IllegalArgumentException("No sender ids");
+ StringBuilder sb = new StringBuilder(senderIds[0]);
+ for (int i = 1; i < senderIds.length; i++) {
+ sb.append(',').append(senderIds[i]);
+ }
+ String sender = sb.toString();
+
+ if (isLegacyFallback()) {
+ Bundle extras = new Bundle();
+ extras.putString(EXTRA_SENDER_LEGACY, sender);
+ return InstanceID.getInstance(context).getToken(sb.toString(), INSTANCE_ID_SCOPE, extras);
+ } else {
+ Bundle extras = new Bundle();
+ extras.putString(EXTRA_SENDER, sender);
+ return rpc.handleRegisterMessageResult(rpc.sendRegisterMessageBlocking(extras));
+ }
+ }
+
+ /**
+ * Send an upstream ("device to cloud") message. You can only use the upstream feature
+ * if your GCM implementation uses the XMPP-based
+ * Cloud Connection Server.
+ *
+ * The current limits for max storage time and number of outstanding messages per
+ * application are documented in the
+ * GCM Developers Guide.
+ *
+ * @param to string identifying the receiver of the message in the format of
+ * SENDER_ID@gcm.googleapis.com
. The SENDER_ID
should be one of the sender
+ * IDs used when calling {@link com.google.android.gms.iid.InstanceID#getToken(java.lang.String, java.lang.String)}
+ * @param msgId ID of the message. This is generated by the application. It must be
+ * unique for each message. This allows error callbacks and debugging.
+ * @param timeToLive If 0, we'll attempt to send immediately and return an
+ * error if we're not connected. Otherwise, the message will be queued.
+ * As for server-side messages, we don't return an error if the message has been
+ * dropped because of TTL—this can happen on the server side, and it would require
+ * extra communication.
+ * @param data key/value pairs to be sent. Values must be String, any other type will
+ * be ignored.
+ * @throws IllegalArgumentException
+ * @throws IOException
+ */
+ public void send(String to, String msgId, long timeToLive, Bundle data) throws IOException {
+ if (TextUtils.isEmpty(to)) throw new IllegalArgumentException("Invalid 'to'");
+
+ if (isLegacyFallback()) {
+ Bundle extras = new Bundle();
+ for (String key : data.keySet()) {
+ Object o = extras.get(key);
+ if (o instanceof String) {
+ extras.putString("gcm." + key, (String) o);
+ }
+ }
+ extras.putString(EXTRA_SEND_TO, to);
+ extras.putString(EXTRA_MESSAGE_ID, msgId);
+ InstanceID.getInstance(context).requestToken("GCM", "upstream", extras);
+ } else {
+ Bundle extras = data != null ? new Bundle(data) : new Bundle();
+ extras.putString(EXTRA_SEND_TO, to);
+ extras.putString(EXTRA_SEND_FROM, getFrom(to));
+ extras.putString(EXTRA_MESSAGE_ID, msgId);
+ extras.putLong(EXTRA_TTL, timeToLive);
+ extras.putInt(EXTRA_DELAY, -1);
+ rpc.sendGcmMessage(extras);
+ }
+ }
+
+ /**
+ * Send an upstream ("device to cloud") message. You can only use the upstream feature
+ * if your GCM implementation uses the XMPP-based
+ * Cloud Connection Server.
+ *
+ * When there is an active connection the message will be sent immediately, otherwise the
+ * message will be queued for the maximum interval.
+ *
+ * @param to string identifying the receiver of the message in the format of
+ * SENDER_ID@gcm.googleapis.com
. The SENDER_ID
should be one of the sender
+ * IDs used when calling {@link com.google.android.gms.iid.InstanceID#getToken(java.lang.String, java.lang.String)}
+ * @param msgId ID of the message. This is generated by the application. It must be
+ * unique for each message. This allows error callbacks and debugging.
+ * @param data key/value pairs to be sent. Values must be String—any other type will
+ * be ignored.
+ * @throws IllegalArgumentException
+ * @throws IOException
+ */
+ public void send(String to, String msgId, Bundle data) throws IOException {
+ send(to, msgId, -1, data);
+ }
+
+ /**
+ * Unregister the application. Calling unregister()
stops any
+ * messages from the server. This is a blocking call—you shouldn't call
+ * it from the UI thread.
+ *
+ * You should rarely (if ever) need to call this method. Not only is it
+ * expensive in terms of resources, but it invalidates all your registration IDs
+ * returned from register() or subscribe(). This should not be done
+ * unnecessarily. A better approach is to simply have your server stop
+ * sending messages.
+ *
+ * @throws IOException if we can't connect to server to unregister.
+ * @deprecated Instead use
+ * {@link com.google.android.gms.iid.InstanceID#deleteToken(java.lang.String, java.lang.String)} or
+ * {@link com.google.android.gms.iid.InstanceID#deleteInstanceID()}.
+ */
+ @Deprecated
+ public void unregister() throws IOException {
+ if (Looper.getMainLooper() == Looper.myLooper()) throw new IOException(ERROR_MAIN_THREAD);
+ InstanceID.getInstance(context).deleteInstanceID();
+ }
+
+ private boolean isLegacyFallback() {
+ String gcmPackageName = CloudMessagingRpc.getGcmPackageName(context);
+ return gcmPackageName != null && gcmPackageName.endsWith(".gsf");
+ }
+
+ private String getFrom(String to) {
+ int i = to.indexOf('@');
+ if (i > 0) {
+ to = to.substring(0, i);
+ }
+ return InstanceID.getInstance(context).getStore().get("", to, INSTANCE_ID_SCOPE);
+ }
+}
\ No newline at end of file
diff --git a/play-services-gcm/src/main/java/com/google/android/gms/gcm/OneoffTask.java b/play-services-gcm/src/main/java/com/google/android/gms/gcm/OneoffTask.java
new file mode 100644
index 00000000..5e56cb12
--- /dev/null
+++ b/play-services-gcm/src/main/java/com/google/android/gms/gcm/OneoffTask.java
@@ -0,0 +1,221 @@
+/*
+ * Copyright (C) 2013-2017 microG Project Team
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.gms.gcm;
+
+import android.os.Bundle;
+import android.os.Parcel;
+
+import org.microg.gms.common.PublicApi;
+
+/**
+ * A task that will execute once,at some point within the specified window.
+ * If one of {@link com.google.android.gms.gcm.GcmNetworkManager#cancelTask(java.lang.String, java.lang.Class extends com.google.android.gms.gcm.GcmTaskService>)} or
+ * {@link com.google.android.gms.gcm.GcmNetworkManager#cancelAllTasks(java.lang.Class extends com.google.android.gms.gcm.GcmTaskService>)} is called before this
+ * executes it will be cancelled.
+ *
+ * Note that you can request a one-off task to be executed at any point in the future, but to
+ * prevent abuse the scheduler will only set an alarm at a minimum of 30 seconds in the
+ * future. Your task can still be run earlier than this if some network event occurs to wake up
+ * the scheduler.
+ */
+@PublicApi
+public class OneoffTask extends com.google.android.gms.gcm.Task {
+ private final long windowStart;
+ private final long windowEnd;
+
+ private OneoffTask(Builder builder) {
+ super(builder);
+ this.windowStart = builder.windowStart;
+ this.windowEnd = builder.windowEnd;
+ }
+
+ private OneoffTask(Parcel source) {
+ super(source);
+ this.windowStart = source.readLong();
+ this.windowEnd = source.readLong();
+ }
+
+ /**
+ * @return The number of seconds from now by which this task must have executed.
+ */
+ public long getWindowEnd() {
+ return windowEnd;
+ }
+
+ /**
+ * @return The number of seconds from now at which this task is eligible for execution.
+ */
+ public long getWindowStart() {
+ return windowStart;
+ }
+
+ /**
+ * Insert the task object into the provided bundle for IPC. Use #fromBundle to recreate the
+ * object on the other side.
+ */
+ public void toBundle(Bundle bundle) {
+ super.toBundle(bundle);
+ bundle.putLong("window_start", this.windowStart);
+ bundle.putLong("window_end", this.windowEnd);
+ }
+
+ public String toString() {
+ return super.toString()
+ + " windowStart=" + this.getWindowStart()
+ + " windowEnd=" + this.getWindowEnd();
+ }
+
+ public void writeToParcel(Parcel parcel, int flags) {
+ super.writeToParcel(parcel, flags);
+ parcel.writeLong(this.windowStart);
+ parcel.writeLong(this.windowEnd);
+ }
+
+ public static final CreatormIntervalInSeconds.
+ *
+ * By default you have no control over where within this period the task will execute.
+ * If you want to restrict the task to run within a certain timeframe from the end of
+ * the period, use {@link com.google.android.gms.gcm.PeriodicTask.Builder#setFlex(long)}
+ */
+ public PeriodicTask.Builder setPeriod(long periodInSeconds) {
+ this.periodInSeconds = periodInSeconds;
+ return this;
+ }
+
+ /**
+ * Optional setter to specify whether this task should be persisted across reboots. This
+ * defaults to true for periodic tasks,
+ *
+ * Callers must hold the permission
+ * android.Manifest.permission.RECEIVE_BOOT_COMPLETED, otherwise this setter is
+ * ignored.
+ *
+ * @param isPersisted True if this task should be persisted across device reboots.
+ */
+ public PeriodicTask.Builder setPersisted(boolean isPersisted) {
+ this.isPersisted = isPersisted;
+ return this;
+ }
+
+ /**
+ * Set the network state your task requires to run. If the specified network is
+ * unavailable your task will not be executed until it becomes available.
+ *
+ * The default for either a periodic or one-off task is
+ * {@link com.google.android.gms.gcm.Task#NETWORK_STATE_CONNECTED}. Note that changing this to
+ * {@link com.google.android.gms.gcm.Task#NETWORK_STATE_ANY} means there is no guarantee that data will be available
+ * when your task executes.
+ *
+ * In addition, the only guarantee for connectivity is at the moment of execution - it is
+ * possible for the device to lose data shortly after your task begins executing.
+ */
+ public PeriodicTask.Builder setRequiredNetwork(int requiredNetworkState) {
+ this.requiredNetworkState = requiredNetworkState;
+ return this;
+ }
+
+ /**
+ * Set whether your task requires that the device be connected to power in order to
+ * execute.
+ *
+ * Use this to defer nonessential operations whenever possible. Note that if you set this
+ * field and the device is not connected to power your task will not run
+ * until the device is plugged in.
+ *
+ * One way to deal with your task not executing until the constraint is met is to schedule
+ * another task without the constraints that is subject to some deadline that you can abide.
+ * This task would be responsible for executing your fallback logic.
+ */
+ public PeriodicTask.Builder setRequiresCharging(boolean requiresCharging) {
+ this.requiresCharging = requiresCharging;
+ return this;
+ }
+
+ /**
+ * Set whichever {@link com.google.android.gms.gcm.GcmTaskService} you implement to execute the logic for this task.
+ * + * @param gcmTaskService Endpoint against which you're scheduling this task. + */ + public PeriodicTask.Builder setService(Class extends GcmTaskService> gcmTaskService) { + this.gcmTaskService = gcmTaskService.getName(); + return this; + } + + /** + * Mandatory setter for specifying the tag identifer for this task. This tag will be + * returned at execution time to your endpoint. See + * {@link com.google.android.gms.gcm.GcmTaskService#onRunTask(com.google.android.gms.gcm.TaskParams)} + * + * Maximum tag length is 100. + * + * @param tag String identifier for this task. Consecutive schedule calls for the same + * tag will update any preexisting task with the same tag. + */ + public PeriodicTask.Builder setTag(String tag) { + this.tag = tag; + return this; + } + + /** + * Optional setter to specify whether this task should override any preexisting tasks + * with the same tag. This defaults to false, which means that a new task will not + * override an existing one. + * + * @param updateCurrent True to update the current task with the parameters of the new. + * Default false. + */ + public PeriodicTask.Builder setUpdateCurrent(boolean updateCurrent) { + this.updateCurrent = updateCurrent; + return this; + } + } +} \ No newline at end of file diff --git a/play-services-gcm/src/main/java/com/google/android/gms/gcm/Task.java b/play-services-gcm/src/main/java/com/google/android/gms/gcm/Task.java new file mode 100644 index 00000000..6f30caed --- /dev/null +++ b/play-services-gcm/src/main/java/com/google/android/gms/gcm/Task.java @@ -0,0 +1,271 @@ +/* + * Copyright (C) 2013-2017 microG Project Team + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.gms.gcm; + +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; + +import org.microg.gms.common.PublicApi; + +/** + * Encapsulates the parameters of a task that you will schedule on the + * {@link com.google.android.gms.gcm.GcmNetworkManager}. + * + * Construct instances of either {@link com.google.android.gms.gcm.PeriodicTask} or + * {@link com.google.android.gms.gcm.OneoffTask} with the desired parameters/behaviour and + * schedule them using {@link com.google.android.gms.gcm.GcmNetworkManager#schedule(com.google.android.gms.gcm.Task)}. + */ +@PublicApi +public abstract class Task implements Parcelable { + + /** + *The maximum size allowed for extras bundle in bytes. + *
+ */ + public static final int EXTRAS_LIMIT_BYTES = 10240; + + /** + *Specify using {@link com.google.android.gms.gcm.Task.Builder#setRequiredNetwork(int)} + * that your task will execute [...] of whether network is available. + *
+ */ + public static final int NETWORK_STATE_ANY = 2; + + /** + *Specify using {@link com.google.android.gms.gcm.Task.Builder#setRequiredNetwork(int)} + * that your task will only execute if [...] sort of data connection is available - + * either metered or unmetered. This is the default.
+ */ + public static final int NETWORK_STATE_CONNECTED = 0; + + /** + *Specify using {@link com.google.android.gms.gcm.Task.Builder#setRequiredNetwork(int)} + * that your task will only execute if there is an unmetered network connection available. + *
+ */ + public static final int NETWORK_STATE_UNMETERED = 1; + + protected static final long UNINITIALIZED = -1; + + private final String serviceName; + private final String tag; + private final boolean updateCurrent; + private final boolean persisted; + private final int requiredNetwork; + private final boolean requiresCharging; + private final Bundle extras; + + Task(Builder builder) { + this.serviceName = builder.gcmTaskService; + this.tag = builder.tag; + this.updateCurrent = builder.updateCurrent; + this.persisted = builder.isPersisted; + this.requiredNetwork = builder.requiredNetworkState; + this.requiresCharging = builder.requiresCharging; + this.extras = builder.extras; + } + + Task(Parcel in) { + this.serviceName = in.readString(); + this.tag = in.readString(); + this.updateCurrent = in.readInt() == 1; + this.persisted = in.readInt() == 1; + this.requiredNetwork = NETWORK_STATE_ANY; + this.requiresCharging = false; + this.extras = null; + } + + public int describeContents() { + return 0; + } + + /** + * @return The extra parameters for the task set by the client. + */ + public Bundle getExtras() { + return extras; + } + + /** + * If the specified network is unavailable, your task will not be run until + * it is. + * + * @return The network type that this task requires in order to run. See the NETWORK_TYPE_* + * flavours for an explanation of what this value can be. + */ + public int getRequiredNetwork() { + return requiredNetwork; + } + + /** + * If the device is not charging and this is set to true, your task will not be run + * until it is. + * + * @return Whether or not this task depends on the device being connected to power in order to + * execute. + */ + public boolean getRequiresCharging() { + return requiresCharging; + } + + /** + * @return The {@link com.google.android.gms.gcm.GcmTaskService} component that this task + * will execute on. + */ + public String getServiceName() { + return serviceName; + } + + /** + * @return The String identifier for this task, that is returned to + * {@link com.google.android.gms.gcm.GcmTaskService#onRunTask(com.google.android.gms.gcm.TaskParams)} + * when this task executes. + */ + public String getTag() { + return tag; + } + + /** + * @return Whether this task will be persisted across devices restarts or Google Play Services + * crashes. + */ + public boolean isPersisted() { + return persisted; + } + + /** + * @return Whether or not this task will update a pre-existing task in the scheduler queue. + */ + public boolean isUpdateCurrent() { + return updateCurrent; + } + + public void toBundle(Bundle bundle) { + bundle.putString("tag", this.tag); + bundle.putBoolean("update_current", this.updateCurrent); + bundle.putBoolean("persisted", this.persisted); + bundle.putString("service", this.serviceName); + bundle.putInt("requiredNetwork", this.requiredNetwork); + bundle.putBoolean("requiresCharging", this.requiresCharging); + bundle.putBundle("retryStrategy", null); // TODO + bundle.putBundle("extras", this.extras); + } + + public void writeToParcel(Parcel parcel, int i) { + parcel.writeString(serviceName); + parcel.writeString(tag); + parcel.writeInt(updateCurrent ? 1 : 0); + parcel.writeInt(persisted ? 1 : 0); + } + + + /** + *Builder object to construct these tasks before sending them to the network manager. Use + * either {@link com.google.android.gms.gcm.PeriodicTask.Builder} or + * {@link com.google.android.gms.gcm.Task.Builder}
+ */ + public abstract static class Builder { + protected Bundle extras; + protected String gcmTaskService; + protected boolean isPersisted; + protected int requiredNetworkState; + protected boolean requiresCharging; + protected String tag; + protected boolean updateCurrent; + + public Builder() { + throw new UnsupportedOperationException(); + } + + public abstract Task build(); + + /** + * Optional setter for specifying any extra parameters necessary for the task. + */ + public abstract Task.Builder setExtras(Bundle extras); + + /** + * Optional setter to specify whether this task should be persisted across reboots. This + * defaults to true for periodic tasks, and is not supported for one-off tasks. + * + * Callers must hold the permission + * android.Manifest.permission.RECEIVE_BOOT_COMPLETED, otherwise this setter is + * ignored. + * + * @param isPersisted True if this task should be persisted across device reboots. + */ + public abstract Task.Builder setPersisted(boolean isPersisted); + + /** + * Set the network state your task requires to run. If the specified network is + * unavailable your task will not be executed until it becomes available. + * + * The default for either a periodic or one-off task is + * {@link com.google.android.gms.gcm.Task#NETWORK_STATE_CONNECTED}. Note that changing this to + * {@link com.google.android.gms.gcm.Task#NETWORK_STATE_ANY} means there is no guarantee that data will be available + * when your task executes. + * + * In addition, the only guarantee for connectivity is at the moment of execution - it is + * possible for the device to lose data shortly after your task begins executing. + */ + public abstract Task.Builder setRequiredNetwork(int requiredNetworkState); + + /** + * Set whether your task requires that the device be connected to power in order to + * execute. + * + * Use this to defer nonessential operations whenever possible. Note that if you set this + * field and the device is not connected to power your task will not run + * until the device is plugged in. + * + * One way to deal with your task not executing until the constraint is met is to schedule + * another task without the constraints that is subject to some deadline that you can abide. + * This task would be responsible for executing your fallback logic. + */ + public abstract Task.Builder setRequiresCharging(boolean requiresCharging); + + /** + * Set whichever {@link com.google.android.gms.gcm.GcmTaskService} you implement to execute the logic for this task. + * + * @param gcmTaskService Endpoint against which you're scheduling this task. + */ + public abstract Task.Builder setService(Class extends GcmTaskService> gcmTaskService); + + /** + * Mandatory setter for specifying the tag identifer for this task. This tag will be + * returned at execution time to your endpoint. See + * {@link com.google.android.gms.gcm.GcmTaskService#onRunTask(com.google.android.gms.gcm.TaskParams)} + * + * Maximum tag length is 100. + * + * @param tag String identifier for this task. Consecutive schedule calls for the same tag + * will update any preexisting task with the same tag. + */ + public abstract Task.Builder setTag(String tag); + + /** + * Optional setter to specify whether this task should override any preexisting tasks with + * the same tag. This defaults to false, which means that a new task will not override an + * existing one. + * + * @param updateCurrent True to update the current task with the parameters of the new. + * Default false. + */ + public abstract Task.Builder setUpdateCurrent(boolean updateCurrent); + } +} \ No newline at end of file diff --git a/play-services-gcm/src/main/java/com/google/android/gms/gcm/TaskParams.java b/play-services-gcm/src/main/java/com/google/android/gms/gcm/TaskParams.java new file mode 100644 index 00000000..0fb0940a --- /dev/null +++ b/play-services-gcm/src/main/java/com/google/android/gms/gcm/TaskParams.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2013-2017 microG Project Team + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.gms.gcm; + +import android.os.Bundle; + +import org.microg.gms.common.PublicApi; + +/** + * Container of parameters handed off to the client app in + * {@link com.google.android.gms.gcm.GcmTaskService#onRunTask(com.google.android.gms.gcm.TaskParams)}. + */ +@PublicApi +public class TaskParams { + private final String tag; + private final Bundle extras; + + public TaskParams(String tag) { + this(tag, null); + } + + public TaskParams(String tag, Bundle extras) { + this.tag = tag; + this.extras = extras; + } + + public Bundle getExtras() { + return extras; + } + + public String getTag() { + return tag; + } + +} diff --git a/play-services-gcm/src/main/java/org/microg/gms/gcm/CloudMessagingRpc.java b/play-services-gcm/src/main/java/org/microg/gms/gcm/CloudMessagingRpc.java new file mode 100644 index 00000000..4496af3e --- /dev/null +++ b/play-services-gcm/src/main/java/org/microg/gms/gcm/CloudMessagingRpc.java @@ -0,0 +1,165 @@ +/* + * Copyright (C) 2013-2017 microG Project Team + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.microg.gms.gcm; + +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.Messenger; +import android.util.Log; + +import com.google.android.gms.iid.InstanceID; + +import java.io.IOException; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import static android.content.pm.PackageManager.PERMISSION_GRANTED; +import static com.google.android.gms.gcm.GoogleCloudMessaging.ERROR_SERVICE_NOT_AVAILABLE; +import static org.microg.gms.common.Constants.GMS_PACKAGE_NAME; +import static org.microg.gms.common.Constants.GSF_PACKAGE_NAME; +import static org.microg.gms.gcm.GcmConstants.ACTION_C2DM_REGISTER; +import static org.microg.gms.gcm.GcmConstants.ACTION_C2DM_REGISTRATION; +import static org.microg.gms.gcm.GcmConstants.ACTION_GCM_SEND; +import static org.microg.gms.gcm.GcmConstants.EXTRA_APP; +import static org.microg.gms.gcm.GcmConstants.EXTRA_ERROR; +import static org.microg.gms.gcm.GcmConstants.EXTRA_MESSAGE_ID; +import static org.microg.gms.gcm.GcmConstants.EXTRA_MESSENGER; +import static org.microg.gms.gcm.GcmConstants.EXTRA_REGISTRATION_ID; +import static org.microg.gms.gcm.GcmConstants.EXTRA_UNREGISTERED; +import static org.microg.gms.gcm.GcmConstants.PERMISSION_GTALK; +import static org.microg.gms.gcm.GcmConstants.PERMISSION_RECEIVE; + +public class CloudMessagingRpc { + private static final AtomicInteger messageIdCounter = new AtomicInteger(1); + private static String gcmPackageName; + + private final BlockingQueue+ *+ * Do not export this service. Instead, keep it private to prevent other apps + * accessing your service. + */ +public class InstanceIDListenerService extends Service { + + private BroadcastReceiver registrationReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + handleIntent(intent); + stop(); + } + }; + private MessengerCompat messengerCompat = new MessengerCompat(new Handler(Looper.getMainLooper()) { + @Override + public void handleMessage(Message msg) { + handleIntent((Intent) msg.obj); + } + }); + + private int counter = 0; + private int startId = -1; + + private void handleIntent(Intent intent) { + // TODO + } + + public IBinder onBind(Intent intent) { + if (intent != null && ACTION_INSTANCE_ID.equals(intent.getAction())) { + return messengerCompat.getBinder(); + } + return null; + } + + public void onCreate() { + IntentFilter filter = new IntentFilter(ACTION_C2DM_REGISTRATION); + filter.addCategory(getPackageName()); + registerReceiver(registrationReceiver, filter); + } + + public void onDestroy() { + unregisterReceiver(registrationReceiver); + } + + public int onStartCommand(Intent intent, int flags, int startId) { + synchronized (this) { + this.counter++; + if (startId > this.startId) this.startId = startId; + } + try { + if (intent != null) { + if (ACTION_INSTANCE_ID.equals(intent.getAction()) && intent.hasExtra(EXTRA_GSF_INTENT)) { + startService((Intent) intent.getParcelableExtra(EXTRA_GSF_INTENT)); + return START_STICKY; + } + + handleIntent(intent); + + if (intent.hasExtra(EXTRA_FROM)) + WakefulBroadcastReceiver.completeWakefulIntent(intent); + } + } finally { + stop(); + } + return START_NOT_STICKY; + } + + /** + * Called when the system determines that the tokens need to be refreshed. The application + * should call getToken() and send the tokens to all application servers. + * + * This will not be called very frequently, it is needed for key rotation and to handle special + * cases. + * + * The system will throttle the refresh event across all devices to avoid overloading + * application servers with token updates. + */ + public void onTokenRefresh() { + // To be overwritten + } + + private void stop() { + synchronized (this) { + counter--; + if (counter <= 0) { + stopSelf(startId); + } + } + } + +} \ No newline at end of file diff --git a/play-services-iid/src/main/java/org/microg/gms/iid/InstanceIdRpc.java b/play-services-iid/src/main/java/org/microg/gms/iid/InstanceIdRpc.java new file mode 100644 index 00000000..77f65f5e --- /dev/null +++ b/play-services-iid/src/main/java/org/microg/gms/iid/InstanceIdRpc.java @@ -0,0 +1,423 @@ +/* + * Copyright (C) 2013-2017 microG Project Team + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.microg.gms.iid; + +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.os.Build; +import android.os.Bundle; +import android.os.ConditionVariable; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.Messenger; +import android.os.Parcelable; +import android.os.RemoteException; +import android.os.SystemClock; +import android.text.TextUtils; +import android.util.Base64; +import android.util.Log; + +import com.google.android.gms.iid.InstanceID; +import com.google.android.gms.iid.MessengerCompat; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.Signature; +import java.security.interfaces.RSAPrivateKey; +import java.util.HashMap; +import java.util.Map; +import java.util.Random; + +import static android.content.pm.PackageManager.PERMISSION_GRANTED; +import static com.google.android.gms.iid.InstanceID.ERROR_BACKOFF; +import static com.google.android.gms.iid.InstanceID.ERROR_MISSING_INSTANCEID_SERVICE; +import static com.google.android.gms.iid.InstanceID.ERROR_SERVICE_NOT_AVAILABLE; +import static com.google.android.gms.iid.InstanceID.ERROR_TIMEOUT; +import static org.microg.gms.common.Constants.GMS_PACKAGE_NAME; +import static org.microg.gms.common.Constants.GSF_PACKAGE_NAME; +import static org.microg.gms.common.Constants.MAX_REFERENCE_VERSION; +import static org.microg.gms.gcm.GcmConstants.ACTION_C2DM_REGISTER; +import static org.microg.gms.gcm.GcmConstants.ACTION_C2DM_REGISTRATION; +import static org.microg.gms.gcm.GcmConstants.ACTION_INSTANCE_ID; +import static org.microg.gms.gcm.GcmConstants.EXTRA_APP; +import static org.microg.gms.gcm.GcmConstants.EXTRA_APP_ID; +import static org.microg.gms.gcm.GcmConstants.EXTRA_APP_VERSION_CODE; +import static org.microg.gms.gcm.GcmConstants.EXTRA_APP_VERSION_NAME; +import static org.microg.gms.gcm.GcmConstants.EXTRA_CLIENT_VERSION; +import static org.microg.gms.gcm.GcmConstants.EXTRA_ERROR; +import static org.microg.gms.gcm.GcmConstants.EXTRA_GMS_VERSION; +import static org.microg.gms.gcm.GcmConstants.EXTRA_GSF_INTENT; +import static org.microg.gms.gcm.GcmConstants.EXTRA_IS_MESSENGER2; +import static org.microg.gms.gcm.GcmConstants.EXTRA_KID; +import static org.microg.gms.gcm.GcmConstants.EXTRA_MESSENGER; +import static org.microg.gms.gcm.GcmConstants.EXTRA_OS_VERSION; +import static org.microg.gms.gcm.GcmConstants.EXTRA_PUBLIC_KEY; +import static org.microg.gms.gcm.GcmConstants.EXTRA_REGISTRATION_ID; +import static org.microg.gms.gcm.GcmConstants.EXTRA_SIGNATURE; +import static org.microg.gms.gcm.GcmConstants.EXTRA_UNREGISTERED; +import static org.microg.gms.gcm.GcmConstants.EXTRA_USE_GSF; +import static org.microg.gms.gcm.GcmConstants.PERMISSION_RECEIVE; + +public class InstanceIdRpc { + private static final String TAG = "InstanceID/Rpc"; + + private static final int BLOCKING_WAIT_TIME = 30000; + + private static String iidPackageName; + private static int lastRequestId; + private static int retryCount; + private static Map+ * + * + *+ *
+ * The methods must be used in conjunction with a GoogleApiClient. E.g. + *
+ * new GoogleApiClient.Builder(context) + * .addApi(ActivityRecognition.API) + * .addConnectionCallbacks(this) + * .addOnConnectionFailedListener(this) + * .build() + *+ */ +public interface ActivityRecognitionApi { + /** + * Removes all activity updates for the specified PendingIntent. + *
+ * Calling this function requires the com.google.android.gms.permission.ACTIVITY_RECOGNITION
+ * permission.
+ *
+ * @param client An existing GoogleApiClient. It must be connected at the time of this
+ * call, which is normally achieved by calling {@link GoogleApiClient#connect()}
+ * and waiting for {@link ConnectionCallbacks#onConnected(Bundle)} to be
+ * called.
+ * @param callbackIntent the PendingIntent that was used in {@code #requestActivityUpdates(GoogleApiClient, long, PendingIntent)}
+ * or is equal as defined by {@link Object#equals(Object)}.
+ * @return a PendingResult for the call, check {@link Status#isSuccess()} to determine if it
+ * was successful.
+ */
+ PendingResult
+ * The activities are detected by periodically waking up the device and reading short bursts of
+ * sensor data. It only makes use of low power sensors in order to keep the power usage to a
+ * minimum. For example, it can detect if the user is currently on foot, in a car, on a bicycle
+ * or still. See {@link DetectedActivity} for more details.
+ *
+ * The activity detection update interval can be controlled with the detectionIntervalMillis
+ * parameter. Larger values will result in fewer activity detections while improving battery
+ * life. Smaller values will result in more frequent activity detections but will consume more
+ * power since the device must be woken up more frequently. {@code Long.MAX_VALUE} means it only
+ * monitors the results requested by other clients without consuming additional power.
+ *
+ * Activities may be received more frequently than the detectionIntervalMillis parameter if
+ * another application has also requested activity updates at a faster rate. It may also receive
+ * updates faster when the activity detection service receives a signal that the current
+ * activity may change, such as if the device has been still for a long period of time and is
+ * then unplugged from a phone charger.
+ *
+ * Activities may arrive several seconds after the requested detectionIntervalMillis if the
+ * activity detection service requires more samples to make a more accurate prediction.
+ *
+ * To conserve battery, activity reporting may stop when the device is 'STILL' for an extended
+ * period of time. It will resume once the device moves again. This only happens on devices that
+ * support the Sensor.TYPE_SIGNIFICANT_MOTION hardware.
+ *
+ * Beginning in API 21, activities may be received less frequently than the
+ * detectionIntervalMillis parameter if the device is in power save mode and the screen is off.
+ *
+ * A common use case is that an application wants to monitor activities in the background and
+ * perform an action when a specific activity is detected. To do this without needing a service
+ * that is always on in the background consuming resources, detected activities are delivered
+ * via an intent. The application specifies a PendingIntent callback (typically an
+ * IntentService) which will be called with an intent when activities are detected. The intent
+ * recipient can extract the {@link ActivityRecognitionResult} using {@link ActivityRecognitionResult#extractResult(android.content.Intent)}.
+ * See the documentation of {@link PendingIntent} for more details.
+ *
+ * Any requests previously registered with {@link #requestActivityUpdates(GoogleApiClient, long, PendingIntent)}
+ * that have the same PendingIntent (as defined by {@link Object#equals(Object)}) will be
+ * replaced by this request.
+ *
+ * Calling this function requires the com.google.android.gms.permission.ACTIVITY_RECOGNITION
+ * permission.
+ *
+ * @param client An existing GoogleApiClient. It must be connected at the time
+ * of this call, which is normally achieved by calling {@link GoogleApiClient#connect()}
+ * and waiting for {@link ConnectionCallbacks#onConnected(Bundle)}
+ * to be called.
+ * @param detectionIntervalMillis the desired time between activity detections. Larger values
+ * will result in fewer activity detections while improving
+ * battery life. A value of 0 will result in activity detections
+ * at the fastest possible rate.
+ * @param callbackIntent a PendingIntent to be sent for each activity detection.
+ * @return a PendingResult for the call, check {@link Status#isSuccess()} to determine if it
+ * was successful.
+ */
+ PendingResult
+ * The methods must be used in conjunction with a GoogleApiClient. E.g.
+ *
+ * This API makes it easy for an app to ensure that the device's system settings are properly
+ * configured for the app's location needs.
+ */
+public interface SettingsApi {
+ /**
+ * Checks if the relevant system settings are enabled on the device to carry out the desired
+ * location requests.
+ *
+ * @param client an existing GoogleApiClient. It does not need to be connected
+ * at the time of this call, but the result will be delayed until
+ * the connection is complete.
+ * @param locationSettingsRequest an object that contains all the location requirements that the
+ * client is interested in.
+ * @return result containing the status of the request.
+ */
+ PendingResult
+ * new GoogleApiClient.Builder(context)
+ * .addApi(LocationServices.API)
+ * .addConnectionCallbacks(this)
+ * .addOnConnectionFailedListener(this)
+ * .build()
+ *
+ */
+public interface GeofencingApi {
+ PendingResulttask.continueWith(new Continuation
+ *
+ * To suppress all failures guard any calls to {@link Task#getResult()} with {@link Task#isSuccessful()}:
+ * task.continueWith(new Continuation
+ *
+ * @param task the completed Task. Never null
+ * @throws Exception if the result couldn't be produced
+ */
+ TContinuationResult then(Taskwear://* /
+ * Listener events will be called on the main thread, or the handler specified on {@code client}
+ * when it was built (using {@link Builder#setHandler(Handler)}).
+ *
+ * Callers wishing to be notified of events in the background should use WearableListenerService.
+ */
+ PendingResult