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 @@ + + + + + + + + + + + diff --git a/play-services-base/src/main/java/com/google/android/gms/common/GoogleApiAvailability.java b/play-services-base/src/main/java/com/google/android/gms/common/GoogleApiAvailability.java new file mode 100644 index 00000000..0439619e --- /dev/null +++ b/play-services-base/src/main/java/com/google/android/gms/common/GoogleApiAvailability.java @@ -0,0 +1,290 @@ +/* + * 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.common; + +import android.app.Activity; +import android.app.Dialog; +import android.app.PendingIntent; +import android.content.Context; +import android.content.DialogInterface; +import android.os.Bundle; +import androidx.fragment.app.FragmentActivity; +import android.util.Log; + +import com.google.android.gms.common.api.GoogleApiClient.OnConnectionFailedListener; +import com.google.android.gms.tasks.Task; +import com.google.android.gms.tasks.Tasks; + +import org.microg.gms.common.Constants; +import org.microg.gms.common.PublicApi; + +import static com.google.android.gms.common.ConnectionResult.INTERNAL_ERROR; +import static com.google.android.gms.common.ConnectionResult.INVALID_ACCOUNT; +import static com.google.android.gms.common.ConnectionResult.NETWORK_ERROR; +import static com.google.android.gms.common.ConnectionResult.RESOLUTION_REQUIRED; +import static com.google.android.gms.common.ConnectionResult.SERVICE_DISABLED; +import static com.google.android.gms.common.ConnectionResult.SERVICE_INVALID; +import static com.google.android.gms.common.ConnectionResult.SERVICE_MISSING; +import static com.google.android.gms.common.ConnectionResult.SERVICE_VERSION_UPDATE_REQUIRED; +import static com.google.android.gms.common.ConnectionResult.SIGN_IN_REQUIRED; +import static com.google.android.gms.common.ConnectionResult.SUCCESS; + +@PublicApi +public class GoogleApiAvailability { + private static final String TAG = "GmsApiAvailability"; + + /** + * Package name for Google Play services. + */ + public static final String GOOGLE_PLAY_SERVICES_PACKAGE = Constants.GMS_PACKAGE_NAME; + + /** + * Google Play services client library version (declared in library's AndroidManifest.xml android:versionCode). + */ + public static final int GOOGLE_PLAY_SERVICES_VERSION_CODE = Constants.MAX_REFERENCE_VERSION; + + private static GoogleApiAvailability instance; + + private GoogleApiAvailability() { + } + + /** + * Returns the singleton instance of GoogleApiAvailability. + */ + public static GoogleApiAvailability getInstance() { + if (instance == null) { + synchronized (GoogleApiAvailability.class) { + if (instance == null) { + instance = new GoogleApiAvailability(); + } + } + } + return instance; + } + + + /** + * Returns a dialog to address the provided errorCode. The returned dialog displays a localized + * message about the error and upon user confirmation (by tapping on dialog) will direct them + * to the Play Store if Google Play services is out of date or missing, or to system settings + * if Google Play services is disabled on the device. + * + * @param activity parent activity for creating the dialog, also used for identifying language to display dialog in. + * @param errorCode error code returned by {@link #isGooglePlayServicesAvailable(Context)} call. + * If errorCode is {@link ConnectionResult#SUCCESS} then null is returned. + * @param requestCode The requestCode given when calling startActivityForResult. + */ + public Dialog getErrorDialog(Activity activity, int errorCode, int requestCode) { + return getErrorDialog(activity, errorCode, requestCode, null); + } + + /** + * Returns a dialog to address the provided errorCode. The returned dialog displays a localized + * message about the error and upon user confirmation (by tapping on dialog) will direct them + * to the Play Store if Google Play services is out of date or missing, or to system settings + * if Google Play services is disabled on the device. + * + * @param activity parent activity for creating the dialog, also used for identifying language to display dialog in. + * @param errorCode error code returned by {@link #isGooglePlayServicesAvailable(Context)} call. + * If errorCode is {@link ConnectionResult#SUCCESS} then null is returned. + * @param requestCode The requestCode given when calling startActivityForResult. + * @param cancelListener The {@link DialogInterface.OnCancelListener} to invoke if the dialog is canceled. + */ + public Dialog getErrorDialog(Activity activity, int errorCode, int requestCode, DialogInterface.OnCancelListener cancelListener) { + // TODO + return null; + } + + /** + * Returns a PendingIntent to address the provided connection failure. + *

+ * If {@link ConnectionResult#hasResolution()} is true, then {@link ConnectionResult#getResolution()} + * will be returned. Otherwise, the returned PendingIntent will direct the user to either the + * Play Store if Google Play services is out of date or missing, or system settings if Google + * Play services is disabled on the device. + * + * @param context parent context for creating the PendingIntent. + * @param result the connection failure. If successful or the error is not resolvable by the user, null is returned. + */ + public PendingIntent getErrorResolutionPendingIntent(Context context, ConnectionResult result) { + if (result.hasResolution()) { + return result.getResolution(); + } + return getErrorResolutionPendingIntent(context, result.getErrorCode(), 0); + } + + /** + * Returns a PendingIntent to address the provided errorCode. It will direct the user to either + * the Play Store if Google Play services is out of date or missing, or system settings if + * Google Play services is disabled on the device. + * + * @param context parent context for creating the PendingIntent. + * @param errorCode error code returned by {@link #isGooglePlayServicesAvailable(Context)} call. + * If errorCode is {@link ConnectionResult#SUCCESS} then null is returned. + * @param requestCode The requestCode given when calling startActivityForResult. + */ + public PendingIntent getErrorResolutionPendingIntent(Context context, int errorCode, int requestCode) { + // TODO + return null; + } + + /** + * Returns a human-readable string of the error code returned from {@link #isGooglePlayServicesAvailable(Context)}. + */ + public final String getErrorString(int errorCode) { + return ConnectionResult.getStatusString(errorCode); + } + + /** + * Verifies that Google Play services is installed and enabled on this device, and that the + * version installed on this device is no older than the one required by this client. + * + * @return status code indicating whether there was an error. Can be one of following in + * {@link ConnectionResult}: SUCCESS, SERVICE_MISSING, SERVICE_UPDATING, + * SERVICE_VERSION_UPDATE_REQUIRED, SERVICE_DISABLED, SERVICE_INVALID + */ + public int isGooglePlayServicesAvailable(Context context) { + Log.d(TAG, "As we can't know right now if the later desired feature is available, " + + "we just pretend it to be."); + return SUCCESS; + } + + /** + * Determines whether an error can be resolved via user action. If true, proceed by calling + * {@link #getErrorDialog(Activity, int, int)} and showing the dialog. + * + * @param errorCode error code returned by {@link #isGooglePlayServicesAvailable(Context)}, or + * returned to your application via {@link OnConnectionFailedListener#onConnectionFailed(ConnectionResult)} + * @return true if the error is resolvable with {@link #getErrorDialog(Activity, int, int)} + */ + public final boolean isUserResolvableError(int errorCode) { + switch (errorCode) { + case SERVICE_MISSING: + case SERVICE_VERSION_UPDATE_REQUIRED: + case SERVICE_DISABLED: + case SERVICE_INVALID: + return true; + case SIGN_IN_REQUIRED: + case INVALID_ACCOUNT: + case RESOLUTION_REQUIRED: + case NETWORK_ERROR: + case INTERNAL_ERROR: + default: + return false; + } + } + + /** + * Attempts to make Google Play services available on this device. If Play Services is already + * available, the returned {@link Task} may complete immediately. + *

+ * If it is necessary to display UI in order to complete this request (e.g. sending the user + * to the Google Play store) the passed {@link Activity} will be used to display this UI. + *

+ * It is recommended to call this method from {@link Activity#onCreate(Bundle)}. + * If the passed {@link Activity} completes before the returned {@link Task} completes, the + * Task will fail with a {@link java.util.concurrent.CancellationException}. + *

+ * This method must be called from the main thread. + * + * @return A {@link Task}. If this Task completes without throwing an exception, Play Services + * is available on this device. + */ + public Task makeGooglePlayServicesAvailable(Activity activity) { + int status = isGooglePlayServicesAvailable(activity); + if (status == SUCCESS) { + return Tasks.forResult(null); + } + // TODO + return Tasks.forResult(null); + } + + /** + * Displays a DialogFragment for an error code returned by {@link #isGooglePlayServicesAvailable(Context)}. + * + * @param activity parent activity for creating the dialog, also used for identifying language to display dialog in. + * @param errorCode error code returned by {@link #isGooglePlayServicesAvailable(Context)} call. + * If errorCode is {@link ConnectionResult#SUCCESS} then null is returned. + * @param requestCode The requestCode given when calling startActivityForResult. + * @return true if the dialog is shown, false otherwise + * @throws RuntimeException if API level is below 11 and activity is not a {@link FragmentActivity}. + * @see ErrorDialogFragment + * @see SupportErrorDialogFragmet + */ + public boolean showErrorDialogFragment(Activity activity, int errorCode, int requestCode) { + return showErrorDialogFragment(activity, errorCode, requestCode, null); + } + + /** + * Displays a DialogFragment for an error code returned by {@link #isGooglePlayServicesAvailable(Context)}. + * + * @param activity parent activity for creating the dialog, also used for identifying language to display dialog in. + * @param errorCode error code returned by {@link #isGooglePlayServicesAvailable(Context)} call. + * If errorCode is {@link ConnectionResult#SUCCESS} then null is returned. + * @param requestCode The requestCode given when calling startActivityForResult. + * @param cancelListener The {@link DialogInterface.OnCancelListener} to invoke if the dialog is canceled. + * @return true if the dialog is shown, false otherwise + * @throws RuntimeException if API level is below 11 and activity is not a {@link FragmentActivity}. + * @see ErrorDialogFragment + * @see SupportErrorDialogFragmet + */ + public boolean showErrorDialogFragment(Activity activity, int errorCode, int requestCode, DialogInterface.OnCancelListener cancelListener) { + Dialog dialog = getErrorDialog(activity, errorCode, requestCode, cancelListener); + if (dialog == null) { + return false; + } else { + // TODO + return false; + } + } + + /** + * Displays a notification for an error code returned from + * {@link #isGooglePlayServicesAvailable(Context)}, if it is resolvable by the user. + *

+ * This method is similar to {@link #getErrorDialog(int, android.app.Activity, int)}, but is + * provided for background tasks that cannot or should not display dialogs. + * + * @param errorCode error code returned by {@link #isGooglePlayServicesAvailable(Context)} call. + * If errorCode is {@link ConnectionResult#SUCCESS} then null is returned. + * @param context used for identifying language to display dialog in as well as accessing the + * {@link android.app.NotificationManager}. + */ + public void showErrorNotification(Context context, int errorCode) { + if (errorCode == RESOLUTION_REQUIRED) { + Log.e(TAG, "showErrorNotification(context, errorCode) is called for RESOLUTION_REQUIRED when showErrorNotification(context, result) should be called"); + } + + if (isUserResolvableError(errorCode)) { + GooglePlayServicesUtil.showErrorNotification(errorCode, context); + } + } + + /** + * Displays a notification for a connection failure, if it is resolvable by the user. + * + * @param context The calling context used to display the notification. + * @param result The connection failure. If successful or the error is not resolvable by the + * user, no notification is shown. + */ + public void showErrorNotification(Context context, ConnectionResult result) { + PendingIntent pendingIntent = getErrorResolutionPendingIntent(context, result); + if (pendingIntent != null) { + // TODO + } + } +} diff --git a/play-services-base/src/main/java/com/google/android/gms/common/GooglePlayServicesClient.java b/play-services-base/src/main/java/com/google/android/gms/common/GooglePlayServicesClient.java new file mode 100644 index 00000000..a5642fdd --- /dev/null +++ b/play-services-base/src/main/java/com/google/android/gms/common/GooglePlayServicesClient.java @@ -0,0 +1,56 @@ +/* + * 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.common; + +import android.os.Bundle; + +@Deprecated +public interface GooglePlayServicesClient { + void connect(); + + void disconnect(); + + boolean isConnected(); + + boolean isConnecting(); + + void registerConnectionCallbacks(ConnectionCallbacks listener); + + boolean isConnectionCallbacksRegistered(ConnectionCallbacks listener); + + void unregisterConnectionCallbacks(ConnectionCallbacks listener); + + void registerConnectionFailedListener(OnConnectionFailedListener listener); + + boolean isConnectionFailedListenerRegistered(OnConnectionFailedListener listener); + + void unregisterConnectionFailedListener(OnConnectionFailedListener listener); + + @Deprecated + interface OnConnectionFailedListener { + + void onConnectionFailed(ConnectionResult result); + } + + @Deprecated + interface ConnectionCallbacks { + + void onConnected(Bundle connectionHint); + + void onDisconnected(); + } +} diff --git a/play-services-base/src/main/java/com/google/android/gms/common/GooglePlayServicesUtil.java b/play-services-base/src/main/java/com/google/android/gms/common/GooglePlayServicesUtil.java new file mode 100644 index 00000000..5fb995b5 --- /dev/null +++ b/play-services-base/src/main/java/com/google/android/gms/common/GooglePlayServicesUtil.java @@ -0,0 +1,235 @@ +/* + * 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.common; + +import android.app.Activity; +import android.app.Dialog; +import android.app.Fragment; +import android.app.PendingIntent; +import android.content.Context; +import android.content.DialogInterface; +import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.util.Log; + +import org.microg.gms.common.Constants; +import org.microg.gms.common.PublicApi; + +/** + * Utility class for verifying that the Google Play services APK is available and up-to-date on + * this device. The same checks are performed if one uses {@link AdvertisingIdClient} or + * {@link GoogleAuthUtil} to connect to the service. + *

+ * TODO: methods :) + */ +@PublicApi +public class GooglePlayServicesUtil { + private static final String TAG = "GooglePlayServicesUtil"; + + public static final String GMS_ERROR_DIALOG = "GooglePlayServicesErrorDialog"; + + /** + * Package name for Google Play services. + */ + @Deprecated + public static final String GOOGLE_PLAY_SERVICES_PACKAGE = Constants.GMS_PACKAGE_NAME; + + /** + * Google Play services client library version (declared in library's AndroidManifest.xml android:versionCode). + */ + @Deprecated + public static final int GOOGLE_PLAY_SERVICES_VERSION_CODE = Constants.MAX_REFERENCE_VERSION; + + /** + * Package name for Google Play Store. + */ + public static final String GOOGLE_PLAY_STORE_PACKAGE = "com.android.vending"; + + /** + * Returns a dialog to address the provided errorCode. The returned dialog displays a localized + * message about the error and upon user confirmation (by tapping on dialog) will direct them + * to the Play Store if Google Play services is out of date or missing, or to system settings + * if Google Play services is disabled on the device. + * + * @param errorCode error code returned by {@link #isGooglePlayServicesAvailable(Context)} call. + * If errorCode is {@link ConnectionResult#SUCCESS} then null is returned. + * @param activity parent activity for creating the dialog, also used for identifying + * language to display dialog in. + * @param requestCode The requestCode given when calling startActivityForResult. + */ + @Deprecated + public static Dialog getErrorDialog(int errorCode, Activity activity, int requestCode) { + return getErrorDialog(errorCode, activity, requestCode, null); + } + + /** + * Returns a dialog to address the provided errorCode. The returned dialog displays a localized + * message about the error and upon user confirmation (by tapping on dialog) will direct them + * to the Play Store if Google Play services is out of date or missing, or to system settings + * if Google Play services is disabled on the device. + * + * @param errorCode error code returned by {@link #isGooglePlayServicesAvailable(Context)} call. + * If errorCode is {@link ConnectionResult#SUCCESS} then null is returned. + * @param activity parent activity for creating the dialog, also used for identifying + * language to display dialog in. + * @param requestCode The requestCode given when calling startActivityForResult. + * @param cancelListener The {@link DialogInterface.OnCancelListener} to invoke if the dialog + * is canceled. + */ + @Deprecated + public static Dialog getErrorDialog(int errorCode, Activity activity, int requestCode, DialogInterface.OnCancelListener cancelListener) { + return GoogleApiAvailability.getInstance().getErrorDialog(activity, errorCode, requestCode, cancelListener); + } + + /** + * Returns a PendingIntent to address the provided errorCode. It will direct them to one of the + * following places to either the Play Store if Google Play services is out of date or missing, + * or system settings if Google Play services is disabled on the device. + * + * @param errorCode error code returned by {@link #isGooglePlayServicesAvailable(Context)} call. + * If errorCode is {@link ConnectionResult#SUCCESS} then null is returned. + * @param activity parent context for creating the PendingIntent. + * @param requestCode The requestCode given when calling startActivityForResult. + */ + @Deprecated + public static PendingIntent getErrorPendingIntent(int errorCode, Activity activity, + int requestCode) { + return null; // TODO + } + + /** + * Returns a human-readable string of the error code returned from {@link #isGooglePlayServicesAvailable(Context)}. + */ + @Deprecated + public static String getErrorString(int errorCode) { + return null; // TODO + } + + /** + * Returns the open source software license information for the Google Play services + * application, or null if Google Play services is not available on this device. + */ + @Deprecated + public static String getOpenSourceSoftwareLicenseInfo(Context context) { + return null; // TODO + } + + /** + * This gets the Context object of the Buddy APK. This loads the Buddy APK code from the Buddy + * APK into memory. This returned context can be used to create classes and obtain resources + * defined in the Buddy APK. + * + * @return The Context object of the Buddy APK or null if the Buddy APK is not installed on the device. + */ + public static Context getRemoteContext(Context context) { + return null; // TODO + } + + /** + * This gets the Resources object of the Buddy APK. + * + * @return The Resources object of the Buddy APK or null if the Buddy APK is not installed on the device. + */ + public static Resources getRemoteResources(Context context) { + return null; // TODO + } + + /** + * Verifies that Google Play services is installed and enabled on this device, and that the + * version installed on this device is no older than the one required by this client. + * + * @return status code indicating whether there was an error. Can be one of following in + * {@link ConnectionResult}: SUCCESS, SERVICE_MISSING, SERVICE_VERSION_UPDATE_REQUIRED, + * SERVICE_DISABLED, SERVICE_INVALID + */ + @Deprecated + public static int isGooglePlayServicesAvailable(Context context) { + Log.d(TAG, "As we can't know right now if the later desired feature is available, " + + "we just pretend it to be."); + return ConnectionResult.SUCCESS; + } + + @Deprecated + public static boolean isGoogleSignedUid(PackageManager packageManager, int uid) { + return false; // TODO + } + + /** + * Determines whether an error is user-recoverable. If true, proceed by calling + * {@link #getErrorDialog(int, Activity, int)} and showing the dialog. + * + * @param errorCode error code returned by {@link #isGooglePlayServicesAvailable(Context)}, or + * returned to your application via {@link com.google.android.gms.common.api.GoogleApiClient.OnConnectionFailedListener#onConnectionFailed(ConnectionResult)} + * @return true if the error is recoverable with {@link #getErrorDialog(int, Activity, int)} + */ + @Deprecated + public static boolean isUserRecoverableError(int errorCode) { + return false; // TODO + } + + /** + * Display a DialogFragment for an error code returned by {@link #isGooglePlayServicesAvailable(Context)}. + * + * @param errorCode error code returned by {@link #isGooglePlayServicesAvailable(Context)} call. + * If errorCode is {@link ConnectionResult#SUCCESS} then null is returned. + * @param activity parent activity for creating the dialog, also used for identifying + * language to display dialog in. + * @param requestCode The requestCode given when calling startActivityForResult. + * @return true if the dialog is shown, false otherwise + * @throws RuntimeException if API level is below 11 and activity is not a {@link android.support.v4.app.FragmentActivity}. + */ + @Deprecated + public static boolean showErrorDialogFragment(int errorCode, Activity activity, int requestCode) { + return showErrorDialogFragment(errorCode, activity, requestCode, null); + } + + @Deprecated + public static boolean showErrorDialogFragment(int errorCode, Activity activity, Fragment fragment, int requestCode, DialogInterface.OnCancelListener cancelListener) { + return false; // TODO + } + + /** + * @param errorCode error code returned by {@link #isGooglePlayServicesAvailable(Context)} call. + * If errorCode is {@link ConnectionResult#SUCCESS} then null is returned. + * @param activity parent activity for creating the dialog, also used for identifying + * language to display dialog in. + * @param requestCode The requestCode given when calling startActivityForResult. + * @param cancelListener The {@link DialogInterface.OnCancelListener} to invoke if the dialog + * is canceled. + * @return true if the dialog is shown, false otherwise. + * @throws RuntimeException if API level is below 11 and activity is not a {@link android.support.v4.app.FragmentActivity}. + */ + @Deprecated + public static boolean showErrorDialogFragment(int errorCode, Activity activity, int requestCode, DialogInterface.OnCancelListener cancelListener) { + return showErrorDialogFragment(errorCode, activity, null, requestCode, cancelListener); + } + + /** + * Displays a notification relevant to the provided error code. This method is similar to + * {@link #getErrorDialog(int, android.app.Activity, int)}, but is provided for background + * tasks that cannot or shouldn't display dialogs. + * + * @param errorCode error code returned by {@link #isGooglePlayServicesAvailable(Context)} call. + * If errorCode is {@link ConnectionResult#SUCCESS} then null is returned. + * @param context used for identifying language to display dialog in as well as accessing the + * {@link android.app.NotificationManager}. + */ + @Deprecated + public static void showErrorNotification(int errorCode, Context context) { + // TODO + } +} diff --git a/play-services-base/src/main/java/com/google/android/gms/common/api/AccountInfo.java b/play-services-base/src/main/java/com/google/android/gms/common/api/AccountInfo.java new file mode 100644 index 00000000..94c11015 --- /dev/null +++ b/play-services-base/src/main/java/com/google/android/gms/common/api/AccountInfo.java @@ -0,0 +1,23 @@ +/* + * 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.common.api; + +import org.microg.safeparcel.AutoSafeParcelable; + +public class AccountInfo extends AutoSafeParcelable { + public static final Creator CREATOR = new AutoCreator(AccountInfo.class); +} diff --git a/play-services-base/src/main/java/com/google/android/gms/common/api/Api.java b/play-services-base/src/main/java/com/google/android/gms/common/api/Api.java new file mode 100644 index 00000000..610da966 --- /dev/null +++ b/play-services-base/src/main/java/com/google/android/gms/common/api/Api.java @@ -0,0 +1,84 @@ +/* + * 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.common.api; + +import org.microg.gms.common.PublicApi; +import org.microg.gms.common.api.ApiBuilder; + +/** + * Describes a section of the Google Play Services API that should be made available. Instances of + * this should be passed into {@link GoogleApiClient.Builder#addApi(Api)} to enable the appropriate + * parts of Google Play Services. + *

+ * Google APIs are partitioned into sections which allow your application to configure only the + * services it requires. Each Google API provides an API object which can be passed to + * {@link GoogleApiClient.Builder#addApi(Api)} in order to configure and enable that functionality + * in your {@link GoogleApiClient} instance. + *

+ * See {@link GoogleApiClient.Builder} for usage examples. + */ +@PublicApi +public final class Api { + + private final ApiBuilder builder; + + @PublicApi(exclude = true) + public Api(ApiBuilder builder) { + this.builder = builder; + } + + @PublicApi(exclude = true) + public ApiBuilder getBuilder() { + return builder; + } + + /** + * Base interface for API options. These are used to configure specific parameters for + * individual API surfaces. The default implementation has no parameters. + */ + @PublicApi + public interface ApiOptions { + /** + * Base interface for {@link ApiOptions} in {@link Api}s that have options. + */ + @PublicApi + interface HasOptions extends ApiOptions { + } + + /** + * Base interface for {@link ApiOptions} that are not required, don't exist. + */ + @PublicApi + interface NotRequiredOptions extends ApiOptions { + } + + /** + * {@link ApiOptions} implementation for {@link Api}s that do not take any options. + */ + @PublicApi + final class NoOptions implements NotRequiredOptions { + } + + /** + * Base interface for {@link ApiOptions} that are optional. + */ + @PublicApi + interface Optional extends HasOptions, NotRequiredOptions { + } + } + +} diff --git a/play-services-base/src/main/java/com/google/android/gms/common/api/GoogleApiClient.java b/play-services-base/src/main/java/com/google/android/gms/common/api/GoogleApiClient.java new file mode 100644 index 00000000..1cd0525f --- /dev/null +++ b/play-services-base/src/main/java/com/google/android/gms/common/api/GoogleApiClient.java @@ -0,0 +1,495 @@ +/* + * 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.common.api; + +import android.app.Activity; +import android.content.Context; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.view.View; + +import androidx.fragment.app.FragmentActivity; + +import com.google.android.gms.common.ConnectionResult; + +import org.microg.gms.auth.AuthConstants; +import org.microg.gms.common.PublicApi; +import org.microg.gms.common.api.GoogleApiClientImpl; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +/** + * The main entry point for Google Play services integration. + *

+ * GoogleApiClient is used with a variety of static methods. Some of these methods require that + * GoogleApiClient be connected, some will queue up calls before GoogleApiClient is connected; + * check the specific API documentation to determine whether you need to be connected. + *

+ * Before any operation is executed, the GoogleApiClient must be connected using the + * {@link #connect()} method. The client is not considered connected until the + * {@link ConnectionCallbacks#onConnected(Bundle)} callback has been called. + *

+ * When your app is done using this client, call {@link #disconnect()}, even if the async result + * from {@link #connect()} has not yet been delivered. + *

+ * You should instantiate a client object in your Activity's {@link Activity#onCreate(Bundle)} + * method and then call {@link #connect()} in {@link Activity#onStart()} and {@link #disconnect()} + * in {@link Activity#onStop()}, regardless of the state. + */ +@PublicApi +public interface GoogleApiClient { + /** + * Connects the client to Google Play services. Blocks until the connection either succeeds or + * fails. This is not allowed on the UI thread. + * + * @return the result of the connection + */ + ConnectionResult blockingConnect(); + + /** + * Connects the client to Google Play services. Blocks until the connection is set or failed or + * has timed out. This is not allowed on the UI thread. + * + * @param timeout the maximum time to wait + * @param unit the time unit of the {@code timeout} argument + * @return the result of the connection + */ + ConnectionResult blockingConnect(long timeout, TimeUnit unit); + + /** + * Clears the account selected by the user and reconnects the client asking the user to pick an + * account again if {@link Builder#useDefaultAccount()} was set. + * + * @return the pending result is fired once the default account has been cleared, but before + * the client is reconnected - for that {@link ConnectionCallbacks} can be used. + */ + PendingResult clearDefaultAccountAndReconnect(); + + /** + * Connects the client to Google Play services. This method returns immediately, and connects + * to the service in the background. If the connection is successful, + * {@link ConnectionCallbacks#onConnected(Bundle)} is called and enqueued items are executed. + * On a failure, {@link OnConnectionFailedListener#onConnectionFailed(ConnectionResult)} is + * called. + */ + void connect(); + + /** + * Closes the connection to Google Play services. No calls can be made using this client after + * calling this method. Any method calls that haven't executed yet will be canceled. That is + * {@link ResultCallback#onResult(Result)} won't be called, if connection to the service hasn't + * been established yet all calls already made will be canceled. + * + * @see #connect() + */ + void disconnect(); + + /** + * Checks if the client is currently connected to the service, so that requests to other + * methods will succeed. Applications should guard client actions caused by the user with a + * call to this method. + * + * @return {@code true} if the client is connected to the service. + */ + boolean isConnected(); + + /** + * Checks if the client is attempting to connect to the service. + * + * @return {@code true} if the client is attempting to connect to the service. + */ + boolean isConnecting(); + + /** + * Returns {@code true} if the specified listener is currently registered to receive connection + * events. + * + * @param listener The listener to check for. + * @return {@code true} if the specified listener is currently registered to receive connection + * events. + * @see #registerConnectionCallbacks(ConnectionCallbacks) + * @see #unregisterConnectionCallbacks(ConnectionCallbacks) + */ + boolean isConnectionCallbacksRegistered(ConnectionCallbacks listener); + + /** + * Returns {@code true} if the specified listener is currently registered to receive connection + * failed events. + * + * @param listener The listener to check for. + * @return {@code true} if the specified listener is currently registered to receive connection + * failed events. + * @see #registerConnectionFailedListener(OnConnectionFailedListener) + * @see #unregisterConnectionFailedListener(OnConnectionFailedListener) + */ + public boolean isConnectionFailedListenerRegistered(OnConnectionFailedListener listener); + + /** + * Closes the current connection to Google Play services and creates a new connection. + *

+ * This method closes the current connection then returns immediately and reconnects to the + * service in the background. + *

+ * After calling this method, your application will receive + * {@link ConnectionCallbacks#onConnected(Bundle)} if the connection is successful, or + * {@link OnConnectionFailedListener#onConnectionFailed(ConnectionResult)} if the connection + * failed. + * + * @see #connect() + * @see #disconnect() + */ + void reconnect(); + + /** + * Registers a listener to receive connection events from this {@link GoogleApiClient}. If the + * service is already connected, the listener's {@link ConnectionCallbacks#onConnected(Bundle)} + * method will be called immediately. Applications should balance calls to this method with + * calls to {@link #unregisterConnectionCallbacks(ConnectionCallbacks)} to avoid leaking + * resources. + *

+ * If the specified listener is already registered to receive connection events, this method + * will not add a duplicate entry for the same listener, but will still call the listener's + * {@link ConnectionCallbacks#onConnected(Bundle)} method if currently connected. + *

+ * Note that the order of messages received here may not be stable, so clients should not rely + * on the order that multiple listeners receive events in. + * + * @param listener the listener where the results of the asynchronous {@link #connect()} call + * are delivered. + */ + void registerConnectionCallbacks(ConnectionCallbacks listener); + + /** + * Registers a listener to receive connection failed events from this {@link GoogleApiClient}. + * Unlike {@link #registerConnectionCallbacks(ConnectionCallbacks)}, if the service is not + * already connected, the listener's + * {@link OnConnectionFailedListener#onConnectionFailed(ConnectionResult)} method will not be + * called immediately. Applications should balance calls to this method with calls to + * {@link #unregisterConnectionFailedListener(OnConnectionFailedListener)} to avoid leaking + * resources. + *

+ * If the specified listener is already registered to receive connection failed events, this + * method will not add a duplicate entry for the same listener. + *

+ * Note that the order of messages received here may not be stable, so clients should not rely + * on the order that multiple listeners receive events in. + * + * @param listener the listener where the results of the asynchronous {@link #connect()} call + * are delivered. + */ + public void registerConnectionFailedListener(OnConnectionFailedListener listener); + + /** + * Disconnects the client and stops automatic lifecycle management. Use this before creating a + * new client (which might be necessary when switching accounts, changing the set of used APIs + * etc.). + *

+ * This method must be called from the main thread. + * + * @param lifecycleActivity the activity managing the client's lifecycle. + * @throws IllegalStateException if called from outside of the main thread. + * @see Builder#enableAutoManage(FragmentActivity, int, OnConnectionFailedListener) + */ + void stopAutoManager(FragmentActivity lifecycleActivity) throws IllegalStateException; + + /** + * Removes a connection listener from this {@link GoogleApiClient}. Note that removing a + * listener does not generate any callbacks. + *

+ * If the specified listener is not currently registered to receive connection events, this + * method will have no effect. + * + * @param listener the listener to unregister. + */ + void unregisterConnectionCallbacks(ConnectionCallbacks listener); + + /** + * Removes a connection failed listener from the {@link GoogleApiClient}. Note that removing a + * listener does not generate any callbacks. + *

+ * If the specified listener is not currently registered to receive connection failed events, + * this method will have no effect. + * + * @param listener the listener to unregister. + */ + void unregisterConnectionFailedListener(OnConnectionFailedListener listener); + + /** + * Builder to configure a {@link GoogleApiClient}. + */ + @PublicApi + class Builder { + private final Context context; + private final Map apis = new HashMap(); + private final Set connectionCallbacks = new HashSet(); + private final Set connectionFailedListeners = new HashSet(); + private final Set scopes = new HashSet(); + private String accountName; + private int clientId = -1; + private FragmentActivity fragmentActivity; + private Looper looper; + private int gravityForPopups; + private OnConnectionFailedListener unresolvedConnectionFailedListener; + private View viewForPopups; + + /** + * Builder to help construct the {@link GoogleApiClient} object. + * + * @param context The context to use for the connection. + */ + public Builder(Context context) { + this.context = context; + this.looper = context.getMainLooper(); + } + + /** + * Builder to help construct the {@link GoogleApiClient} object. + * + * @param context The context to use for the connection. + * @param connectedListener The listener where the results of the asynchronous + * {@link #connect()} call are delivered. + * @param connectionFailedListener The listener which will be notified if the connection + * attempt fails. + */ + public Builder(Context context, ConnectionCallbacks connectedListener, + OnConnectionFailedListener connectionFailedListener) { + this(context); + addConnectionCallbacks(connectedListener); + addOnConnectionFailedListener(connectionFailedListener); + } + + /** + * Specify which Apis are requested by your app. See {@link Api} for more information. + * + * @param api The Api requested by your app. + * @param options Any additional parameters required for the specific AP + * @see Api + */ + public Builder addApi(Api api, O options) { + apis.put(api, options); + return this; + } + + /** + * Specify which Apis are requested by your app. See {@link Api} for more information. + * + * @param api The Api requested by your app. + * @see Api + */ + public Builder addApi(Api api) { + apis.put(api, null); + return this; + } + + /** + * Registers a listener to receive connection events from this {@link GoogleApiClient}. + * Applications should balance calls to this method with calls to + * {@link #unregisterConnectionCallbacks(ConnectionCallbacks)} to avoid + * leaking resources. + *

+ * If the specified listener is already registered to receive connection events, this + * method will not add a duplicate entry for the same listener. + *

+ * Note that the order of messages received here may not be stable, so clients should not + * rely on the order that multiple listeners receive events in. + * + * @param listener the listener where the results of the asynchronous {@link #connect()} + * call are delivered. + */ + public Builder addConnectionCallbacks(ConnectionCallbacks listener) { + connectionCallbacks.add(listener); + return this; + } + + /** + * Adds a listener to register to receive connection failed events from this + * {@link GoogleApiClient}. Applications should balance calls to this method with calls to + * {@link #unregisterConnectionFailedListener(OnConnectionFailedListener)} to avoid + * leaking resources. + *

+ * If the specified listener is already registered to receive connection failed events, + * this method will not add a duplicate entry for the same listener. + *

+ * Note that the order of messages received here may not be stable, so clients should not + * rely on the order that multiple listeners receive events in. + * + * @param listener the listener where the results of the asynchronous {@link #connect()} + * call are delivered. + */ + public Builder addOnConnectionFailedListener(OnConnectionFailedListener listener) { + connectionFailedListeners.add(listener); + return this; + } + + /** + * Specify the OAuth 2.0 scopes requested by your app. See + * {@link com.google.android.gms.common.Scopes} for more information. + * + * @param scope The OAuth 2.0 scopes requested by your app. + * @see com.google.android.gms.common.Scopes + */ + public Builder addScope(Scope scope) { + scopes.add(scope.getScopeUri()); + return this; + } + + /** + * Builds a new {@link GoogleApiClient} object for communicating with the Google APIs. + * + * @return The {@link GoogleApiClient} object. + */ + public GoogleApiClient build() { + return new GoogleApiClientImpl(context, looper, getAccountInfo(), apis, + connectionCallbacks, connectionFailedListeners, clientId); + } + + private AccountInfo getAccountInfo() { + return null; + } + + public Builder enableAutoManage(FragmentActivity fragmentActivity, int cliendId, + OnConnectionFailedListener unresolvedConnectionFailedListener) + throws NullPointerException, IllegalArgumentException, IllegalStateException { + this.fragmentActivity = fragmentActivity; + this.clientId = cliendId; + this.unresolvedConnectionFailedListener = unresolvedConnectionFailedListener; + return this; + } + + /** + * Specify an account name on the device that should be used. If this is never called, the + * client will use the current default account for Google Play services for this + * application. + * + * @param accountName The account name on the device that should be used by + * {@link GoogleApiClient}. + */ + public Builder setAccountName(String accountName) { + this.accountName = accountName; + return this; + } + + /** + * Specifies the part of the screen at which games service popups (for example, + * "welcome back" or "achievement unlocked" popups) will be displayed using gravity. + * + * @param gravityForPopups The gravity which controls the placement of games service popups. + */ + public Builder setGravityForPopups(int gravityForPopups) { + this.gravityForPopups = gravityForPopups; + return this; + } + + /** + * Sets a {@link Handler} to indicate which thread to use when invoking callbacks. Will not + * be used directly to handle callbacks. If this is not called then the application's main + * thread will be used. + */ + public Builder setHandler(Handler handler) { + this.looper = handler.getLooper(); + return this; + } + + /** + * Sets the {@link View} to use as a content view for popups. + * + * @param viewForPopups The view to use as a content view for popups. View cannot be null. + */ + public Builder setViewForPopups(View viewForPopups) { + this.viewForPopups = viewForPopups; + return this; + } + + /** + * Specify that the default account should be used when connecting to services. + */ + public Builder useDefaultAccount() { + this.accountName = AuthConstants.DEFAULT_ACCOUNT; + return this; + } + } + + /** + * Provides callbacks that are called when the client is connected or disconnected from the + * service. Most applications implement {@link #onConnected(Bundle)} to start making requests. + */ + @PublicApi + interface ConnectionCallbacks { + /** + * A suspension cause informing that the service has been killed. + */ + int CAUSE_SERVICE_DISCONNECTED = 1; + /** + * A suspension cause informing you that a peer device connection was lost. + */ + int CAUSE_NETWORK_LOST = 2; + + /** + * After calling {@link #connect()}, this method will be invoked asynchronously when the + * connect request has successfully completed. After this callback, the application can + * make requests on other methods provided by the client and expect that no user + * intervention is required to call methods that use account and scopes provided to the + * client constructor. + *

+ * Note that the contents of the {@code connectionHint} Bundle are defined by the specific + * services. Please see the documentation of the specific implementation of + * {@link GoogleApiClient} you are using for more information. + * + * @param connectionHint Bundle of data provided to clients by Google Play services. May + * be null if no content is provided by the service. + */ + void onConnected(Bundle connectionHint); + + /** + * Called when the client is temporarily in a disconnected state. This can happen if there + * is a problem with the remote service (e.g. a crash or resource problem causes it to be + * killed by the system). When called, all requests have been canceled and no outstanding + * listeners will be executed. GoogleApiClient will automatically attempt to restore the + * connection. Applications should disable UI components that require the service, and wait + * for a call to {@link #onConnected(Bundle)} to re-enable them. + * + * @param cause The reason for the disconnection. Defined by constants {@code CAUSE_*}. + */ + void onConnectionSuspended(int cause); + } + + /** + * Provides callbacks for scenarios that result in a failed attempt to connect the client to + * the service. See {@link ConnectionResult} for a list of error codes and suggestions for + * resolution. + */ + @PublicApi + interface OnConnectionFailedListener { + /** + * Called when there was an error connecting the client to the service. + * + * @param result A {@link ConnectionResult} that can be used for resolving the error, and + * deciding what sort of error occurred. To resolve the error, the resolution + * must be started from an activity with a non-negative {@code requestCode} + * passed to {@link ConnectionResult#startResolutionForResult(Activity, int)}. + * Applications should implement {@link Activity#onActivityResult} in their + * Activity to call {@link #connect()} again if the user has resolved the + * issue (resultCode is {@link Activity#RESULT_OK}). + */ + void onConnectionFailed(ConnectionResult result); + } +} diff --git a/play-services-base/src/main/java/com/google/android/gms/common/api/PendingResult.java b/play-services-base/src/main/java/com/google/android/gms/common/api/PendingResult.java new file mode 100644 index 00000000..ec162920 --- /dev/null +++ b/play-services-base/src/main/java/com/google/android/gms/common/api/PendingResult.java @@ -0,0 +1,59 @@ +/* + * 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.common.api; + +import java.util.concurrent.TimeUnit; + +/** + * Represents a pending result from calling an API method in Google Play services. The final result + * object from a PendingResult is of type R, which can be retrieved in one of two ways. + *

+ *

+ * After the result has been retrieved using {@link #await()} or delivered to the result callback, + * it is an error to attempt to retrieve the result again. It is the responsibility of the caller + * or callback receiver to release any resources associated with the returned result. Some result + * types may implement {@link Releasable}, in which case {@link Releasable#release()} should be + * used to free the associated resources. + *

+ * TODO: Docs + */ +public interface PendingResult { + /** + * Blocks until the task is completed. This is not allowed on the UI thread. The returned + * result object can have an additional failure mode of INTERRUPTED. + */ + public R await(); + + /** + * Blocks until the task is completed or has timed out waiting for the result. This is not + * allowed on the UI thread. The returned result object can have an additional failure mode + * of either INTERRUPTED or TIMEOUT. + */ + public R await(long time, TimeUnit unit); + + public void cancel(); + + public boolean isCanceled(); + + public void setResultCallback(ResultCallback callback, long time, TimeUnit unit); + + public void setResultCallback(ResultCallback callback); +} diff --git a/play-services-base/src/main/java/com/google/android/gms/common/data/DataBuffer.java b/play-services-base/src/main/java/com/google/android/gms/common/data/DataBuffer.java new file mode 100644 index 00000000..309a667b --- /dev/null +++ b/play-services-base/src/main/java/com/google/android/gms/common/data/DataBuffer.java @@ -0,0 +1,89 @@ +/* + * 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.common.data; + +import com.google.android.gms.common.api.Releasable; + +import org.microg.gms.common.PublicApi; + +import java.util.Iterator; + +/** + * TODO + */ +@PublicApi +public abstract class DataBuffer implements Releasable, Iterable { + + private DataHolder dataHolder; + + @PublicApi(exclude = true) + public DataBuffer(DataHolder dataHolder) { + this.dataHolder = dataHolder; + } + + /** + * @deprecated use {@link #release()} instead + */ + @Deprecated + public final void close() { + release(); + } + + /** + * Get the item at the specified position. Note that the objects returned from subsequent + * invocations of this method for the same position may not be identical objects, but will be + * equal in value. + * + * @param position The position of the item to retrieve. + * @return the item at {@code position} in this buffer. + */ + public abstract T get(int position); + + public int getCount() { + return dataHolder == null ? 0 : dataHolder.getCount(); + } + + /** + * @deprecated {@link #release()} is idempotent, and so is safe to call multiple times + */ + @Deprecated + public boolean isClosed() { + return false; + } + + @Override + public Iterator iterator() { + return null; + } + + /** + * Releases resources used by the buffer. This method is idempotent. + */ + @Override + public void release() { + + } + + /** + * In order to use this one should correctly override setDataRow(int) in his DataBufferRef + * implementation. Be careful: there will be single DataBufferRef while iterating. + * If you are not sure - DO NOT USE this iterator. + */ + public Iterator singleRefIterator() { + return null; + } +} diff --git a/play-services-base/src/main/java/org/microg/gms/common/DummyApiConnection.java b/play-services-base/src/main/java/org/microg/gms/common/DummyApiConnection.java new file mode 100644 index 00000000..9476d3e7 --- /dev/null +++ b/play-services-base/src/main/java/org/microg/gms/common/DummyApiConnection.java @@ -0,0 +1,43 @@ +/* + * 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.common; + +import org.microg.gms.common.api.ApiConnection; + +public class DummyApiConnection implements ApiConnection { + private boolean connected = false; + + @Override + public void connect() { + connected = true; + } + + @Override + public void disconnect() { + connected = false; + } + + @Override + public boolean isConnected() { + return connected; + } + + @Override + public boolean isConnecting() { + return false; + } +} diff --git a/play-services-base/src/main/java/org/microg/gms/common/ForwardConnectionCallbacks.java b/play-services-base/src/main/java/org/microg/gms/common/ForwardConnectionCallbacks.java new file mode 100644 index 00000000..fdffdac7 --- /dev/null +++ b/play-services-base/src/main/java/org/microg/gms/common/ForwardConnectionCallbacks.java @@ -0,0 +1,51 @@ +/* + * 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.common; + +import android.os.Bundle; + +import com.google.android.gms.common.GooglePlayServicesClient; +import com.google.android.gms.common.api.GoogleApiClient; + +public final class ForwardConnectionCallbacks implements GoogleApiClient.ConnectionCallbacks { + private final GooglePlayServicesClient.ConnectionCallbacks callbacks; + + public ForwardConnectionCallbacks(GooglePlayServicesClient.ConnectionCallbacks callbacks) { + this.callbacks = callbacks; + } + + @Override + public boolean equals(Object o) { + return o instanceof ForwardConnectionCallbacks && + callbacks.equals(((ForwardConnectionCallbacks) o).callbacks); + } + + @Override + public int hashCode() { + return callbacks.hashCode(); + } + + @Override + public void onConnected(Bundle connectionHint) { + callbacks.onConnected(connectionHint); + } + + @Override + public void onConnectionSuspended(int cause) { + callbacks.onDisconnected(); + } +} diff --git a/play-services-base/src/main/java/org/microg/gms/common/ForwardConnectionFailedListener.java b/play-services-base/src/main/java/org/microg/gms/common/ForwardConnectionFailedListener.java new file mode 100644 index 00000000..1d1f4434 --- /dev/null +++ b/play-services-base/src/main/java/org/microg/gms/common/ForwardConnectionFailedListener.java @@ -0,0 +1,47 @@ +/* + * 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.common; + +import com.google.android.gms.common.ConnectionResult; +import com.google.android.gms.common.GooglePlayServicesClient; +import com.google.android.gms.common.api.GoogleApiClient; + +public final class ForwardConnectionFailedListener + implements GoogleApiClient.OnConnectionFailedListener { + private final GooglePlayServicesClient.OnConnectionFailedListener listener; + + public ForwardConnectionFailedListener( + GooglePlayServicesClient.OnConnectionFailedListener listener) { + this.listener = listener; + } + + @Override + public boolean equals(Object o) { + return o instanceof ForwardConnectionFailedListener && + listener.equals(((ForwardConnectionFailedListener) o).listener); + } + + @Override + public int hashCode() { + return listener.hashCode(); + } + + @Override + public void onConnectionFailed(ConnectionResult result) { + listener.onConnectionFailed(result); + } +} diff --git a/play-services-base/src/main/java/org/microg/gms/common/GmsClient.java b/play-services-base/src/main/java/org/microg/gms/common/GmsClient.java new file mode 100644 index 00000000..779f4769 --- /dev/null +++ b/play-services-base/src/main/java/org/microg/gms/common/GmsClient.java @@ -0,0 +1,184 @@ +/* + * 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.common; + +import android.accounts.Account; +import android.content.ComponentName; +import android.content.Context; +import android.content.ServiceConnection; +import android.os.Bundle; +import android.os.IBinder; +import android.os.IInterface; +import android.os.RemoteException; +import android.util.Log; + +import com.google.android.gms.common.ConnectionResult; +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.internal.GetServiceRequest; +import com.google.android.gms.common.internal.IGmsCallbacks; +import com.google.android.gms.common.internal.IGmsServiceBroker; + +import org.microg.gms.common.api.ApiConnection; + +public abstract class GmsClient implements ApiConnection { + private static final String TAG = "GmsClient"; + + private final Context context; + protected final GoogleApiClient.ConnectionCallbacks callbacks; + protected final GoogleApiClient.OnConnectionFailedListener connectionFailedListener; + protected ConnectionState state = ConnectionState.NOT_CONNECTED; + private ServiceConnection serviceConnection; + private I serviceInterface; + private String actionString; + + protected int serviceId = -1; + protected Account account = null; + protected Bundle extras = new Bundle(); + + public GmsClient(Context context, GoogleApiClient.ConnectionCallbacks callbacks, + GoogleApiClient.OnConnectionFailedListener connectionFailedListener, String actionString) { + this.context = context; + this.callbacks = callbacks; + this.connectionFailedListener = connectionFailedListener; + this.actionString = actionString; + } + + protected void onConnectedToBroker(IGmsServiceBroker broker, GmsCallbacks callbacks) throws RemoteException { + if (serviceId == -1) { + throw new IllegalStateException("Service ID not set in constructor and onConnectedToBroker not implemented"); + } + GetServiceRequest request = new GetServiceRequest(serviceId); + request.extras = new Bundle(); + request.packageName = context.getPackageName(); + request.account = account; + request.extras = extras; + broker.getService(callbacks, request); + } + + protected abstract I interfaceFromBinder(IBinder binder); + + @Override + public synchronized void connect() { + Log.d(TAG, "connect()"); + if (state == ConnectionState.CONNECTED || state == ConnectionState.CONNECTING) { + Log.d(TAG, "Already connected/connecting - nothing to do"); + } + state = ConnectionState.CONNECTING; + if (serviceConnection != null) { + MultiConnectionKeeper.getInstance(context).unbind(actionString, serviceConnection); + } + serviceConnection = new GmsServiceConnection(); + if (!MultiConnectionKeeper.getInstance(context).bind(actionString, serviceConnection)) { + state = ConnectionState.ERROR; + handleConnectionFailed(); + } + } + + public void handleConnectionFailed() { + connectionFailedListener.onConnectionFailed(new ConnectionResult(ConnectionResult + .API_UNAVAILABLE, null)); + } + + @Override + public synchronized void disconnect() { + Log.d(TAG, "disconnect()"); + if (state == ConnectionState.DISCONNECTING) return; + if (state == ConnectionState.CONNECTING) { + state = ConnectionState.DISCONNECTING; + return; + } + serviceInterface = null; + if (serviceConnection != null) { + MultiConnectionKeeper.getInstance(context).unbind(actionString, serviceConnection); + serviceConnection = null; + } + state = ConnectionState.NOT_CONNECTED; + } + + @Override + public synchronized boolean isConnected() { + return state == ConnectionState.CONNECTED || state == ConnectionState.PSEUDO_CONNECTED; + } + + @Override + public synchronized boolean isConnecting() { + return state == ConnectionState.CONNECTING; + } + + public synchronized boolean hasError() { + return state == ConnectionState.ERROR; + } + + public Context getContext() { + return context; + } + + public synchronized I getServiceInterface() { + if (isConnecting()) { + // TODO: wait for connection to be established and return afterwards. + throw new IllegalStateException("Waiting for connection"); + } else if (!isConnected()) { + throw new IllegalStateException("interface only available once connected!"); + } + return serviceInterface; + } + + protected enum ConnectionState { + NOT_CONNECTED, CONNECTING, CONNECTED, DISCONNECTING, ERROR, PSEUDO_CONNECTED + } + + private class GmsServiceConnection implements ServiceConnection { + + @Override + public void onServiceConnected(ComponentName componentName, IBinder iBinder) { + try { + Log.d(TAG, "ServiceConnection : onServiceConnected(" + componentName + ")"); + onConnectedToBroker(IGmsServiceBroker.Stub.asInterface(iBinder), + new GmsCallbacks()); + } catch (RemoteException e) { + disconnect(); + } + } + + @Override + public void onServiceDisconnected(ComponentName componentName) { + synchronized (GmsClient.this) { + state = ConnectionState.NOT_CONNECTED; + } + } + } + + public class GmsCallbacks extends IGmsCallbacks.Stub { + + @Override + public void onPostInitComplete(int statusCode, IBinder binder, Bundle params) + throws RemoteException { + synchronized (GmsClient.this) { + if (state == ConnectionState.DISCONNECTING) { + state = ConnectionState.CONNECTED; + disconnect(); + return; + } + state = ConnectionState.CONNECTED; + serviceInterface = interfaceFromBinder(binder); + } + Log.d(TAG, "GmsCallbacks : onPostInitComplete(" + serviceInterface + ")"); + callbacks.onConnected(params); + } + } + +} diff --git a/play-services-base/src/main/java/org/microg/gms/common/GmsConnector.java b/play-services-base/src/main/java/org/microg/gms/common/GmsConnector.java new file mode 100644 index 00000000..9de363f6 --- /dev/null +++ b/play-services-base/src/main/java/org/microg/gms/common/GmsConnector.java @@ -0,0 +1,93 @@ +/* + * 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.common; + +import android.os.Looper; +import android.os.Message; +import android.os.RemoteException; +import android.util.Log; + +import com.google.android.gms.common.api.Api; +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.PendingResult; +import com.google.android.gms.common.api.Result; + +import org.microg.gms.common.api.AbstractPendingResult; +import org.microg.gms.common.api.ApiConnection; +import org.microg.gms.common.api.GoogleApiClientImpl; + +public class GmsConnector { + private static final String TAG = "GmsConnector"; + + private final GoogleApiClientImpl apiClient; + private final Api api; + private final Callback callback; + + public GmsConnector(GoogleApiClient apiClient, Api api, Callback callback) { + this.apiClient = (GoogleApiClientImpl) apiClient; + this.api = api; + this.callback = callback; + } + + public static PendingResult call(GoogleApiClient client, Api api, GmsConnector.Callback callback) { + return new GmsConnector(client, api, callback).connect(); + } + + public AbstractPendingResult connect() { + Log.d(TAG, "connect()"); + apiClient.incrementUsageCounter(); + apiClient.getApiConnection(api); + Looper looper = apiClient.getLooper(); + final AbstractPendingResult result = new AbstractPendingResult(looper); + Message msg = new Message(); + msg.obj = result; + new Handler(looper).sendMessage(msg); + return result; + } + + public interface Callback { + void onClientAvailable(C client, ResultProvider resultProvider) throws RemoteException; + + interface ResultProvider { + void onResultAvailable(R result); + } + } + + private class Handler extends android.os.Handler { + private Handler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + Log.d(TAG, "Handler : handleMessage"); + final AbstractPendingResult result = (AbstractPendingResult) msg.obj; + try { + C connection = (C) apiClient.getApiConnection(api); + callback.onClientAvailable(connection, new GmsConnector.Callback.ResultProvider() { + @Override + public void onResultAvailable(R realResult) { + result.deliverResult(realResult); + apiClient.decrementUsageCounter(); + } + }); + } catch (RemoteException ignored) { + + } + } + } +} diff --git a/play-services-base/src/main/java/org/microg/gms/common/MultiConnectionKeeper.java b/play-services-base/src/main/java/org/microg/gms/common/MultiConnectionKeeper.java new file mode 100644 index 00000000..ea4cfbe6 --- /dev/null +++ b/play-services-base/src/main/java/org/microg/gms/common/MultiConnectionKeeper.java @@ -0,0 +1,176 @@ +/* + * 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.common; + +import android.annotation.SuppressLint; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.IBinder; +import android.util.Log; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import static android.os.Build.VERSION.SDK_INT; +import static android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH; +import static org.microg.gms.common.Constants.GMS_PACKAGE_NAME; + +public class MultiConnectionKeeper { + private static final String TAG = "GmsMultiConKeeper"; + + private static MultiConnectionKeeper INSTANCE; + + private final Context context; + private final Map connections = new HashMap(); + + public MultiConnectionKeeper(Context context) { + this.context = context; + } + + public synchronized static MultiConnectionKeeper getInstance(Context context) { + if (INSTANCE == null) + INSTANCE = new MultiConnectionKeeper(context); + return INSTANCE; + } + + public synchronized boolean bind(String action, ServiceConnection connection) { + Log.d(TAG, "bind(" + action + ", " + connection + ")"); + Connection con = connections.get(action); + if (con != null) { + if (!con.forwardsConnection(connection)) { + con.addConnectionForward(connection); + if (!con.isBound()) + con.bind(); + } + } else { + con = new Connection(action); + con.addConnectionForward(connection); + con.bind(); + connections.put(action, con); + } + return con.isBound(); + } + + public synchronized void unbind(String action, ServiceConnection connection) { + Log.d(TAG, "unbind(" + action + ", " + connection + ")"); + Connection con = connections.get(action); + if (con != null) { + con.removeConnectionForward(connection); + if (!con.hasForwards() && con.isBound()) { + con.unbind(); + connections.remove(action); + } + } + } + + public class Connection { + private final String actionString; + private final Set connectionForwards = new HashSet(); + private boolean bound = false; + private boolean connected = false; + private IBinder binder; + private ComponentName component; + private ServiceConnection serviceConnection = new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName componentName, IBinder iBinder) { + Log.d(TAG, "Connection(" + actionString + ") : ServiceConnection : " + + "onServiceConnected(" + componentName + ")"); + binder = iBinder; + component = componentName; + for (ServiceConnection connection : connectionForwards) { + connection.onServiceConnected(componentName, iBinder); + } + connected = true; + } + + @Override + public void onServiceDisconnected(ComponentName componentName) { + Log.d(TAG, "Connection(" + actionString + ") : ServiceConnection : " + + "onServiceDisconnected(" + componentName + ")"); + binder = null; + component = componentName; + for (ServiceConnection connection : connectionForwards) { + connection.onServiceDisconnected(componentName); + } + connected = false; + bound = false; + } + }; + + public Connection(String actionString) { + this.actionString = actionString; + } + + @SuppressLint("InlinedApi") + public void bind() { + Log.d(TAG, "Connection(" + actionString + ") : bind()"); + Intent intent = new Intent(actionString).setPackage(GMS_PACKAGE_NAME); + int flags = Context.BIND_AUTO_CREATE; + if (SDK_INT >= ICE_CREAM_SANDWICH) { + flags |= Context.BIND_ADJUST_WITH_ACTIVITY; + } + bound = context.bindService(intent, serviceConnection, flags); + if (!bound) { + context.unbindService(serviceConnection); + } + } + + public boolean isBound() { + return bound; + } + + public IBinder getBinder() { + return binder; + } + + public void unbind() { + Log.d(TAG, "Connection(" + actionString + ") : unbind()"); + try { + context.unbindService(serviceConnection); + } catch (IllegalArgumentException e) { // not bound (whatever reason) + Log.w(TAG, e); + } + bound = false; + } + + public void addConnectionForward(ServiceConnection connection) { + connectionForwards.add(connection); + if (connected) { + connection.onServiceConnected(component, binder); + } + } + + public void removeConnectionForward(ServiceConnection connection) { + connectionForwards.remove(connection); + if (connected) { + connection.onServiceDisconnected(component); + } + } + + public boolean forwardsConnection(ServiceConnection connection) { + return connectionForwards.contains(connection); + } + + public boolean hasForwards() { + return !connectionForwards.isEmpty(); + } + } +} diff --git a/play-services-base/src/main/java/org/microg/gms/common/api/AbstractPendingResult.java b/play-services-base/src/main/java/org/microg/gms/common/api/AbstractPendingResult.java new file mode 100644 index 00000000..15aac9e3 --- /dev/null +++ b/play-services-base/src/main/java/org/microg/gms/common/api/AbstractPendingResult.java @@ -0,0 +1,111 @@ +/* + * 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.common.api; + +import android.os.Looper; + +import com.google.android.gms.common.api.PendingResult; +import com.google.android.gms.common.api.Result; +import com.google.android.gms.common.api.ResultCallback; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +public class AbstractPendingResult implements PendingResult { + private final Object lock = new Object(); + private final CountDownLatch countDownLatch = new CountDownLatch(1); + private final ResultCallbackHandler handler; + private boolean canceled; + private R result; + private ResultCallback resultCallback; + + public AbstractPendingResult(Looper looper) { + handler = new ResultCallbackHandler(looper); + } + + private R getResult() { + synchronized (lock) { + return result; + } + } + + @Override + public R await() { + try { + countDownLatch.await(); + } catch (InterruptedException ignored) { + } + return getResult(); + } + + @Override + public R await(long time, TimeUnit unit) { + try { + countDownLatch.await(time, unit); + } catch (InterruptedException ignored) { + } + return getResult(); + } + + @Override + public void cancel() { + // TODO + } + + @Override + public boolean isCanceled() { + synchronized (lock) { + return canceled; + } + } + + public boolean isReady() { + return this.countDownLatch.getCount() == 0L; + } + + @Override + public void setResultCallback(ResultCallback callback, long time, TimeUnit unit) { + synchronized (lock) { + if (!isCanceled()) { + if (isReady()) { + handler.sendResultCallback(callback, getResult()); + } else { + handler.sendTimeoutResultCallback(this, unit.toMillis(time)); + } + } + } + } + + @Override + public void setResultCallback(ResultCallback callback) { + synchronized (lock) { + if (!isCanceled()) { + if (isReady()) { + handler.sendResultCallback(callback, getResult()); + } else { + resultCallback = callback; + } + } + } + } + + public void deliverResult(R result) { + this.result = result; + countDownLatch.countDown(); + + } +} diff --git a/play-services-base/src/main/java/org/microg/gms/common/api/AbstractPlayServicesClient.java b/play-services-base/src/main/java/org/microg/gms/common/api/AbstractPlayServicesClient.java new file mode 100644 index 00000000..eb9d66bd --- /dev/null +++ b/play-services-base/src/main/java/org/microg/gms/common/api/AbstractPlayServicesClient.java @@ -0,0 +1,99 @@ +/* + * 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.common.api; + +import android.util.Log; + +import com.google.android.gms.common.GooglePlayServicesClient; +import com.google.android.gms.common.api.GoogleApiClient; + +import org.microg.gms.common.ForwardConnectionCallbacks; +import org.microg.gms.common.ForwardConnectionFailedListener; + +public class AbstractPlayServicesClient implements GooglePlayServicesClient { + private static final String TAG = "GmsPlayServicesClient"; + + protected final GoogleApiClient googleApiClient; + + public AbstractPlayServicesClient(GoogleApiClient googleApiClient) { + this.googleApiClient = googleApiClient; + } + + public void assertConnected() { + if (!isConnected()) throw new IllegalStateException("Not connected!"); + } + + @Override + public void connect() { + Log.d(TAG, "connect()"); + googleApiClient.connect(); + } + + @Override + public void disconnect() { + Log.d(TAG, "disconnect()"); + //TODO googleApiClient.disconnect(); + } + + @Override + public boolean isConnected() { + return googleApiClient.isConnected(); + } + + @Override + public boolean isConnecting() { + return googleApiClient.isConnecting(); + } + + @Override + public void registerConnectionCallbacks(final ConnectionCallbacks listener) { + googleApiClient.registerConnectionCallbacks(new ForwardConnectionCallbacks(listener)); + } + + @Override + public boolean isConnectionCallbacksRegistered(ConnectionCallbacks listener) { + return googleApiClient + .isConnectionCallbacksRegistered(new ForwardConnectionCallbacks(listener)); + } + + @Override + public void unregisterConnectionCallbacks( + ConnectionCallbacks listener) { + googleApiClient.unregisterConnectionCallbacks(new ForwardConnectionCallbacks(listener)); + } + + @Override + public void registerConnectionFailedListener( + OnConnectionFailedListener listener) { + googleApiClient.registerConnectionFailedListener( + new ForwardConnectionFailedListener(listener)); + } + + @Override + public boolean isConnectionFailedListenerRegistered( + OnConnectionFailedListener listener) { + return googleApiClient.isConnectionFailedListenerRegistered( + new ForwardConnectionFailedListener(listener)); + } + + @Override + public void unregisterConnectionFailedListener( + OnConnectionFailedListener listener) { + googleApiClient.unregisterConnectionFailedListener( + new ForwardConnectionFailedListener(listener)); + } +} diff --git a/play-services-base/src/main/java/org/microg/gms/common/api/ApiBuilder.java b/play-services-base/src/main/java/org/microg/gms/common/api/ApiBuilder.java new file mode 100644 index 00000000..7d1fb020 --- /dev/null +++ b/play-services-base/src/main/java/org/microg/gms/common/api/ApiBuilder.java @@ -0,0 +1,30 @@ +/* + * 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.common.api; + +import android.content.Context; +import android.os.Looper; + +import com.google.android.gms.common.api.AccountInfo; +import com.google.android.gms.common.api.Api; +import com.google.android.gms.common.api.GoogleApiClient; + +public interface ApiBuilder { + ApiConnection build(Context context, Looper looper, O options, AccountInfo accountInfo, + GoogleApiClient.ConnectionCallbacks callbacks, + GoogleApiClient.OnConnectionFailedListener connectionFailedListener); +} diff --git a/play-services-base/src/main/java/org/microg/gms/common/api/ApiConnection.java b/play-services-base/src/main/java/org/microg/gms/common/api/ApiConnection.java new file mode 100644 index 00000000..9ca239b4 --- /dev/null +++ b/play-services-base/src/main/java/org/microg/gms/common/api/ApiConnection.java @@ -0,0 +1,27 @@ +/* + * 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.common.api; + +public interface ApiConnection { + void connect(); + + void disconnect(); + + boolean isConnected(); + + boolean isConnecting(); +} diff --git a/play-services-base/src/main/java/org/microg/gms/common/api/GoogleApiClientImpl.java b/play-services-base/src/main/java/org/microg/gms/common/api/GoogleApiClientImpl.java new file mode 100644 index 00000000..8c802d4a --- /dev/null +++ b/play-services-base/src/main/java/org/microg/gms/common/api/GoogleApiClientImpl.java @@ -0,0 +1,242 @@ +/* + * 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.common.api; + +import android.content.Context; +import android.os.Bundle; +import android.os.Looper; +import android.os.Message; +import androidx.fragment.app.FragmentActivity; +import android.util.Log; + +import com.google.android.gms.common.ConnectionResult; +import com.google.android.gms.common.api.AccountInfo; +import com.google.android.gms.common.api.Api; +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.PendingResult; +import com.google.android.gms.common.api.Status; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +public class GoogleApiClientImpl implements GoogleApiClient { + private static final String TAG = "GmsApiClientImpl"; + + private final Context context; + private final Looper looper; + private final AccountInfo accountInfo; + private final Map apis = new HashMap(); + private final Map apiConnections = new HashMap(); + private final Handler handler; + private final Set connectionCallbacks = new HashSet(); + private final Set connectionFailedListeners = new HashSet(); + private final int clientId; + private final ConnectionCallbacks baseConnectionCallbacks = new ConnectionCallbacks() { + @Override + public void onConnected(Bundle connectionHint) { + Log.d(TAG, "ConnectionCallbacks : onConnected()"); + for (ConnectionCallbacks callback : connectionCallbacks) { + callback.onConnected(connectionHint); + } + } + + @Override + public void onConnectionSuspended(int cause) { + Log.d(TAG, "ConnectionCallbacks : onConnectionSuspended()"); + for (ConnectionCallbacks callback : connectionCallbacks) { + callback.onConnectionSuspended(cause); + } + } + }; + private final OnConnectionFailedListener baseConnectionFailedListener = new + OnConnectionFailedListener() { + @Override + public void onConnectionFailed(ConnectionResult result) { + Log.d(TAG, "OnConnectionFailedListener : onConnectionFailed()"); + for (OnConnectionFailedListener listener : connectionFailedListeners) { + listener.onConnectionFailed(result); + } + } + }; + private int usageCounter = 0; + private boolean shouldDisconnect = false; + + public GoogleApiClientImpl(Context context, Looper looper, AccountInfo accountInfo, + Map apis, + Set connectionCallbacks, + Set connectionFailedListeners, int clientId) { + this.context = context; + this.looper = looper; + this.handler = new Handler(looper); + this.accountInfo = accountInfo; + this.apis.putAll(apis); + this.connectionCallbacks.addAll(connectionCallbacks); + this.connectionFailedListeners.addAll(connectionFailedListeners); + this.clientId = clientId; + + for (Api api : apis.keySet()) { + apiConnections.put(api, api.getBuilder().build(context, looper, + apis.get(api), accountInfo, baseConnectionCallbacks, + baseConnectionFailedListener)); + } + } + + public synchronized void incrementUsageCounter() { + usageCounter++; + } + + public synchronized void decrementUsageCounter() { + usageCounter--; + if (shouldDisconnect) disconnect(); + } + + public Looper getLooper() { + return looper; + } + + public ApiConnection getApiConnection(Api api) { + return apiConnections.get(api); + } + + @Override + public ConnectionResult blockingConnect() { + return null; + } + + @Override + public ConnectionResult blockingConnect(long timeout, TimeUnit unit) { + return null; + } + + @Override + public PendingResult clearDefaultAccountAndReconnect() { + return null; + } + + @Override + public synchronized void connect() { + Log.d(TAG, "connect()"); + if (isConnected() || isConnecting()) { + if (shouldDisconnect) { + shouldDisconnect = false; + return; + } + Log.d(TAG, "Already connected/connecting, nothing to do"); + return; + } + for (ApiConnection connection : apiConnections.values()) { + if (!connection.isConnected()) { + connection.connect(); + } + } + } + + @Override + public synchronized void disconnect() { + if (usageCounter > 0) { + shouldDisconnect = true; + } else { + Log.d(TAG, "disconnect()"); + for (ApiConnection connection : apiConnections.values()) { + if (connection.isConnected()) { + connection.disconnect(); + } + } + } + } + + @Override + public synchronized boolean isConnected() { + for (ApiConnection connection : apiConnections.values()) { + if (!connection.isConnected()) return false; + } + return true; + } + + @Override + public synchronized boolean isConnecting() { + for (ApiConnection connection : apiConnections.values()) { + if (connection.isConnecting()) return true; + } + return false; + } + + @Override + public boolean isConnectionCallbacksRegistered(ConnectionCallbacks listener) { + return connectionCallbacks.contains(listener); + } + + @Override + public boolean isConnectionFailedListenerRegistered( + OnConnectionFailedListener listener) { + return connectionFailedListeners.contains(listener); + } + + @Override + public synchronized void reconnect() { + Log.d(TAG, "reconnect()"); + disconnect(); + connect(); + } + + @Override + public void registerConnectionCallbacks(ConnectionCallbacks listener) { + connectionCallbacks.add(listener); + } + + @Override + public void registerConnectionFailedListener(OnConnectionFailedListener listener) { + connectionFailedListeners.add(listener); + } + + @Override + public void stopAutoManager(FragmentActivity lifecycleActivity) throws IllegalStateException { + + } + + @Override + public void unregisterConnectionCallbacks(ConnectionCallbacks listener) { + connectionCallbacks.remove(listener); + } + + @Override + public void unregisterConnectionFailedListener(OnConnectionFailedListener listener) { + connectionFailedListeners.remove(listener); + } + + private class Handler extends android.os.Handler { + private Handler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + if (msg.what == 0 && msg.obj instanceof Runnable) { + ((Runnable) msg.obj).run(); + } else { + super.handleMessage(msg); + } + } + + public void sendRunnable(Runnable runnable) { + sendMessage(obtainMessage(1, runnable)); + } + } +} diff --git a/play-services-base/src/main/java/org/microg/gms/common/api/InstantPendingResult.java b/play-services-base/src/main/java/org/microg/gms/common/api/InstantPendingResult.java new file mode 100644 index 00000000..ba75725c --- /dev/null +++ b/play-services-base/src/main/java/org/microg/gms/common/api/InstantPendingResult.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 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.common.api; + +import com.google.android.gms.common.api.PendingResult; +import com.google.android.gms.common.api.Result; +import com.google.android.gms.common.api.ResultCallback; + +import java.util.concurrent.TimeUnit; + +public class InstantPendingResult implements PendingResult { + R value; + + public InstantPendingResult(R value) { + this.value = value; + } + + @Override + public R await() { + return value; + } + + @Override + public R await(long time, TimeUnit unit) { + return value; + } + + @Override + public void cancel() { + + } + + @Override + public boolean isCanceled() { + return false; + } + + @Override + public void setResultCallback(ResultCallback callback, long time, TimeUnit unit) { + callback.onResult(value); + } + + @Override + public void setResultCallback(ResultCallback callback) { + callback.onResult(value); + } +} diff --git a/play-services-base/src/main/java/org/microg/gms/common/api/ResultCallbackHandler.java b/play-services-base/src/main/java/org/microg/gms/common/api/ResultCallbackHandler.java new file mode 100644 index 00000000..c19c849c --- /dev/null +++ b/play-services-base/src/main/java/org/microg/gms/common/api/ResultCallbackHandler.java @@ -0,0 +1,70 @@ +/* + * 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.common.api; + +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.util.Log; + +import com.google.android.gms.common.api.Result; +import com.google.android.gms.common.api.ResultCallback; + +class ResultCallbackHandler extends Handler { + private static final String TAG = "GmsResultCbackHandler"; + public static final int CALLBACK_ON_COMPLETE = 1; + public static final int CALLBACK_ON_TIMEOUT = 2; + + public ResultCallbackHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case CALLBACK_ON_COMPLETE: + OnCompleteObject o = (OnCompleteObject) msg.obj; + Log.d(TAG, "handleMessage() : onResult(" + o.result + ")"); + o.callback.onResult(o.result); + break; + case CALLBACK_ON_TIMEOUT: + // TODO + break; + } + } + + public void sendResultCallback(ResultCallback callback, R result) { + Message message = new Message(); + message.what = CALLBACK_ON_COMPLETE; + message.obj = new OnCompleteObject(callback, result); + sendMessage(message); + } + + public void sendTimeoutResultCallback(AbstractPendingResult pendingResult, long millis) { + + } + + public static class OnCompleteObject { + public ResultCallback callback; + public R result; + + public OnCompleteObject(ResultCallback callback, R result) { + this.callback = callback; + this.result = result; + } + } +} diff --git a/play-services-base/src/main/res/values/version.xml b/play-services-base/src/main/res/values/version.xml new file mode 100644 index 00000000..318bb1c2 --- /dev/null +++ b/play-services-base/src/main/res/values/version.xml @@ -0,0 +1,20 @@ + + + + + 6599436 + diff --git a/play-services-cast/build.gradle b/play-services-cast/build.gradle new file mode 100644 index 00000000..85ea138a --- /dev/null +++ b/play-services-cast/build.gradle @@ -0,0 +1,47 @@ +/* + * 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-base') + api project(':play-services-cast-api') +} diff --git a/play-services-cast/gradle.properties b/play-services-cast/gradle.properties new file mode 100644 index 00000000..937fcc06 --- /dev/null +++ b/play-services-cast/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 Cast Library +POM_DESCRIPTION=The Play Services Library module to access the Cast API + +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-cast/src/main/AndroidManifest.xml b/play-services-cast/src/main/AndroidManifest.xml new file mode 100644 index 00000000..7e14f394 --- /dev/null +++ b/play-services-cast/src/main/AndroidManifest.xml @@ -0,0 +1,18 @@ + + + + diff --git a/play-services-cast/src/main/java/com/google/android/gms/cast/Cast.java b/play-services-cast/src/main/java/com/google/android/gms/cast/Cast.java new file mode 100644 index 00000000..b223b584 --- /dev/null +++ b/play-services-cast/src/main/java/com/google/android/gms/cast/Cast.java @@ -0,0 +1,223 @@ +/* + * 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.cast; + +import android.os.Bundle; + +import com.google.android.gms.common.api.Api; +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.PendingResult; +import com.google.android.gms.common.api.Result; +import com.google.android.gms.common.api.Status; + +import org.microg.gms.cast.CastApiBuilder; +import org.microg.gms.cast.CastApiImpl; +import org.microg.gms.common.PublicApi; + +import java.io.IOException; + +@PublicApi +public final class Cast { + + /** + * A constant indicating that the Google Cast device is not the currently active video input. + */ + public static final int ACTIVE_INPUT_STATE_NO = 0; + + /** + * A constant indicating that it is not known (and/or not possible to know) whether the Google Cast device is + * the currently active video input. Active input state can only be reported when the Google Cast device is + * connected to a TV or AVR with CEC support. + */ + public static final int ACTIVE_INPUT_STATE_UNKNOWN = -1; + + /** + * A constant indicating that the Google Cast device is the currently active video input. + */ + public static final int ACTIVE_INPUT_STATE_YES = 1; + + /** + * A boolean extra for the connection hint bundle passed to + * {@link GoogleApiClient.ConnectionCallbacks#onConnected(Bundle)} that indicates that the connection was + * re-established, but the receiver application that was in use at the time of the connection loss is no longer + * running on the receiver. + */ + public static final String EXTRA_APP_NO_LONGER_RUNNING = "com.google.android.gms.cast.EXTRA_APP_NO_LONGER_RUNNING"; + + /** + * The maximum raw message length (in bytes) that is supported by a Cast channel. + */ + public static final int MAX_MESSAGE_LENGTH = 65536; + + /** + * The maximum length (in characters) of a namespace name. + */ + public static final int MAX_NAMESPACE_LENGTH = 128; + + /** + * A constant indicating that the Google Cast device is not currently in standby. + */ + public static final int STANDBY_STATE_NO = 0; + + /** + * A constant indicating that it is not known (and/or not possible to know) whether the Google Cast device is + * currently in standby. Standby state can only be reported when the Google Cast device is connected to a TV or + * AVR with CEC support. + */ + public static final int STANDBY_STATE_UNKNOWN = -1; + + /** + * A constant indicating that the Google Cast device is currently in standby. + */ + public static final int STANDBY_STATE_YES = 1; + + + /** + * Token to pass to {@link GoogleApiClient.Builder#addApi(Api)} to enable the Cast features. + */ + public static final Api API = new Api(new CastApiBuilder()); + + /** + * An implementation of the CastApi interface. The interface is used to interact with a cast device. + */ + public static final Cast.CastApi CastApi = new CastApiImpl(); + + private Cast() { + } + + public interface ApplicationConnectionResult extends Result { + ApplicationMetadata getApplicationMetadata(); + + String getApplicationStatus(); + + String getSessionId(); + + boolean getWasLaunched(); + } + + public interface CastApi { + int getActiveInputState(GoogleApiClient client); + + ApplicationMetadata getApplicationMetadata(GoogleApiClient client); + + String getApplicationStatus(GoogleApiClient client); + + int getStandbyState(GoogleApiClient client); + + double getVolume(GoogleApiClient client); + + boolean isMute(GoogleApiClient client); + + PendingResult joinApplication(GoogleApiClient client); + + PendingResult joinApplication(GoogleApiClient client, String applicationId, String sessionId); + + PendingResult joinApplication(GoogleApiClient client, String applicationId); + + PendingResult launchApplication(GoogleApiClient client, String applicationId, LaunchOptions launchOptions); + + PendingResult launchApplication(GoogleApiClient client, String applicationId); + + @Deprecated + PendingResult launchApplication(GoogleApiClient client, String applicationId, boolean relaunchIfRunning); + + PendingResult leaveApplication(GoogleApiClient client); + + void removeMessageReceivedCallbacks(GoogleApiClient client, String namespace) throws IOException; + + void requestStatus(GoogleApiClient client) throws IOException; + + PendingResult sendMessage(GoogleApiClient client, String namespace, String message); + + void setMessageReceivedCallbacks(GoogleApiClient client, String namespace, Cast.MessageReceivedCallback callbacks) throws IOException; + + void setMute(GoogleApiClient client, boolean mute) throws IOException; + + void setVolume(GoogleApiClient client, double volume) throws IOException; + + PendingResult stopApplication(GoogleApiClient client); + + PendingResult stopApplication(GoogleApiClient client, String sessionId); + } + + public static class CastOptions implements Api.ApiOptions.HasOptions { + private final CastDevice castDevice; + private final Listener castListener; + private final boolean verboseLoggingEnabled; + + public CastOptions(CastDevice castDevice, Listener castListener, boolean verboseLoggingEnabled) { + this.castDevice = castDevice; + this.castListener = castListener; + this.verboseLoggingEnabled = verboseLoggingEnabled; + } + + @Deprecated + public static Builder builder(CastDevice castDevice, Listener castListener) { + return new Builder(castDevice, castListener); + } + + public static class Builder { + private final CastDevice castDevice; + private final Listener castListener; + private boolean verboseLoggingEnabled; + + public Builder(CastDevice castDevice, Listener castListener) { + this.castDevice = castDevice; + this.castListener = castListener; + } + + public CastOptions build() { + return new CastOptions(castDevice, castListener, verboseLoggingEnabled); + } + + public Builder setVerboseLoggingEnabled(boolean verboseLoggingEnabled) { + this.verboseLoggingEnabled = verboseLoggingEnabled; + return this; + } + } + } + + public static class Listener { + public void onActiveInputStateChanged(int activeInputState) { + + } + + public void onApplicationDisconnected(int statusCode) { + + } + + public void onApplicationMetadataChanged(ApplicationMetadata applicationMetadata) { + + } + + public void onApplicationStatusChanged() { + + } + + public void onStandbyStateChanged(int standbyState) { + + } + + public void onVolumeChanged() { + + } + } + + public interface MessageReceivedCallback { + void onMessageReceived(CastDevice castDevice, String namespace, String message); + } +} diff --git a/play-services-cast/src/main/java/com/google/android/gms/cast/CastPresentation.java b/play-services-cast/src/main/java/com/google/android/gms/cast/CastPresentation.java new file mode 100644 index 00000000..cfc55685 --- /dev/null +++ b/play-services-cast/src/main/java/com/google/android/gms/cast/CastPresentation.java @@ -0,0 +1,33 @@ +/* + * 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.cast; + +import android.annotation.TargetApi; +import android.app.Presentation; +import android.content.Context; +import android.view.Display; + +@TargetApi(17) +public class CastPresentation extends Presentation { + public CastPresentation(Context outerContext, Display display) { + super(outerContext, display); + } + + public CastPresentation(Context outerContext, Display display, int theme) { + super(outerContext, display, theme); + } +} diff --git a/play-services-cast/src/main/java/com/google/android/gms/cast/CastRemoteDisplay.java b/play-services-cast/src/main/java/com/google/android/gms/cast/CastRemoteDisplay.java new file mode 100644 index 00000000..ddb5d53c --- /dev/null +++ b/play-services-cast/src/main/java/com/google/android/gms/cast/CastRemoteDisplay.java @@ -0,0 +1,76 @@ +/* + * 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.cast; + +import android.view.Display; + +import com.google.android.gms.common.api.Api; +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.Result; +import com.google.android.gms.common.api.Status; + +import org.microg.gms.cast.CastRemoteDisplayApiBuilder; +import org.microg.gms.cast.CastRemoteDisplayApiImpl; +import org.microg.gms.common.PublicApi; + +@PublicApi +public final class CastRemoteDisplay { + /** + * Token to pass to {@link GoogleApiClient.Builder#addApi(Api)} to enable the CastRemoteDisplay features. + */ + public static final Api API = new Api(new CastRemoteDisplayApiBuilder()); + + /** + * An implementation of the CastRemoteDisplayAPI interface. The interface is used to interact with a cast device. + */ + public static final CastRemoteDisplayApi CastApi = new CastRemoteDisplayApiImpl(); + + private CastRemoteDisplay() { + } + + public static final class CastRemoteDisplayOptions implements Api.ApiOptions.HasOptions { + private CastDevice castDevice; + private CastRemoteDisplaySessionCallbacks callbacks; + + private CastRemoteDisplayOptions(CastDevice castDevice, CastRemoteDisplaySessionCallbacks callbacks) { + this.castDevice = castDevice; + this.callbacks = callbacks; + } + + public static final class Builder { + private CastDevice castDevice; + private CastRemoteDisplaySessionCallbacks callbacks; + + public Builder(CastDevice castDevice, CastRemoteDisplaySessionCallbacks callbacks) { + this.castDevice = castDevice; + this.callbacks = callbacks; + } + + public CastRemoteDisplayOptions build() { + return new CastRemoteDisplayOptions(castDevice, callbacks); + } + } + } + + public interface CastRemoteDisplaySessionCallbacks { + void onRemoteDisplayEnded(Status status); + } + + public interface CastRemoteDisplaySessionResult extends Result { + Display getPresentationDisplay(); + } +} diff --git a/play-services-cast/src/main/java/com/google/android/gms/cast/CastRemoteDisplayApi.java b/play-services-cast/src/main/java/com/google/android/gms/cast/CastRemoteDisplayApi.java new file mode 100644 index 00000000..5c053581 --- /dev/null +++ b/play-services-cast/src/main/java/com/google/android/gms/cast/CastRemoteDisplayApi.java @@ -0,0 +1,26 @@ +/* + * 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.cast; + +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.PendingResult; + +public interface CastRemoteDisplayApi { + PendingResult startRemoteDisplay(GoogleApiClient apiClient, String applicationId); + + PendingResult stopRemoteDisplay(GoogleApiClient apiClient); +} diff --git a/play-services-cast/src/main/java/com/google/android/gms/cast/CastRemoteDisplayLocalService.java b/play-services-cast/src/main/java/com/google/android/gms/cast/CastRemoteDisplayLocalService.java new file mode 100644 index 00000000..dbc4108a --- /dev/null +++ b/play-services-cast/src/main/java/com/google/android/gms/cast/CastRemoteDisplayLocalService.java @@ -0,0 +1,28 @@ +/* + * 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.cast; + +import android.app.Service; +import android.content.Intent; +import android.os.IBinder; + +public class CastRemoteDisplayLocalService extends Service { + @Override + public IBinder onBind(Intent intent) { + return null; + } +} diff --git a/play-services-cast/src/main/java/org/microg/gms/cast/CastApiBuilder.java b/play-services-cast/src/main/java/org/microg/gms/cast/CastApiBuilder.java new file mode 100644 index 00000000..cc40793f --- /dev/null +++ b/play-services-cast/src/main/java/org/microg/gms/cast/CastApiBuilder.java @@ -0,0 +1,34 @@ +/* + * 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.cast; + +import android.content.Context; +import android.os.Looper; + +import com.google.android.gms.cast.Cast; +import com.google.android.gms.common.api.AccountInfo; +import com.google.android.gms.common.api.GoogleApiClient; + +import org.microg.gms.common.api.ApiBuilder; +import org.microg.gms.common.api.ApiConnection; + +public class CastApiBuilder implements ApiBuilder{ + @Override + public ApiConnection build(Context context, Looper looper, Cast.CastOptions options, AccountInfo accountInfo, GoogleApiClient.ConnectionCallbacks callbacks, GoogleApiClient.OnConnectionFailedListener connectionFailedListener) { + return new CastClientImpl(context, options, callbacks, connectionFailedListener); + } +} diff --git a/play-services-cast/src/main/java/org/microg/gms/cast/CastApiImpl.java b/play-services-cast/src/main/java/org/microg/gms/cast/CastApiImpl.java new file mode 100644 index 00000000..bb95565e --- /dev/null +++ b/play-services-cast/src/main/java/org/microg/gms/cast/CastApiImpl.java @@ -0,0 +1,134 @@ +/* + * 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.cast; + +import com.google.android.gms.cast.ApplicationMetadata; +import com.google.android.gms.cast.Cast; +import com.google.android.gms.cast.LaunchOptions; +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.PendingResult; +import com.google.android.gms.common.api.Status; + +import java.io.IOException; + +// TODO +public class CastApiImpl implements Cast.CastApi { + @Override + public int getActiveInputState(GoogleApiClient client) { + return 0; + } + + @Override + public ApplicationMetadata getApplicationMetadata(GoogleApiClient client) { + return null; + } + + @Override + public String getApplicationStatus(GoogleApiClient client) { + return null; + } + + @Override + public int getStandbyState(GoogleApiClient client) { + return 0; + } + + @Override + public double getVolume(GoogleApiClient client) { + return 0; + } + + @Override + public boolean isMute(GoogleApiClient client) { + return false; + } + + @Override + public PendingResult joinApplication(GoogleApiClient client) { + return null; + } + + @Override + public PendingResult joinApplication(GoogleApiClient client, String applicationId, String sessionId) { + return null; + } + + @Override + public PendingResult joinApplication(GoogleApiClient client, String applicationId) { + return null; + } + + @Override + public PendingResult launchApplication(GoogleApiClient client, String applicationId, LaunchOptions launchOptions) { + return null; + } + + @Override + public PendingResult launchApplication(GoogleApiClient client, String applicationId) { + return null; + } + + @Override + public PendingResult launchApplication(GoogleApiClient client, String applicationId, boolean relaunchIfRunning) { + return null; + } + + @Override + public PendingResult leaveApplication(GoogleApiClient client) { + return null; + } + + @Override + public void removeMessageReceivedCallbacks(GoogleApiClient client, String namespace) throws IOException { + + } + + @Override + public void requestStatus(GoogleApiClient client) throws IOException { + + } + + @Override + public PendingResult sendMessage(GoogleApiClient client, String namespace, String message) { + return null; + } + + @Override + public void setMessageReceivedCallbacks(GoogleApiClient client, String namespace, Cast.MessageReceivedCallback callbacks) throws IOException { + + } + + @Override + public void setMute(GoogleApiClient client, boolean mute) throws IOException { + + } + + @Override + public void setVolume(GoogleApiClient client, double volume) throws IOException { + + } + + @Override + public PendingResult stopApplication(GoogleApiClient client) { + return null; + } + + @Override + public PendingResult stopApplication(GoogleApiClient client, String sessionId) { + return null; + } +} diff --git a/play-services-cast/src/main/java/org/microg/gms/cast/CastClientImpl.java b/play-services-cast/src/main/java/org/microg/gms/cast/CastClientImpl.java new file mode 100644 index 00000000..1f80aa1f --- /dev/null +++ b/play-services-cast/src/main/java/org/microg/gms/cast/CastClientImpl.java @@ -0,0 +1,29 @@ +/* + * 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.cast; + +import android.content.Context; + +import com.google.android.gms.cast.Cast; +import com.google.android.gms.common.api.GoogleApiClient; + +import org.microg.gms.common.DummyApiConnection; + +public class CastClientImpl extends DummyApiConnection { + public CastClientImpl(Context context, Cast.CastOptions options, GoogleApiClient.ConnectionCallbacks callbacks, GoogleApiClient.OnConnectionFailedListener connectionFailedListener) { + } +} diff --git a/play-services-cast/src/main/java/org/microg/gms/cast/CastRemoteDisplayApiBuilder.java b/play-services-cast/src/main/java/org/microg/gms/cast/CastRemoteDisplayApiBuilder.java new file mode 100644 index 00000000..048afd15 --- /dev/null +++ b/play-services-cast/src/main/java/org/microg/gms/cast/CastRemoteDisplayApiBuilder.java @@ -0,0 +1,35 @@ +/* + * 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.cast; + +import android.content.Context; +import android.os.Looper; + +import com.google.android.gms.cast.CastRemoteDisplay; +import com.google.android.gms.common.api.AccountInfo; +import com.google.android.gms.common.api.GoogleApiClient; + +import org.microg.gms.common.DummyApiConnection; +import org.microg.gms.common.api.ApiBuilder; +import org.microg.gms.common.api.ApiConnection; + +public class CastRemoteDisplayApiBuilder implements ApiBuilder { + @Override + public ApiConnection build(Context context, Looper looper, CastRemoteDisplay.CastRemoteDisplayOptions options, AccountInfo accountInfo, GoogleApiClient.ConnectionCallbacks callbacks, GoogleApiClient.OnConnectionFailedListener connectionFailedListener) { + return new DummyApiConnection(); + } +} diff --git a/play-services-cast/src/main/java/org/microg/gms/cast/CastRemoteDisplayApiImpl.java b/play-services-cast/src/main/java/org/microg/gms/cast/CastRemoteDisplayApiImpl.java new file mode 100644 index 00000000..4f47a90d --- /dev/null +++ b/play-services-cast/src/main/java/org/microg/gms/cast/CastRemoteDisplayApiImpl.java @@ -0,0 +1,34 @@ +/* + * 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.cast; + +import com.google.android.gms.cast.CastRemoteDisplay; +import com.google.android.gms.cast.CastRemoteDisplayApi; +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.PendingResult; + +public class CastRemoteDisplayApiImpl implements CastRemoteDisplayApi { + @Override + public PendingResult startRemoteDisplay(GoogleApiClient apiClient, String applicationId) { + return null; + } + + @Override + public PendingResult stopRemoteDisplay(GoogleApiClient apiClient) { + return null; + } +} diff --git a/play-services-gcm/build.gradle b/play-services-gcm/build.gradle new file mode 100644 index 00000000..fc2881e0 --- /dev/null +++ b/play-services-gcm/build.gradle @@ -0,0 +1,47 @@ +/* + * 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-iid') + // compile project(':play-services-measurement') +} \ No newline at end of file diff --git a/play-services-gcm/gradle.properties b/play-services-gcm/gradle.properties new file mode 100644 index 00000000..2265b126 --- /dev/null +++ b/play-services-gcm/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 GCM Library +POM_DESCRIPTION=The Play Services Library module to access Google Cloud Messaging + +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-gcm/src/main/AndroidManifest.xml b/play-services-gcm/src/main/AndroidManifest.xml new file mode 100644 index 00000000..ca9ea221 --- /dev/null +++ b/play-services-gcm/src/main/AndroidManifest.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + diff --git a/play-services-gcm/src/main/java/com/google/android/gms/gcm/GcmListenerService.java b/play-services-gcm/src/main/java/com/google/android/gms/gcm/GcmListenerService.java new file mode 100644 index 00000000..ccf99acf --- /dev/null +++ b/play-services-gcm/src/main/java/com/google/android/gms/gcm/GcmListenerService.java @@ -0,0 +1,194 @@ +/* + * 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.PendingIntent; +import android.app.Service; +import android.content.Intent; +import android.os.AsyncTask; +import android.os.Bundle; +import android.os.IBinder; +import android.util.Log; + +import org.microg.gms.common.PublicApi; + +import static org.microg.gms.gcm.GcmConstants.ACTION_C2DM_RECEIVE; +import static org.microg.gms.gcm.GcmConstants.ACTION_NOTIFICATION_OPEN; +import static org.microg.gms.gcm.GcmConstants.EXTRA_ERROR; +import static org.microg.gms.gcm.GcmConstants.EXTRA_FROM; +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_PENDING_INTENT; +import static org.microg.gms.gcm.GcmConstants.MESSAGE_TYPE_DELETED_MESSAGE; +import static org.microg.gms.gcm.GcmConstants.MESSAGE_TYPE_GCM; +import static org.microg.gms.gcm.GcmConstants.MESSAGE_TYPE_SEND_ERROR; +import static org.microg.gms.gcm.GcmConstants.MESSAGE_TYPE_SEND_EVENT; + +/** + * Base class for communicating with Google Cloud Messaging. + *

+ * It also provides functionality such as automatically displaying + * notifications when requested by app server. + *

+ * Override base class methods to handle any events required by the application. + * Methods are invoked asynchronously. + *

+ * Include the following in the manifest: + *

+ * 
+ *     
+ *         
+ *     
+ * 
+ */ +@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() { + @Override + protected Void doInBackground(Void... params) { + handleC2dmMessage(intent); + return null; + } + }.execute(); + } else { + Log.w(TAG, "Unknown intent action: " + intent.getAction()); + + } + + return START_REDELIVER_INTENT; + } else { + finishCounter(); + return START_NOT_STICKY; + } + } + + private void handleC2dmMessage(Intent intent) { + try { + String messageType = intent.getStringExtra(EXTRA_MESSAGE_TYPE); + if (messageType == null || MESSAGE_TYPE_GCM.equals(messageType)) { + String from = intent.getStringExtra(EXTRA_FROM); + Bundle data = intent.getExtras(); + data.remove(EXTRA_MESSAGE_TYPE); + data.remove("android.support.content.wakelockid"); // WakefulBroadcastReceiver.EXTRA_WAKE_LOCK_ID + data.remove(EXTRA_FROM); + onMessageReceived(from, data); + } else if (MESSAGE_TYPE_DELETED_MESSAGE.equals(messageType)) { + onDeletedMessages(); + } else if (MESSAGE_TYPE_SEND_EVENT.equals(messageType)) { + onMessageSent(intent.getStringExtra(EXTRA_MESSAGE_ID)); + } else if (MESSAGE_TYPE_SEND_ERROR.equals(messageType)) { + onSendError(intent.getStringExtra(EXTRA_MESSAGE_ID), intent.getStringExtra(EXTRA_ERROR)); + } else { + Log.w(TAG, "Unknown message type: " + messageType); + } + finishCounter(); + } finally { + GcmReceiver.completeWakefulIntent(intent); + } + } + + private void handlePendingNotification(Intent intent) { + PendingIntent pendingIntent = intent.getParcelableExtra(EXTRA_PENDING_INTENT); + if (pendingIntent != null) { + try { + pendingIntent.send(); + } catch (PendingIntent.CanceledException e) { + Log.w(TAG, "Notification cancelled", e); + } + } else { + Log.w(TAG, "Notification was null"); + } + } + + private void finishCounter() { + synchronized (lock) { + this.counter--; + if (counter == 0) { + stopSelfResult(startId); + } + } + } + +} diff --git a/play-services-gcm/src/main/java/com/google/android/gms/gcm/GcmNetworkManager.java b/play-services-gcm/src/main/java/com/google/android/gms/gcm/GcmNetworkManager.java new file mode 100644 index 00000000..a7ee9a89 --- /dev/null +++ b/play-services-gcm/src/main/java/com/google/android/gms/gcm/GcmNetworkManager.java @@ -0,0 +1,244 @@ +/* + * 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.PendingIntent; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.os.Bundle; +import android.text.TextUtils; + +import java.util.List; + +import static org.microg.gms.common.Constants.GMS_PACKAGE_NAME; +import static org.microg.gms.gcm.GcmConstants.ACTION_SCHEDULE; +import static org.microg.gms.gcm.GcmConstants.ACTION_TASK_READY; +import static org.microg.gms.gcm.GcmConstants.EXTRA_COMPONENT; +import static org.microg.gms.gcm.GcmConstants.EXTRA_SCHEDULER_ACTION; +import static org.microg.gms.gcm.GcmConstants.EXTRA_TAG; +import static org.microg.gms.gcm.GcmConstants.SCHEDULER_ACTION_CANCEL; +import static org.microg.gms.gcm.GcmConstants.SCHEDULER_ACTION_CANCEL_ALL; +import static org.microg.gms.gcm.GcmConstants.SCHEDULER_ACTION_SCHEDULE; + +/** + * Class to create apps with robust "send-to-sync", which is the mechanism to sync data with + * servers where new information is available. + *

+ * You can use the API to schedule network-oriented tasks, and let Google Play services batch + * network operations across the system. This greatly simplifies the implementation of common + * patterns, such as waiting for network connectivity, network retries, and backoff. + *

+ * Tasks must be scheduled based on an execution window in time. During this execution window + * the scheduler will use its discretion in picking an optimal execution time, based on network + * availability (whether the device has connectivity), network activity (whether packages are + * actively being transferred). and load (how many other pending tasks are available for + * execution at that point in time). If none of these factors are influential, the + * scheduler will always wait until the end of the specified window. + *

+ * To receive the notification from the scheduler that a task is ready to be executed, your + * client app must implement a {@link com.google.android.gms.gcm.GcmTaskService} and filter + * on the action {@link com.google.android.gms.gcm.GcmTaskService#SERVICE_ACTION_EXECUTE_TASK}. + *

+ * Note that tags of arbitrary length are not allowed; if the tag you + * provide is greater than 100 characters an exception will be thrown when you try to create your + * {@link com.google.android.gms.gcm.Task} object. + *

+ * The service should be protected by the permission + * com.google.android.gms.permission.BIND_NETWORK_TASK_SERVICE which is used by Google Play + * Services. This prevents other code from invoking the broadcast receiver. Here is an excerpt from + * a sample manifest: + *

+ * 
+ *     
+ *        
+ *     
+ * 
+ * 
+ * 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 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 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 serviceResolves = pm.queryIntentServices(taskIntent, 0); + if (serviceResolves == null || serviceResolves.isEmpty()) + throw new IllegalArgumentException("No service found"); + for (ResolveInfo info : serviceResolves) { + if (serviceName.equals(info.serviceInfo.name)) return; + } + throw new IllegalArgumentException("Service not supported."); + } +} diff --git a/play-services-gcm/src/main/java/com/google/android/gms/gcm/GcmPubSub.java b/play-services-gcm/src/main/java/com/google/android/gms/gcm/GcmPubSub.java new file mode 100644 index 00000000..07c9f4a5 --- /dev/null +++ b/play-services-gcm/src/main/java/com/google/android/gms/gcm/GcmPubSub.java @@ -0,0 +1,121 @@ +/* + * 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.os.Bundle; +import android.text.TextUtils; + +import com.google.android.gms.iid.InstanceID; + +import java.io.IOException; +import java.util.regex.Pattern; + +import static org.microg.gms.gcm.GcmConstants.EXTRA_TOPIC; + +/** + * GcmPubSub provides a publish-subscribe model for sending GCM topic messages. + *

+ * An app can subscribe to different topics defined by the + * developer. The app server can then send messages to the subscribed devices + * without having to maintain topic-subscribers mapping. Topics do not + * need to be explicitly created before subscribing or publishing—they + * are automatically created when publishing or subscribing. + *

+ * 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)} or + * {@link com.google.android.gms.gcm.GcmNetworkManager#cancelAllTasks(java.lang.Class)} 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 Creator CREATOR = new Creator() { + @Override + public OneoffTask createFromParcel(Parcel source) { + return new OneoffTask(source); + } + + @Override + public OneoffTask[] newArray(int size) { + return new OneoffTask[size]; + } + }; + + public static class Builder extends Task.Builder { + private long windowStart = -1; + private long windowEnd = -1; + + public Builder() { + this.isPersisted = false; + } + + public OneoffTask build() { + return new OneoffTask(this); + } + + /** + * Mandatory setter for creating a one-off task. You specify the earliest point in + * time in the future from which your task might start executing, as well as the + * latest point in time in the future at which your task must have executed. + * + * @param windowStartDelaySeconds Earliest point from which your task is eligible to + * run. + * @param windowEndDelaySeconds Latest point at which your task must be run. + */ + public OneoffTask.Builder setExecutionWindow(long windowStartDelaySeconds, long windowEndDelaySeconds) { + this.windowEnd = windowEndDelaySeconds; + this.windowStart = windowStartDelaySeconds; + return this; + } + + /** + * Optional setter for specifying any extra parameters necessary for the task. + */ + public OneoffTask.Builder setExtras(Bundle extras) { + this.extras = extras; + return this; + } + + /** + * Optional setter to specify whether this task should be persisted across reboots.. + * 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 OneoffTask.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 OneoffTask.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 OneoffTask.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 OneoffTask.Builder setService(Class 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 OneoffTask.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 OneoffTask.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/PendingCallback.java b/play-services-gcm/src/main/java/com/google/android/gms/gcm/PendingCallback.java new file mode 100644 index 00000000..a6527ac9 --- /dev/null +++ b/play-services-gcm/src/main/java/com/google/android/gms/gcm/PendingCallback.java @@ -0,0 +1,59 @@ +/* + * 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.IBinder; +import android.os.Parcel; +import android.os.Parcelable; + +public class PendingCallback implements Parcelable { + private final IBinder binder; + + public PendingCallback(IBinder binder) { + this.binder = binder; + } + + private PendingCallback(Parcel in) { + this.binder = in.readStrongBinder(); + } + + public IBinder getBinder() { + return binder; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeStrongBinder(binder); + } + + public static final Creator CREATOR = new Creator() { + @Override + public PendingCallback createFromParcel(Parcel source) { + return new PendingCallback(source); + } + + @Override + public PendingCallback[] newArray(int size) { + return new PendingCallback[size]; + } + }; +} diff --git a/play-services-gcm/src/main/java/com/google/android/gms/gcm/PeriodicTask.java b/play-services-gcm/src/main/java/com/google/android/gms/gcm/PeriodicTask.java new file mode 100644 index 00000000..37aefe95 --- /dev/null +++ b/play-services-gcm/src/main/java/com/google/android/gms/gcm/PeriodicTask.java @@ -0,0 +1,235 @@ +/* + * 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 periodic task is one that will recur at the specified interval, without needing to be + * rescheduled. + * Schedule a task that will recur until the user calls one of + * {@link com.google.android.gms.gcm.GcmNetworkManager#cancelAllTasks(java.lang.Class)}, or + * {@link com.google.android.gms.gcm.GcmNetworkManager#cancelTask(java.lang.String, java.lang.Class)} with + * an identifying tag. + *

+ * Periodic tasks will not be scheduled if their period is below a certain minimum + * (currently 30 seconds). + */ +@PublicApi +public class PeriodicTask extends com.google.android.gms.gcm.Task { + + protected long mFlexInSeconds; + + protected long mIntervalInSeconds; + + private PeriodicTask(Builder builder) { + super(builder); + this.mIntervalInSeconds = builder.periodInSeconds; + this.mFlexInSeconds = Math.min(builder.flexInSeconds, mIntervalInSeconds); + } + + private PeriodicTask(Parcel source) { + super(source); + mIntervalInSeconds = source.readLong(); + mFlexInSeconds = Math.min(source.readLong(), mIntervalInSeconds); + } + + + /** + * @return The number of seconds before the end of the period returned via + * {@link com.google.android.gms.gcm.PeriodicTask#getPeriod()} that this periodic task can be executed early. + */ + public long getFlex() { + return mFlexInSeconds; + } + + /** + * @return The period for this task. The number of seconds between subsequent executions. + */ + public long getPeriod() { + return mIntervalInSeconds; + } + + /** + * 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("period", this.mIntervalInSeconds); + bundle.putLong("period_flex", this.mFlexInSeconds); + } + + public String toString() { + return super.toString() + " period=" + this.getPeriod() + " flex=" + this.getFlex(); + } + + public void writeToParcel(Parcel parcel, int flags) { + super.writeToParcel(parcel, flags); + parcel.writeLong(this.mIntervalInSeconds); + parcel.writeLong(this.mFlexInSeconds); + } + + public static final Creator CREATOR = new Creator() { + @Override + public PeriodicTask createFromParcel(Parcel source) { + return new PeriodicTask(source); + } + + @Override + public PeriodicTask[] newArray(int size) { + return new PeriodicTask[size]; + } + }; + + public static class Builder extends com.google.android.gms.gcm.Task.Builder { + private long flexInSeconds = -1; + private long periodInSeconds = -1; + + public Builder() { + isPersisted = true; + } + + public PeriodicTask build() { + return new PeriodicTask(this); + } + + /** + * Optional setter for specifying any extra parameters necessary for the task. + */ + public PeriodicTask.Builder setExtras(Bundle extras) { + this.extras = extras; + return this; + } + + /** + * Optional setter for specifying how close to the end of the period set in + * {@link com.google.android.gms.gcm.PeriodicTask.Builder#setPeriod(long)} you are willing to execute. + *

+ * For example, specifying a period of 30 seconds, with a flex value of 10 seconds + * will allow the scheduler to determine the best moment between the 20th and 30th + * second at which to execute your task. + */ + public PeriodicTask.Builder setFlex(long flexInSeconds) { + this.flexInSeconds = flexInSeconds; + return this; + } + + /** + * Mandatory setter for creating a periodic task. This specifies that you would like + * this task to recur at most once every mIntervalInSeconds. + *

+ * 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 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 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 messengerResponseQueue = new LinkedBlockingQueue(); + private final Messenger messenger = new Messenger(new Handler(Looper.getMainLooper()) { + @Override + public void handleMessage(Message msg) { + if (msg == null || !(msg.obj instanceof Intent)) { + // Invalid message -> drop + return; + } + Intent intent = (Intent) msg.obj; + if (ACTION_C2DM_REGISTRATION.equals(intent.getAction())) { + messengerResponseQueue.add(intent); + } + } + }); + + /** + * Due to it's nature of being a monitored reference, Intents can be used to authenticate a package source. + */ + private PendingIntent selfAuthIntent; + private Context context; + + public CloudMessagingRpc(Context context) { + this.context = context; + } + + public static String getGcmPackageName(Context context) { + if (gcmPackageName != null) { + return gcmPackageName; + } + PackageManager packageManager = context.getPackageManager(); + for (ResolveInfo resolveInfo : packageManager.queryIntentServices(new Intent(ACTION_C2DM_REGISTER), 0)) { + if (packageManager.checkPermission(PERMISSION_RECEIVE, resolveInfo.serviceInfo.packageName) == PERMISSION_GRANTED) { + return gcmPackageName = resolveInfo.serviceInfo.packageName; + } + } + try { + ApplicationInfo appInfo = packageManager.getApplicationInfo(GMS_PACKAGE_NAME, 0); + return gcmPackageName = appInfo.packageName; + } catch (PackageManager.NameNotFoundException ignored) { + } + try { + ApplicationInfo appInfo = packageManager.getApplicationInfo(GSF_PACKAGE_NAME, 0); + return gcmPackageName = appInfo.packageName; + } catch (PackageManager.NameNotFoundException ex3) { + return null; + } + } + + public void close() { + // Cancel the authentication + if (selfAuthIntent != null) { + selfAuthIntent.cancel(); + selfAuthIntent = null; + } + } + + private PendingIntent getSelfAuthIntent() { + if (selfAuthIntent == null) { + Intent intent = new Intent(); + intent.setPackage("com.google.example.invalidpackage"); + selfAuthIntent = PendingIntent.getBroadcast(context, 0, intent, 0); + } + return selfAuthIntent; + } + + public Intent sendRegisterMessageBlocking(Bundle extras) throws IOException { + sendRegisterMessage(extras); + Intent resultIntent; + try { + resultIntent = messengerResponseQueue.poll(30, TimeUnit.SECONDS); + } catch (InterruptedException e) { + throw new IOException(e.getMessage()); + } + if (resultIntent == null) { + throw new IOException(ERROR_SERVICE_NOT_AVAILABLE); + } + return resultIntent; + } + + private void sendRegisterMessage(Bundle extras) { + Intent intent = new Intent(ACTION_C2DM_REGISTER); + intent.setPackage(getGcmPackageName(context)); + extras.putString(EXTRA_MESSAGE_ID, "google.rpc" + messageIdCounter.getAndIncrement()); + intent.putExtras(extras); + intent.putExtra(EXTRA_MESSENGER, messenger); + intent.putExtra(EXTRA_APP, getSelfAuthIntent()); + context.startService(intent); + } + + public void sendGcmMessage(Bundle extras) { + Intent intent = new Intent(ACTION_GCM_SEND); + intent.setPackage(GMS_PACKAGE_NAME); + intent.putExtras(extras); + intent.putExtra(EXTRA_APP, getSelfAuthIntent()); + context.sendOrderedBroadcast(intent, PERMISSION_GTALK); + } + + public String handleRegisterMessageResult(Intent resultIntent) throws IOException { + if (resultIntent == null) throw new IOException(InstanceID.ERROR_SERVICE_NOT_AVAILABLE); + String result = resultIntent.getStringExtra(EXTRA_REGISTRATION_ID); + if (result == null) result = resultIntent.getStringExtra(EXTRA_UNREGISTERED); + if (result != null) return result; + result = resultIntent.getStringExtra(EXTRA_ERROR); + throw new IOException(result != null ? result : InstanceID.ERROR_SERVICE_NOT_AVAILABLE); + } +} diff --git a/play-services-iid/build.gradle b/play-services-iid/build.gradle new file mode 100644 index 00000000..c8960415 --- /dev/null +++ b/play-services-iid/build.gradle @@ -0,0 +1,47 @@ +/* + * 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-base') + api project(':play-services-iid-api') +} diff --git a/play-services-iid/gradle.properties b/play-services-iid/gradle.properties new file mode 100644 index 00000000..2e179864 --- /dev/null +++ b/play-services-iid/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 IID Library +POM_DESCRIPTION=The Play Services Library module to access the InstanceID API + +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-iid/src/main/AndroidManifest.xml b/play-services-iid/src/main/AndroidManifest.xml new file mode 100644 index 00000000..67377ff0 --- /dev/null +++ b/play-services-iid/src/main/AndroidManifest.xml @@ -0,0 +1,24 @@ + + + + + + + + + diff --git a/play-services-iid/src/main/java/com/google/android/gms/iid/InstanceID.java b/play-services-iid/src/main/java/com/google/android/gms/iid/InstanceID.java new file mode 100644 index 00000000..8a4dbefa --- /dev/null +++ b/play-services-iid/src/main/java/com/google/android/gms/iid/InstanceID.java @@ -0,0 +1,275 @@ +/* + * 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.iid; + +import android.content.Context; +import android.os.Bundle; +import android.os.Looper; +import android.text.TextUtils; +import android.util.Base64; +import android.util.Log; + +import org.microg.gms.common.PublicApi; +import org.microg.gms.gcm.GcmConstants; +import org.microg.gms.iid.InstanceIdRpc; +import org.microg.gms.iid.InstanceIdStore; + +import java.io.IOException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.HashMap; +import java.util.Map; + +import static org.microg.gms.gcm.GcmConstants.EXTRA_DELETE; +import static org.microg.gms.gcm.GcmConstants.EXTRA_SCOPE; +import static org.microg.gms.gcm.GcmConstants.EXTRA_SENDER; +import static org.microg.gms.gcm.GcmConstants.EXTRA_SUBSCIPTION; +import static org.microg.gms.gcm.GcmConstants.EXTRA_SUBTYPE; + +/** + * Instance ID provides a unique identifier for each app instance and a mechanism + * to authenticate and authorize actions (for example, sending a GCM message). + *

+ * Instance ID is stable but may become invalid, if: + *

+ * If Instance ID has become invalid, the app can call {@link com.google.android.gms.iid.InstanceID#getId()} + * to request a new Instance ID. + * To prove ownership of Instance ID and to allow servers to access data or + * services associated with the app, call {@link com.google.android.gms.iid.InstanceID#getToken(java.lang.String, java.lang.String)}. + */ +@PublicApi +public class InstanceID { + /** + * Error returned when failed requests are retried too often. Use + * exponential backoff when retrying requests + */ + public static final String ERROR_BACKOFF = "RETRY_LATER"; + + /** + * Blocking methods must not be called on the main thread. + */ + public static final String ERROR_MAIN_THREAD = "MAIN_THREAD"; + + /** + * Tokens can't be generated. Only devices with Google Play are supported. + */ + public static final String ERROR_MISSING_INSTANCEID_SERVICE = "MISSING_INSTANCEID_SERVICE"; + + /** + * The device cannot read the response, or there was a server error. + * Application should retry the request later using exponential backoff + * and retry (on each subsequent failure increase delay before retrying). + */ + public static final String ERROR_SERVICE_NOT_AVAILABLE = GcmConstants.ERROR_SERVICE_NOT_AVAILABLE; + + /** + * Timeout waiting for a response. + */ + public static final String ERROR_TIMEOUT = "TIMEOUT"; + + private static final int RSA_KEY_SIZE = 2048; + private static final String TAG = "InstanceID"; + + private static InstanceIdStore storeInstance; + private static InstanceIdRpc rpc; + private static Map instances = new HashMap(); + + private final String subtype; + private KeyPair keyPair; + private long creationTime; + + private InstanceID(String subtype) { + this.subtype = subtype == null ? "" : subtype; + } + + /** + * Resets Instance ID and revokes all tokens. + * + * @throws IOException + */ + public void deleteInstanceID() throws IOException { + deleteToken("*", "*"); + creationTime = 0; + storeInstance.delete(subtype + "|"); + keyPair = null; + } + + /** + * Revokes access to a scope (action) for an entity previously + * authorized by {@link com.google.android.gms.iid.InstanceID#getToken(java.lang.String, java.lang.String)}. + *

+ * Do not call this function on the main thread. + * + * @param authorizedEntity Entity that must no longer have access. + * @param scope Action that entity is no longer authorized to perform. + * @throws IOException if the request fails. + */ + public void deleteToken(String authorizedEntity, String scope) throws IOException { + deleteToken(authorizedEntity, scope, null); + } + + @PublicApi(exclude = true) + public void deleteToken(String authorizedEntity, String scope, Bundle extras) throws IOException { + if (Looper.getMainLooper() == Looper.myLooper()) throw new IOException(ERROR_MAIN_THREAD); + + storeInstance.delete(subtype, authorizedEntity, scope); + + if (extras == null) extras = new Bundle(); + extras.putString(EXTRA_SENDER, authorizedEntity); + extras.putString(EXTRA_SUBSCIPTION, authorizedEntity); + extras.putString(EXTRA_DELETE, "1"); + extras.putString("X-" + EXTRA_DELETE, "1"); + extras.putString(EXTRA_SUBTYPE, TextUtils.isEmpty(subtype) ? authorizedEntity : subtype); + extras.putString("X-" + EXTRA_SUBTYPE, TextUtils.isEmpty(subtype) ? authorizedEntity : subtype); + if (scope != null) extras.putString(EXTRA_SCOPE, scope); + + rpc.handleRegisterMessageResult(rpc.sendRegisterMessageBlocking(extras, getKeyPair())); + } + + /** + * Returns time when instance ID was created. + * + * @return Time when instance ID was created (milliseconds since Epoch). + */ + public long getCreationTime() { + if (creationTime == 0) { + String s = storeInstance.get(subtype, "cre"); + if (s != null) { + creationTime = Long.parseLong(s); + } + } + return creationTime; + } + + /** + * Returns a stable identifier that uniquely identifies the app instance. + * + * @return The identifier for the application instance. + */ + public String getId() { + return sha1KeyPair(getKeyPair()); + } + + /** + * Returns an instance of this class. + * + * @return InstanceID instance. + */ + public static InstanceID getInstance(Context context) { + String subtype = ""; + if (storeInstance == null) { + storeInstance = new InstanceIdStore(context.getApplicationContext()); + rpc = new InstanceIdRpc(context.getApplicationContext()); + } + InstanceID instance = instances.get(subtype); + if (instance == null) { + instance = new InstanceID(subtype); + instances.put(subtype, instance); + } + return instance; + } + + /** + * Returns a token that authorizes an Entity (example: cloud service) to perform + * an action on behalf of the application identified by Instance ID. + *

+ * This is similar to an OAuth2 token except, it applies to the + * application instance instead of a user. + *

+ * Do not call this function on the main thread. + * + * @param authorizedEntity Entity authorized by the token. + * @param scope Action authorized for authorizedEntity. + * @param extras additional parameters specific to each token scope. + * Bundle keys starting with 'GCM.' and 'GOOGLE.' are + * reserved. + * @return a token that can identify and authorize the instance of the + * application on the device. + * @throws IOException if the request fails. + */ + public String getToken(String authorizedEntity, String scope, Bundle extras) throws IOException { + if (Looper.getMainLooper() == Looper.myLooper()) throw new IOException(ERROR_MAIN_THREAD); + + throw new UnsupportedOperationException(); + } + + /** + * Returns a token that authorizes an Entity (example: cloud service) to perform + * an action on behalf of the application identified by Instance ID. + *

+ * This is similar to an OAuth2 token except, it applies to the + * application instance instead of a user. + *

+ * Do not call this function on the main thread. + * + * @param authorizedEntity Entity authorized by the token. + * @param scope Action authorized for authorizedEntity. + * @return a token that can identify and authorize the instance of the + * application on the device. + * @throws IOException if the request fails. + */ + public String getToken(String authorizedEntity, String scope) throws IOException { + return getToken(authorizedEntity, scope, null); + } + + @PublicApi(exclude = true) + public InstanceIdStore getStore() { + return storeInstance; + } + + @PublicApi(exclude = true) + public String requestToken(String authorizedEntity, String scope, Bundle extras) { + throw new UnsupportedOperationException(); + } + + private synchronized KeyPair getKeyPair() { + if (keyPair == null) { + keyPair = storeInstance.getKeyPair(subtype); + if (keyPair == null) { + try { + KeyPairGenerator rsaGenerator = KeyPairGenerator.getInstance("RSA"); + rsaGenerator.initialize(RSA_KEY_SIZE); + keyPair = rsaGenerator.generateKeyPair(); + creationTime = System.currentTimeMillis(); + storeInstance.put(subtype, keyPair, creationTime); + } catch (NoSuchAlgorithmException e) { + Log.w(TAG, e); + } + } + } + return keyPair; + } + + @PublicApi(exclude = true) + public static String sha1KeyPair(KeyPair keyPair) { + try { + byte[] digest = MessageDigest.getInstance("SHA1").digest(keyPair.getPublic().getEncoded()); + digest[0] = (byte) (112 + (0xF & digest[0]) & 0xFF); + return Base64.encodeToString(digest, 0, 8, Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING); + } catch (NoSuchAlgorithmException e) { + Log.w(TAG, e); + return null; + } + } +} \ No newline at end of file diff --git a/play-services-iid/src/main/java/com/google/android/gms/iid/InstanceIDListenerService.java b/play-services-iid/src/main/java/com/google/android/gms/iid/InstanceIDListenerService.java new file mode 100644 index 00000000..a15baa0f --- /dev/null +++ b/play-services-iid/src/main/java/com/google/android/gms/iid/InstanceIDListenerService.java @@ -0,0 +1,138 @@ +/* + * 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.iid; + +import android.app.Service; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.Message; +import android.support.v4.content.WakefulBroadcastReceiver; + +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_GSF_INTENT; + +/** + * Base class to handle Instance ID service notifications on token + * refresh. + *

+ * Any app using Instance ID or GCM must include a class extending + * InstanceIDListenerService and implement {@link com.google.android.gms.iid.InstanceIDListenerService#onTokenRefresh()}. + *

+ * Include the following in the manifest: + *

+ * 
+ *     
+ *         
+ *     
+ * 
+ * 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 blockingResponses = new HashMap(); + + private long nextAttempt; + private int interval; + private Context context; + private PendingIntent selfAuthToken; + private Messenger messenger; + private Messenger myMessenger; + private MessengerCompat messengerCompat; + + public InstanceIdRpc(Context context) { + this.context = context; + } + + public static String getIidPackageName(Context context) { + if (iidPackageName != null) { + return iidPackageName; + } + PackageManager packageManager = context.getPackageManager(); + for (ResolveInfo resolveInfo : packageManager.queryIntentServices(new Intent(ACTION_C2DM_REGISTER), 0)) { + if (packageManager.checkPermission(PERMISSION_RECEIVE, resolveInfo.serviceInfo.packageName) == PERMISSION_GRANTED) { + return iidPackageName = resolveInfo.serviceInfo.packageName; + } + } + try { + ApplicationInfo appInfo = packageManager.getApplicationInfo(GMS_PACKAGE_NAME, 0); + return iidPackageName = appInfo.packageName; + } catch (PackageManager.NameNotFoundException ignored) { + } + try { + ApplicationInfo appInfo = packageManager.getApplicationInfo(GSF_PACKAGE_NAME, 0); + return iidPackageName = appInfo.packageName; + } catch (PackageManager.NameNotFoundException ex3) { + Log.w(TAG, "Both Google Play Services and legacy GSF package are missing"); + return null; + } + } + + private static int getGmsVersionCode(final Context context) { + final PackageManager packageManager = context.getPackageManager(); + try { + return packageManager.getPackageInfo(getIidPackageName(context), 0).versionCode; + } catch (PackageManager.NameNotFoundException ex) { + return -1; + } + } + + private static int getSelfVersionCode(final Context context) { + try { + return context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionCode; + } catch (PackageManager.NameNotFoundException neverHappens) { + return 0; + } + } + + private static String getSelfVersionName(final Context context) { + try { + return context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionName; + } catch (PackageManager.NameNotFoundException neverHappens) { + return null; + } + } + + void initialize() { + if (myMessenger != null) return; + getIidPackageName(context); + myMessenger = new Messenger(new Handler(Looper.getMainLooper()) { + @Override + public void handleMessage(Message msg) { + if (msg == null) { + return; + } + if (msg.obj instanceof Intent) { + Intent intent = (Intent) msg.obj; + intent.setExtrasClassLoader(MessengerCompat.class.getClassLoader()); + if (intent.hasExtra(EXTRA_MESSENGER)) { + Parcelable messengerCandidate = intent.getParcelableExtra(EXTRA_MESSENGER); + if (messengerCandidate instanceof MessengerCompat) { + messengerCompat = (MessengerCompat) messengerCandidate; + } else if (messengerCandidate instanceof Messenger) { + messenger = (Messenger) messengerCandidate; + } + } + handleResponseInternal(intent); + } else { + Log.w(TAG, "Dropping invalid message"); + } + } + }); + } + + public void handleResponseInternal(Intent resultIntent) { + if (resultIntent == null) return; + if (!ACTION_C2DM_REGISTRATION.equals(resultIntent.getAction()) && !ACTION_INSTANCE_ID.equals(resultIntent.getAction())) + return; + String result = resultIntent.getStringExtra(EXTRA_REGISTRATION_ID); + if (result == null) result = resultIntent.getStringExtra(EXTRA_UNREGISTERED); + if (result == null) { + handleError(resultIntent); + return; + } + retryCount = 0; + nextAttempt = 0; + interval = 0; + + String requestId = null; + if (result.startsWith("|")) { + // parse structured response + String[] split = result.split("\\|"); + if (!"ID".equals(split[1])) { + Log.w(TAG, "Unexpected structured response " + result); + } + requestId = split[2]; + if (split.length > 4) { + if ("SYNC".equals(split[3])) { + // TODO: sync + } else if("RST".equals(split[3])) { + // TODO: rst + resultIntent.removeExtra(EXTRA_REGISTRATION_ID); + return; + } + } + result = split[split.length-1]; + if (result.startsWith(":")) + result = result.substring(1); + resultIntent.putExtra(EXTRA_REGISTRATION_ID, result); + } + setResponse(requestId, resultIntent); + } + + private void handleError(Intent resultIntent) { + String error = resultIntent.getStringExtra("error"); + if (error == null) return; + String requestId = null; + if (error.startsWith("|")) { + // parse structured error message + String[] split = error.split("\\|"); + if (!"ID".equals(split[1])) { + Log.w(TAG, "Unexpected structured response " + error); + } + if (split.length > 2) { + requestId = split[2]; + error = split[3]; + if (error.startsWith(":")) + error = error.substring(1); + } else { + error = "UNKNOWN"; + } + resultIntent.putExtra("error", error); + } + setResponse(requestId, resultIntent); + long retryAfter = resultIntent.getLongExtra("Retry-After", 0); + if (retryAfter > 0) { + interval = (int) (retryAfter * 1000); + nextAttempt = SystemClock.elapsedRealtime() + interval; + Log.d(TAG, "Server requested retry delay: " + interval); + } else if (ERROR_SERVICE_NOT_AVAILABLE.equals(error) || "AUTHENTICATION_FAILED".equals(error) + && GSF_PACKAGE_NAME.equals(getIidPackageName(context))) { + retryCount++; + if (retryCount < 3) return; + if (retryCount == 3) interval = 1000 + new Random().nextInt(1000); + interval = interval * 2; + nextAttempt = SystemClock.elapsedRealtime() + interval; + Log.d(TAG, "Setting retry delay to " + interval); + } + } + + private synchronized PendingIntent getSelfAuthToken() { + if (selfAuthToken == null) { + Intent intent = new Intent(); + intent.setPackage("com.google.example.invalidpackage"); + selfAuthToken = PendingIntent.getBroadcast(context, 0, intent, 0); + } + return selfAuthToken; + } + + private static synchronized String getRequestId() { + return Integer.toString(lastRequestId++); + } + + private void sendRegisterMessage(Bundle data, KeyPair keyPair, String requestId) throws IOException { + long elapsedRealtime = SystemClock.elapsedRealtime(); + if (nextAttempt != 0 && elapsedRealtime <= nextAttempt) { + Log.w(TAG, "Had to wait for " + interval + ", that's still " + (nextAttempt - elapsedRealtime)); + throw new IOException(ERROR_BACKOFF); + } + initialize(); + if (iidPackageName == null) { + throw new IOException(ERROR_MISSING_INSTANCEID_SERVICE); + } + Intent intent = new Intent(ACTION_C2DM_REGISTER); + intent.setPackage(iidPackageName); + data.putString(EXTRA_GMS_VERSION, Integer.toString(getGmsVersionCode(context))); + data.putString(EXTRA_OS_VERSION, Integer.toString(Build.VERSION.SDK_INT)); + data.putString(EXTRA_APP_VERSION_CODE, Integer.toString(getSelfVersionCode(context))); + data.putString(EXTRA_APP_VERSION_NAME, getSelfVersionName(context)); + data.putString(EXTRA_CLIENT_VERSION, "iid-" + MAX_REFERENCE_VERSION); + data.putString(EXTRA_APP_ID, InstanceID.sha1KeyPair(keyPair)); + String pub = base64encode(keyPair.getPublic().getEncoded()); + data.putString(EXTRA_PUBLIC_KEY, pub); + data.putString(EXTRA_SIGNATURE, sign(keyPair, context.getPackageName(), pub)); + intent.putExtras(data); + intent.putExtra(EXTRA_APP, getSelfAuthToken()); + sendRequest(intent, requestId); + } + + private static String sign(KeyPair keyPair, String... payload) { + byte[] bytes; + try { + bytes = TextUtils.join("\n", payload).getBytes("UTF-8"); + } catch (UnsupportedEncodingException e) { + Log.e(TAG, "Unable to encode", e); + return null; + } + PrivateKey privateKey = keyPair.getPrivate(); + try { + Signature signature = Signature.getInstance(privateKey instanceof RSAPrivateKey ? "SHA256withRSA" : "SHA256withECDSA"); + signature.initSign(privateKey); + signature.update(bytes); + return base64encode(signature.sign()); + } catch (GeneralSecurityException e) { + Log.e(TAG, "Unable to sign", e); + return null; + } + } + + private static String base64encode(byte[] bytes) { + return Base64.encodeToString(bytes, Base64.URL_SAFE + Base64.NO_PADDING + Base64.NO_WRAP); + } + + private void sendRequest(Intent intent, String requestId) { + intent.putExtra(EXTRA_KID, "|ID|" + requestId + "|"); + intent.putExtra("X-" + EXTRA_KID, "|ID|" + requestId + "|"); + Log.d(TAG, "Sending " + intent.getExtras()); + if (messenger != null) { + intent.putExtra(EXTRA_MESSENGER, myMessenger); + Message msg = Message.obtain(); + msg.obj = intent; + try { + messenger.send(msg); + return; + } catch (RemoteException e) { + Log.d(TAG, "Messenger failed, falling back to service"); + } + } + + boolean useGsf = iidPackageName.endsWith(".gsf"); + if (intent.hasExtra(EXTRA_USE_GSF)) + useGsf = "1".equals(intent.getStringExtra(EXTRA_USE_GSF)); + + if (useGsf) { + Intent holder = new Intent(ACTION_INSTANCE_ID); + holder.setPackage(context.getPackageName()); + holder.putExtra(EXTRA_GSF_INTENT, intent); + context.startService(holder); + } else { + intent.putExtra(EXTRA_MESSENGER, myMessenger); + intent.putExtra(EXTRA_IS_MESSENGER2, "1"); + if (messengerCompat != null) { + Message msg = Message.obtain(); + msg.obj = intent; + try { + messengerCompat.send(msg); + return; + } catch (RemoteException e) { + Log.d(TAG, "Messenger failed, falling back to service"); + } + } + context.startService(intent); + } + } + + public Intent sendRegisterMessageBlocking(Bundle data, KeyPair keyPair) throws IOException { + Intent intent = sendRegisterMessageBlockingInternal(data, keyPair); + if (intent != null && intent.hasExtra(EXTRA_MESSENGER)) { + // Now with a messenger + intent = sendRegisterMessageBlockingInternal(data, keyPair); + } + return intent; + } + + private Intent sendRegisterMessageBlockingInternal(Bundle data, KeyPair keyPair) throws IOException { + ConditionVariable cv = new ConditionVariable(); + String requestId = getRequestId(); + synchronized (InstanceIdRpc.class) { + blockingResponses.put(requestId, cv); + } + + sendRegisterMessage(data, keyPair, requestId); + + cv.block(BLOCKING_WAIT_TIME); + synchronized (InstanceIdRpc.class) { + Object res = blockingResponses.remove(requestId); + if (res instanceof Intent) { + return (Intent) res; + } else if (res instanceof String) { + throw new IOException((String) res); + } + Log.w(TAG, "No response " + res); + throw new IOException(ERROR_TIMEOUT); + } + } + + public String handleRegisterMessageResult(Intent resultIntent) throws IOException { + if (resultIntent == null) throw new IOException(ERROR_SERVICE_NOT_AVAILABLE); + String result = resultIntent.getStringExtra(EXTRA_REGISTRATION_ID); + if (result == null) result = resultIntent.getStringExtra(EXTRA_UNREGISTERED); + if (result != null) return result; + result = resultIntent.getStringExtra(EXTRA_ERROR); + throw new IOException(result != null ? result : ERROR_SERVICE_NOT_AVAILABLE); + } + + private void setResponse(String requestId, Object response) { + if (requestId == null) { + for (String r : blockingResponses.keySet()) { + setResponse(r, response); + } + } + Object old = blockingResponses.get(requestId); + blockingResponses.put(requestId, response); + if (old instanceof ConditionVariable) { + ((ConditionVariable) old).open(); + } else if (old instanceof Messenger) { + Message msg = Message.obtain(); + msg.obj = response; + try { + ((Messenger) old).send(msg); + } catch (RemoteException e) { + Log.w(TAG, "Failed to send response", e); + } + } + } +} diff --git a/play-services-iid/src/main/java/org/microg/gms/iid/InstanceIdStore.java b/play-services-iid/src/main/java/org/microg/gms/iid/InstanceIdStore.java new file mode 100644 index 00000000..65778936 --- /dev/null +++ b/play-services-iid/src/main/java/org/microg/gms/iid/InstanceIdStore.java @@ -0,0 +1,111 @@ +/* + * 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.content.Context; +import android.content.SharedPreferences; +import android.util.Base64; +import android.util.Log; + +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; + +public class InstanceIdStore { + private static final String TAG = "InstanceID/Store"; + private static final String PREF_NAME = "com.google.android.gms.appid"; + + private Context context; + private SharedPreferences sharedPreferences; + + public InstanceIdStore(Context context) { + this.context = context; + this.sharedPreferences = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); + } + + public synchronized String get(String key) { + return sharedPreferences.getString(key, null); + } + + public String get(String subtype, String key) { + return get(subtype + "|S|" + key); + } + + public String get(String subtype, String authorizedEntity, String scope) { + return get(subtype + "|T|" + authorizedEntity + "|" + scope); + } + + public KeyPair getKeyPair(String subtype) { + String pub = get(subtype, "|P|"); + String priv = get(subtype, "|K|"); + if (pub == null || priv == null) { + return null; + } + try { + byte[] pubKey = Base64.decode(pub, Base64.URL_SAFE); + byte[] privKey = Base64.decode(priv, Base64.URL_SAFE); + KeyFactory rsaFactory = KeyFactory.getInstance("RSA"); + return new KeyPair(rsaFactory.generatePublic(new X509EncodedKeySpec(pubKey)), rsaFactory.generatePrivate(new PKCS8EncodedKeySpec(privKey))); + } catch (Exception e) { + Log.w(TAG, "Invalid key stored " + e); + return null; + } + } + + public synchronized void put(String key, String value) { + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putString(key, value); + editor.apply(); + } + + public void put(String subtype, String key, String value) { + put(subtype + "|S|" + key, value); + } + + public void put(String subtype, String authorizedEntity, String scope, String value) { + put(subtype + "|T|" + authorizedEntity + "|" + scope, value); + } + + public synchronized void put(String subtype, KeyPair keyPair, long timestamp) { + put(subtype, "|P|", Base64.encodeToString(keyPair.getPublic().getEncoded(), Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING)); + put(subtype, "|K|", Base64.encodeToString(keyPair.getPrivate().getEncoded(), Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING)); + put(subtype, "cre", Long.toString(timestamp)); + } + + public synchronized void delete() { + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.clear(); + editor.apply(); + } + + public synchronized void delete(String prefix) { + SharedPreferences.Editor editor = sharedPreferences.edit(); + for (String key : sharedPreferences.getAll().keySet()) { + if (key.startsWith(prefix)) { + editor.remove(key); + } + } + editor.apply(); + } + + public synchronized void delete(String subtype, String authorizedEntity, String scope) { + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.remove(subtype + "|T|" + authorizedEntity + "|" + scope); + editor.apply(); + } +} diff --git a/play-services-location/build.gradle b/play-services-location/build.gradle new file mode 100644 index 00000000..a19126df --- /dev/null +++ b/play-services-location/build.gradle @@ -0,0 +1,47 @@ +/* + * 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-base') + api project(':play-services-location-api') +} \ No newline at end of file diff --git a/play-services-location/gradle.properties b/play-services-location/gradle.properties new file mode 100644 index 00000000..bfc9b4b2 --- /dev/null +++ b/play-services-location/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 Cast Library +POM_DESCRIPTION=The Play Services Library module to access Google Location Services + +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-location/src/main/AndroidManifest.xml b/play-services-location/src/main/AndroidManifest.xml new file mode 100644 index 00000000..79e10fc2 --- /dev/null +++ b/play-services-location/src/main/AndroidManifest.xml @@ -0,0 +1,24 @@ + + + + + + + + + diff --git a/play-services-location/src/main/java/com/google/android/gms/location/ActivityRecognition.java b/play-services-location/src/main/java/com/google/android/gms/location/ActivityRecognition.java new file mode 100644 index 00000000..ef9d76c3 --- /dev/null +++ b/play-services-location/src/main/java/com/google/android/gms/location/ActivityRecognition.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 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.location; + +import com.google.android.gms.common.api.Api; +import com.google.android.gms.common.api.GoogleApiClient.Builder; + +import org.microg.gms.location.ActivityRecognitionApiBuilder; +import org.microg.gms.location.ActivityRecognitionApiImpl; + +/** + * The main entry point for activity recognition integration. + */ +public class ActivityRecognition { + public static final String CLIENT_NAME = "activity_recognition"; + + /** + * Token to pass to {@link Builder#addApi(Api)} to enable ContextServices. + */ + public static final Api API = new Api(new ActivityRecognitionApiBuilder()); + + /** + * Entry point to the activity recognition APIs. + */ + public static final ActivityRecognitionApi ActivityRecognitionApi = new ActivityRecognitionApiImpl(); +} diff --git a/play-services-location/src/main/java/com/google/android/gms/location/ActivityRecognitionApi.java b/play-services-location/src/main/java/com/google/android/gms/location/ActivityRecognitionApi.java new file mode 100644 index 00000000..08bc50b9 --- /dev/null +++ b/play-services-location/src/main/java/com/google/android/gms/location/ActivityRecognitionApi.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 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.location; + +import android.app.PendingIntent; +import android.os.Bundle; + +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.GoogleApiClient.ConnectionCallbacks; +import com.google.android.gms.common.api.PendingResult; +import com.google.android.gms.common.api.Status; + +/** + * The main entry point for interacting with activity recognition. + *

+ * 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 removeActivityUpdates(GoogleApiClient client, PendingIntent callbackIntent); + + /** + * Register for activity recognition updates. + *

+ * 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 requestActivityUpdates(GoogleApiClient client, long detectionIntervalMillis, PendingIntent callbackIntent); +} diff --git a/play-services-location/src/main/java/com/google/android/gms/location/FusedLocationProviderApi.java b/play-services-location/src/main/java/com/google/android/gms/location/FusedLocationProviderApi.java new file mode 100644 index 00000000..768111b5 --- /dev/null +++ b/play-services-location/src/main/java/com/google/android/gms/location/FusedLocationProviderApi.java @@ -0,0 +1,53 @@ +/* + * 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.location; + +import android.app.PendingIntent; +import android.location.Location; +import android.os.Looper; + +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.PendingResult; +import com.google.android.gms.common.api.Status; + +import org.microg.gms.location.LocationConstants; + +public interface FusedLocationProviderApi { + @Deprecated + String KEY_LOCATION_CHANGED = "com.google.android.location.LOCATION"; + String KEY_MOCK_LOCATION = LocationConstants.KEY_MOCK_LOCATION; + + Location getLastLocation(GoogleApiClient client); + + PendingResult requestLocationUpdates(GoogleApiClient client, LocationRequest request, + LocationListener listener); + + PendingResult requestLocationUpdates(GoogleApiClient client, LocationRequest request, + LocationListener listener, Looper looper); + + PendingResult requestLocationUpdates(GoogleApiClient client, LocationRequest request, + PendingIntent callbackIntent); + + PendingResult removeLocationUpdates(GoogleApiClient client, LocationListener listener); + + PendingResult removeLocationUpdates(GoogleApiClient client, + PendingIntent callbackIntent); + + PendingResult setMockMode(GoogleApiClient client, boolean isMockMode); + + PendingResult setMockLocation(GoogleApiClient client, Location mockLocation); +} diff --git a/play-services-location/src/main/java/com/google/android/gms/location/GeofencingApi.java b/play-services-location/src/main/java/com/google/android/gms/location/GeofencingApi.java new file mode 100644 index 00000000..784c5c3e --- /dev/null +++ b/play-services-location/src/main/java/com/google/android/gms/location/GeofencingApi.java @@ -0,0 +1,48 @@ +/* + * 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.location; + +import android.app.PendingIntent; + +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.PendingResult; +import com.google.android.gms.common.api.Status; + +import java.util.List; + +/** + * The main entry point for interacting with the geofencing APIs. + *

+ * The methods must be used in conjunction with a GoogleApiClient. E.g. + *

+ *  new GoogleApiClient.Builder(context)
+ *          .addApi(LocationServices.API)
+ *          .addConnectionCallbacks(this)
+ *          .addOnConnectionFailedListener(this)
+ *          .build()
+ * 
+ */ +public interface GeofencingApi { + PendingResult addGeofences(GoogleApiClient client, GeofencingRequest geofencingRequest, PendingIntent pendingIntent); + + @Deprecated + PendingResult addGeofences(GoogleApiClient client, List geofences, PendingIntent pendingIntent); + + PendingResult removeGeofences(GoogleApiClient client, List geofenceRequestIds); + + PendingResult removeGeofences(GoogleApiClient client, PendingIntent pendingIntent); +} diff --git a/play-services-location/src/main/java/com/google/android/gms/location/LocationClient.java b/play-services-location/src/main/java/com/google/android/gms/location/LocationClient.java new file mode 100644 index 00000000..47ec9568 --- /dev/null +++ b/play-services-location/src/main/java/com/google/android/gms/location/LocationClient.java @@ -0,0 +1,95 @@ +/* + * 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.location; + +import android.app.PendingIntent; +import android.content.Context; +import android.location.Location; +import android.os.Looper; + +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.PendingResult; + +import org.microg.gms.common.ForwardConnectionCallbacks; +import org.microg.gms.common.ForwardConnectionFailedListener; +import org.microg.gms.common.api.AbstractPlayServicesClient; + +/** + * This class is deprecated as of play services 6.5, do not use it in production systems, + * it's just a forwarder for the {@link FusedLocationProviderApi}. + */ +@Deprecated +public class LocationClient extends AbstractPlayServicesClient { + public static final String KEY_LOCATION_CHANGED = "com.google.android.location.LOCATION"; + + public LocationClient(Context context, ConnectionCallbacks callbacks, + OnConnectionFailedListener connectionFailedListener) { + super(new GoogleApiClient.Builder(context) + .addApi(LocationServices.API) + .addConnectionCallbacks(new ForwardConnectionCallbacks(callbacks)) + .addOnConnectionFailedListener(new ForwardConnectionFailedListener + (connectionFailedListener)) + .build()); + } + + public Location getLastLocation() { + assertConnected(); + return LocationServices.FusedLocationApi.getLastLocation(googleApiClient); + } + + public void requestLocationUpdates(LocationRequest request, + LocationListener listener) { + assertConnected(); + LocationServices.FusedLocationApi.requestLocationUpdates(googleApiClient, request, + listener).await(); + } + + public void requestLocationUpdates(LocationRequest request, + LocationListener listener, Looper looper) { + assertConnected(); + LocationServices.FusedLocationApi.requestLocationUpdates(googleApiClient, request, + listener, looper).await(); + } + + public void requestLocationUpdates(LocationRequest request, + PendingIntent callbackIntent) { + assertConnected(); + LocationServices.FusedLocationApi.requestLocationUpdates(googleApiClient, request, + callbackIntent).await(); + } + + public void removeLocationUpdates(LocationListener listener) { + assertConnected(); + LocationServices.FusedLocationApi.removeLocationUpdates(googleApiClient, listener).await(); + } + + public void removeLocationUpdates(PendingIntent callbackIntent) { + assertConnected(); + LocationServices.FusedLocationApi.removeLocationUpdates(googleApiClient, + callbackIntent).await(); + } + + public void setMockMode(boolean isMockMode) { + assertConnected(); + LocationServices.FusedLocationApi.setMockMode(googleApiClient, isMockMode).await(); + } + + public void setMockLocation(Location mockLocation) { + assertConnected(); + LocationServices.FusedLocationApi.setMockLocation(googleApiClient, mockLocation).await(); + } +} diff --git a/play-services-location/src/main/java/com/google/android/gms/location/LocationListener.java b/play-services-location/src/main/java/com/google/android/gms/location/LocationListener.java new file mode 100644 index 00000000..59c9585c --- /dev/null +++ b/play-services-location/src/main/java/com/google/android/gms/location/LocationListener.java @@ -0,0 +1,34 @@ +/* + * 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.location; + +import android.location.Location; + +/** + * Used for receiving notifications from the {@link FusedLocationProviderApi} when the location has + * changed. The methods are called if the LocationListener has been registered with the location + * client. + */ +public interface LocationListener { + + /** + * Called when the location has changed. + * + * @param location The updated location. + */ + public void onLocationChanged(Location location); +} diff --git a/play-services-location/src/main/java/com/google/android/gms/location/LocationServices.java b/play-services-location/src/main/java/com/google/android/gms/location/LocationServices.java new file mode 100644 index 00000000..839550f4 --- /dev/null +++ b/play-services-location/src/main/java/com/google/android/gms/location/LocationServices.java @@ -0,0 +1,50 @@ +/* + * 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.location; + +import com.google.android.gms.common.api.Api; +import com.google.android.gms.common.api.GoogleApiClient.Builder; + +import org.microg.gms.location.FusedLocationProviderApiImpl; +import org.microg.gms.location.GeofencingApiImpl; +import org.microg.gms.location.LocationServicesApiBuilder; +import org.microg.gms.location.SettingsApiImpl; + +/** + * The main entry point for location services integration. + */ +public class LocationServices { + /** + * Token to pass to {@link Builder#addApi(Api)} to enable LocationServices. + */ + public static final Api API = new Api(new LocationServicesApiBuilder()); + + /** + * Entry point to the fused location APIs. + */ + public static final FusedLocationProviderApi FusedLocationApi = new FusedLocationProviderApiImpl(); + + /** + * Entry point to the geofencing APIs. + */ + public static final GeofencingApi GeofencingApi = new GeofencingApiImpl(); + + /** + * Entry point to the location settings-enabler dialog APIs. + */ + public static final SettingsApi SettingsApi = new SettingsApiImpl(); +} diff --git a/play-services-location/src/main/java/com/google/android/gms/location/LocationStatusCodes.java b/play-services-location/src/main/java/com/google/android/gms/location/LocationStatusCodes.java new file mode 100644 index 00000000..4e3f7d54 --- /dev/null +++ b/play-services-location/src/main/java/com/google/android/gms/location/LocationStatusCodes.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 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.location; + +@Deprecated +public class LocationStatusCodes { + public static final int ERROR = 1; + public static final int GEOFENCE_NOT_AVAILABLE = 1000; + public static final int GEOFENCE_TOO_MANY_GEOFENCES = 1001; + public static final int GEOFENCE_TOO_MANY_PENDING_INTENTS = 1002; + public static final int SUCCESS = 0; +} diff --git a/play-services-location/src/main/java/com/google/android/gms/location/SettingsApi.java b/play-services-location/src/main/java/com/google/android/gms/location/SettingsApi.java new file mode 100644 index 00000000..708d7d02 --- /dev/null +++ b/play-services-location/src/main/java/com/google/android/gms/location/SettingsApi.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 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.location; + +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.PendingResult; + +/** + * The main entry point for interacting with the location settings-enabler APIs. + *

+ * 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 checkLocationSettings(GoogleApiClient client, LocationSettingsRequest locationSettingsRequest); +} diff --git a/play-services-location/src/main/java/org/microg/gms/location/ActivityRecognitionApiBuilder.java b/play-services-location/src/main/java/org/microg/gms/location/ActivityRecognitionApiBuilder.java new file mode 100644 index 00000000..f36ed0c0 --- /dev/null +++ b/play-services-location/src/main/java/org/microg/gms/location/ActivityRecognitionApiBuilder.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 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.location; + +import android.content.Context; +import android.os.Looper; + +import com.google.android.gms.common.api.AccountInfo; +import com.google.android.gms.common.api.Api; +import com.google.android.gms.common.api.GoogleApiClient; + +import org.microg.gms.common.api.ApiBuilder; +import org.microg.gms.common.api.ApiConnection; + +public class ActivityRecognitionApiBuilder implements ApiBuilder { + @Override + public ApiConnection build(Context context, Looper looper, Api.ApiOptions.NoOptions options, AccountInfo accountInfo, GoogleApiClient.ConnectionCallbacks callbacks, GoogleApiClient.OnConnectionFailedListener connectionFailedListener) { + return new ActivityRecognitionClientImpl(context, callbacks, connectionFailedListener); + } +} diff --git a/play-services-location/src/main/java/org/microg/gms/location/ActivityRecognitionApiImpl.java b/play-services-location/src/main/java/org/microg/gms/location/ActivityRecognitionApiImpl.java new file mode 100644 index 00000000..5c09eefd --- /dev/null +++ b/play-services-location/src/main/java/org/microg/gms/location/ActivityRecognitionApiImpl.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 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.location; + +import android.app.PendingIntent; +import android.os.RemoteException; +import android.util.Log; + +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.PendingResult; +import com.google.android.gms.common.api.Status; +import com.google.android.gms.location.ActivityRecognition; +import com.google.android.gms.location.ActivityRecognitionApi; + +import org.microg.gms.common.GmsConnector; + +public class ActivityRecognitionApiImpl implements ActivityRecognitionApi { + private static final String TAG = "GmsActivityApiImpl"; + + @Override + public PendingResult removeActivityUpdates(GoogleApiClient client, final PendingIntent callbackIntent) { + return callVoid(client, new Runnable() { + @Override + public void run(ActivityRecognitionClientImpl client) throws RemoteException { + client.removeActivityUpdates(callbackIntent); + } + }); + } + + @Override + public PendingResult requestActivityUpdates(GoogleApiClient client, final long detectionIntervalMillis, final PendingIntent callbackIntent) { + return callVoid(client, new Runnable() { + @Override + public void run(ActivityRecognitionClientImpl client) throws RemoteException { + client.requestActivityUpdates(detectionIntervalMillis, callbackIntent); + } + }); + } + + private PendingResult callVoid(GoogleApiClient client, final Runnable runnable) { + return GmsConnector.call(client, ActivityRecognition.API, new GmsConnector.Callback() { + @Override + public void onClientAvailable(ActivityRecognitionClientImpl client, ResultProvider resultProvider) throws RemoteException { + runnable.run(client); + resultProvider.onResultAvailable(Status.SUCCESS); + } + }); + } + + private interface Runnable { + void run(ActivityRecognitionClientImpl client) throws RemoteException; + } +} diff --git a/play-services-location/src/main/java/org/microg/gms/location/ActivityRecognitionClientImpl.java b/play-services-location/src/main/java/org/microg/gms/location/ActivityRecognitionClientImpl.java new file mode 100644 index 00000000..cf065fc9 --- /dev/null +++ b/play-services-location/src/main/java/org/microg/gms/location/ActivityRecognitionClientImpl.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 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.location; + +import android.app.PendingIntent; +import android.content.Context; +import android.os.RemoteException; + +import com.google.android.gms.common.api.GoogleApiClient; + +public class ActivityRecognitionClientImpl extends GoogleLocationManagerClient { + public ActivityRecognitionClientImpl(Context context, GoogleApiClient.ConnectionCallbacks callbacks, GoogleApiClient.OnConnectionFailedListener connectionFailedListener) { + super(context, callbacks, connectionFailedListener); + } + + public void requestActivityUpdates(long detectionIntervalMillis, PendingIntent callbackIntent) throws RemoteException { + getServiceInterface().requestActivityUpdates(detectionIntervalMillis, true, callbackIntent); + } + + public void removeActivityUpdates(PendingIntent callbackIntent) throws RemoteException { + getServiceInterface().removeActivityUpdates(callbackIntent); + } +} diff --git a/play-services-location/src/main/java/org/microg/gms/location/FusedLocationProviderApiImpl.java b/play-services-location/src/main/java/org/microg/gms/location/FusedLocationProviderApiImpl.java new file mode 100644 index 00000000..0069974c --- /dev/null +++ b/play-services-location/src/main/java/org/microg/gms/location/FusedLocationProviderApiImpl.java @@ -0,0 +1,140 @@ +/* + * 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.location; + +import android.app.PendingIntent; +import android.location.Location; +import android.os.Looper; +import android.os.RemoteException; +import android.util.Log; + +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.PendingResult; +import com.google.android.gms.common.api.Result; +import com.google.android.gms.common.api.Status; +import com.google.android.gms.location.FusedLocationProviderApi; +import com.google.android.gms.location.LocationListener; +import com.google.android.gms.location.LocationRequest; +import com.google.android.gms.location.LocationServices; + +import org.microg.gms.common.GmsConnector; +import org.microg.gms.common.api.ApiConnection; + +public class FusedLocationProviderApiImpl implements FusedLocationProviderApi { + private static final String TAG = "GmsFusedApiImpl"; + + @Override + public Location getLastLocation(GoogleApiClient client) { + try { + Log.d(TAG, "getLastLocation(" + client + ")"); + return LocationClientImpl.get(client).getLastLocation(); + } catch (RemoteException e) { + Log.w(TAG, e); + return null; + } + } + + @Override + public PendingResult requestLocationUpdates(GoogleApiClient client, + final LocationRequest request, final LocationListener listener) { + return callVoid(client, new Runnable() { + @Override + public void run(LocationClientImpl client) throws RemoteException { + client.requestLocationUpdates(request, listener); + } + }); + } + + @Override + public PendingResult requestLocationUpdates(GoogleApiClient client, + final LocationRequest request, final LocationListener listener, + final Looper looper) { + return callVoid(client, new Runnable() { + @Override + public void run(LocationClientImpl client) throws RemoteException { + client.requestLocationUpdates(request, listener, looper); + } + }); + } + + @Override + public PendingResult requestLocationUpdates(GoogleApiClient client, + final LocationRequest request, final PendingIntent callbackIntent) { + return callVoid(client, new Runnable() { + @Override + public void run(LocationClientImpl client) throws RemoteException { + client.requestLocationUpdates(request, callbackIntent); + } + }); + } + + @Override + public PendingResult removeLocationUpdates(GoogleApiClient client, + final LocationListener listener) { + return callVoid(client, new Runnable() { + @Override + public void run(LocationClientImpl client) throws RemoteException { + client.removeLocationUpdates(listener); + } + }); + } + + @Override + public PendingResult removeLocationUpdates(GoogleApiClient client, + final PendingIntent callbackIntent) { + return callVoid(client, new Runnable() { + @Override + public void run(LocationClientImpl client) throws RemoteException { + client.removeLocationUpdates(callbackIntent); + } + }); + } + + @Override + public PendingResult setMockMode(GoogleApiClient client, final boolean isMockMode) { + return callVoid(client, new Runnable() { + @Override + public void run(LocationClientImpl client) throws RemoteException { + client.setMockMode(isMockMode); + } + }); + } + + @Override + public PendingResult setMockLocation(GoogleApiClient client, final Location mockLocation) { + return callVoid(client, new Runnable() { + @Override + public void run(LocationClientImpl client) throws RemoteException { + client.setMockLocation(mockLocation); + } + }); + } + + private PendingResult callVoid(GoogleApiClient client, final Runnable runnable) { + return GmsConnector.call(client, LocationServices.API, new GmsConnector.Callback() { + @Override + public void onClientAvailable(LocationClientImpl client, ResultProvider resultProvider) throws RemoteException { + runnable.run(client); + resultProvider.onResultAvailable(Status.SUCCESS); + } + }); + } + + private interface Runnable { + void run(LocationClientImpl client) throws RemoteException; + } +} diff --git a/play-services-location/src/main/java/org/microg/gms/location/GeofencingApiImpl.java b/play-services-location/src/main/java/org/microg/gms/location/GeofencingApiImpl.java new file mode 100644 index 00000000..aed9276d --- /dev/null +++ b/play-services-location/src/main/java/org/microg/gms/location/GeofencingApiImpl.java @@ -0,0 +1,116 @@ +/* + * 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.location; + +import android.app.PendingIntent; +import android.os.RemoteException; +import android.support.annotation.NonNull; +import android.util.Log; + +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.PendingResult; +import com.google.android.gms.common.api.Status; +import com.google.android.gms.location.Geofence; +import com.google.android.gms.location.GeofencingApi; +import com.google.android.gms.location.GeofencingRequest; +import com.google.android.gms.location.LocationServices; +import com.google.android.gms.location.internal.IGeofencerCallbacks; +import com.google.android.gms.location.internal.ParcelableGeofence; + +import org.microg.gms.common.GmsConnector; + +import java.util.ArrayList; +import java.util.List; + +public class GeofencingApiImpl implements GeofencingApi { + @Override + public PendingResult addGeofences(GoogleApiClient client, final GeofencingRequest geofencingRequest, final PendingIntent pendingIntent) { + return callGeofencer(client, new Runnable() { + @Override + public void run(LocationClientImpl client, IGeofencerCallbacks callbacks) throws RemoteException { + client.addGeofences(geofencingRequest, pendingIntent, callbacks); + } + }); + } + + @Override + public PendingResult addGeofences(GoogleApiClient client, final List geofences, final PendingIntent pendingIntent) { + final List geofenceList = new ArrayList(); + for (Geofence geofence : geofences) { + if (geofence instanceof ParcelableGeofence) geofenceList.add((ParcelableGeofence) geofence); + } + return callGeofencer(client, new Runnable() { + @Override + public void run(LocationClientImpl client, IGeofencerCallbacks callbacks) throws RemoteException { + client.addGeofences(geofenceList, pendingIntent, callbacks); + } + }); + } + + @Override + public PendingResult removeGeofences(GoogleApiClient client, final List geofenceRequestIds) { + return callGeofencer(client, new Runnable() { + @Override + public void run(LocationClientImpl client, IGeofencerCallbacks callbacks) throws RemoteException { + client.removeGeofences(geofenceRequestIds, callbacks); + } + }); + } + + @Override + public PendingResult removeGeofences(GoogleApiClient client, final PendingIntent pendingIntent) { + return callGeofencer(client, new Runnable() { + @Override + public void run(LocationClientImpl client, IGeofencerCallbacks callbacks) throws RemoteException { + client.removeGeofences(pendingIntent, callbacks); + } + }); + } + + @NonNull + private IGeofencerCallbacks.Stub createGeofencerCallbacks(final GmsConnector.Callback.ResultProvider resultProvider) { + return new IGeofencerCallbacks.Stub(){ + @Override + public void onAddGeofenceResult(int statusCode, String[] requestIds) throws RemoteException { + resultProvider.onResultAvailable(new Status(statusCode)); + } + + @Override + public void onRemoveGeofencesByRequestIdsResult(int statusCode, String[] requestIds) throws RemoteException { + resultProvider.onResultAvailable(new Status(statusCode)); + } + + @Override + public void onRemoveGeofencesByPendingIntentResult(int statusCode, PendingIntent pendingIntent) throws RemoteException { + resultProvider.onResultAvailable(new Status(statusCode)); + } + }; + } + + private PendingResult callGeofencer(GoogleApiClient client, final Runnable runnable) { + return GmsConnector.call(client, LocationServices.API, new GmsConnector.Callback() { + @Override + public void onClientAvailable(LocationClientImpl client, ResultProvider resultProvider) throws RemoteException { + runnable.run(client, createGeofencerCallbacks(resultProvider)); + } + }); + } + + private interface Runnable { + void run(LocationClientImpl client, IGeofencerCallbacks callbacks) throws RemoteException; + } +} diff --git a/play-services-location/src/main/java/org/microg/gms/location/GoogleLocationManagerClient.java b/play-services-location/src/main/java/org/microg/gms/location/GoogleLocationManagerClient.java new file mode 100644 index 00000000..55d0fae9 --- /dev/null +++ b/play-services-location/src/main/java/org/microg/gms/location/GoogleLocationManagerClient.java @@ -0,0 +1,51 @@ +/* + * 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.location; + +import android.content.Context; +import android.os.Bundle; +import android.os.IBinder; +import android.os.RemoteException; + +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.internal.IGmsServiceBroker; +import com.google.android.gms.location.internal.IGoogleLocationManagerService; + +import org.microg.gms.common.Constants; +import org.microg.gms.common.GmsClient; +import org.microg.gms.common.GmsService; + +public abstract class GoogleLocationManagerClient extends GmsClient { + public GoogleLocationManagerClient(Context context, GoogleApiClient.ConnectionCallbacks + callbacks, GoogleApiClient.OnConnectionFailedListener connectionFailedListener) { + super(context, callbacks, connectionFailedListener, GmsService.LOCATION_MANAGER.ACTION); + } + + @Override + protected IGoogleLocationManagerService interfaceFromBinder(IBinder binder) { + return IGoogleLocationManagerService.Stub.asInterface(binder); + } + + @Override + protected void onConnectedToBroker(IGmsServiceBroker broker, GmsCallbacks callbacks) + throws RemoteException { + Bundle bundle = new Bundle(); + bundle.putString("client_name", "locationServices"); + broker.getGoogleLocationManagerService(callbacks, Constants.MAX_REFERENCE_VERSION, + getContext().getPackageName(), bundle); + } +} diff --git a/play-services-location/src/main/java/org/microg/gms/location/LocationClientImpl.java b/play-services-location/src/main/java/org/microg/gms/location/LocationClientImpl.java new file mode 100644 index 00000000..bc161d13 --- /dev/null +++ b/play-services-location/src/main/java/org/microg/gms/location/LocationClientImpl.java @@ -0,0 +1,179 @@ +/* + * 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.location; + +import android.app.PendingIntent; +import android.content.Context; +import android.location.Location; +import android.os.Bundle; +import android.os.Looper; +import android.os.RemoteException; +import android.util.Log; + +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.location.GeofencingRequest; +import com.google.android.gms.location.ILocationListener; +import com.google.android.gms.location.LocationListener; +import com.google.android.gms.location.LocationRequest; +import com.google.android.gms.location.LocationServices; +import com.google.android.gms.location.internal.IGeofencerCallbacks; +import com.google.android.gms.location.internal.ParcelableGeofence; + +import org.microg.gms.common.api.GoogleApiClientImpl; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class LocationClientImpl extends GoogleLocationManagerClient { + private static final String TAG = "GmsLocationClientImpl"; + private NativeLocationClientImpl nativeLocation = null; + private Map listenerMap = new HashMap(); + + + public LocationClientImpl(Context context, GoogleApiClient.ConnectionCallbacks callbacks, + GoogleApiClient.OnConnectionFailedListener connectionFailedListener) { + super(context, callbacks, connectionFailedListener); + Log.d(TAG, ""); + } + + public static LocationClientImpl get(GoogleApiClient apiClient) { + if (apiClient instanceof GoogleApiClientImpl) { + return (LocationClientImpl) ((GoogleApiClientImpl) apiClient) + .getApiConnection(LocationServices.API); + } + return null; + } + + public void addGeofences(GeofencingRequest request, PendingIntent pendingIntent, IGeofencerCallbacks callbacks) throws RemoteException { + if (nativeLocation != null) { + nativeLocation.addGeofences(request, pendingIntent, callbacks); + } else { + getServiceInterface().addGeofences(request, pendingIntent, callbacks); + } + } + + public void addGeofences(List request, PendingIntent pendingIntent, IGeofencerCallbacks callbacks) throws RemoteException { + if (nativeLocation != null) { + nativeLocation.addGeofences(request, pendingIntent, callbacks); + } else { + getServiceInterface().addGeofencesList(request, pendingIntent, callbacks, getContext().getPackageName()); + } + } + + public void removeGeofences(List geofenceRequestIds, IGeofencerCallbacks callbacks) throws RemoteException { + if (nativeLocation != null) { + nativeLocation.removeGeofences(geofenceRequestIds, callbacks); + } else { + getServiceInterface().removeGeofencesById(geofenceRequestIds.toArray(new String[geofenceRequestIds.size()]), callbacks, getContext().getPackageName()); + } + } + + public void removeGeofences(PendingIntent pendingIntent, IGeofencerCallbacks callbacks) throws RemoteException { + if (nativeLocation != null) { + nativeLocation.removeGeofences(pendingIntent, callbacks); + } else { + getServiceInterface().removeGeofencesByIntent(pendingIntent, callbacks, getContext().getPackageName()); + } + } + + public Location getLastLocation() throws RemoteException { + Log.d(TAG, "getLastLocation()"); + if (nativeLocation != null) { + return nativeLocation.getLastLocation(); + } else { + return getServiceInterface().getLastLocation(); + } + } + + public void requestLocationUpdates(LocationRequest request, final LocationListener listener) + throws RemoteException { + if (nativeLocation != null) { + nativeLocation.requestLocationUpdates(request, listener); + } else { + if (!listenerMap.containsKey(listener)) { + listenerMap.put(listener, new ILocationListener.Stub() { + @Override + public void onLocationChanged(Location location) throws RemoteException { + listener.onLocationChanged(location); + } + }); + } + getServiceInterface().requestLocationUpdatesWithPackage(request, + listenerMap.get(listener), getContext().getPackageName()); + } + } + + public void requestLocationUpdates(LocationRequest request, PendingIntent pendingIntent) + throws RemoteException { + if (nativeLocation != null) { + nativeLocation.requestLocationUpdates(request, pendingIntent); + } else { + getServiceInterface().requestLocationUpdatesWithIntent(request, pendingIntent); + } + } + + public void requestLocationUpdates(LocationRequest request, LocationListener listener, + Looper looper) throws RemoteException { + if (nativeLocation != null) { + nativeLocation.requestLocationUpdates(request, listener, looper); + } + requestLocationUpdates(request, listener); // TODO + } + + public void removeLocationUpdates(LocationListener listener) throws RemoteException { + if (nativeLocation != null) { + nativeLocation.removeLocationUpdates(listener); + } else { + getServiceInterface().removeLocationUpdatesWithListener(listenerMap.get(listener)); + } + } + + public void removeLocationUpdates(PendingIntent pendingIntent) throws RemoteException { + if (nativeLocation != null) { + nativeLocation.removeLocationUpdates(pendingIntent); + } else { + getServiceInterface().removeLocationUpdatesWithIntent(pendingIntent); + } + } + + public void setMockMode(boolean isMockMode) throws RemoteException { + if (nativeLocation != null) { + nativeLocation.setMockMode(isMockMode); + } else { + getServiceInterface().setMockMode(isMockMode); + } + } + + public void setMockLocation(Location mockLocation) throws RemoteException { + if (nativeLocation != null) { + nativeLocation.setMockLocation(mockLocation); + } else { + getServiceInterface().setMockLocation(mockLocation); + } + } + + @Override + public void handleConnectionFailed() { + // DO NOT call super here, because fails are not really problems :) + nativeLocation = new NativeLocationClientImpl(this); + state = ConnectionState.PSEUDO_CONNECTED; + Bundle bundle = new Bundle(); + bundle.putBoolean("fallback_to_native_active", true); + callbacks.onConnected(bundle); + } +} diff --git a/play-services-location/src/main/java/org/microg/gms/location/LocationServicesApiBuilder.java b/play-services-location/src/main/java/org/microg/gms/location/LocationServicesApiBuilder.java new file mode 100644 index 00000000..2e053a81 --- /dev/null +++ b/play-services-location/src/main/java/org/microg/gms/location/LocationServicesApiBuilder.java @@ -0,0 +1,37 @@ +/* + * 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.location; + +import android.content.Context; +import android.os.Looper; + +import com.google.android.gms.common.api.AccountInfo; +import com.google.android.gms.common.api.Api; +import com.google.android.gms.common.api.GoogleApiClient; + +import org.microg.gms.common.api.ApiBuilder; +import org.microg.gms.common.api.ApiConnection; + +public class LocationServicesApiBuilder implements ApiBuilder { + @Override + public ApiConnection build(Context context, Looper looper, + Api.ApiOptions.NoOptions options, + AccountInfo accountInfo, GoogleApiClient.ConnectionCallbacks callbacks, + GoogleApiClient.OnConnectionFailedListener connectionFailedListener) { + return new LocationClientImpl(context, callbacks, connectionFailedListener); + } +} diff --git a/play-services-location/src/main/java/org/microg/gms/location/NativeLocationClientImpl.java b/play-services-location/src/main/java/org/microg/gms/location/NativeLocationClientImpl.java new file mode 100644 index 00000000..17a017d9 --- /dev/null +++ b/play-services-location/src/main/java/org/microg/gms/location/NativeLocationClientImpl.java @@ -0,0 +1,264 @@ +/* + * 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.location; + +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.location.Criteria; +import android.location.Location; +import android.location.LocationManager; +import android.os.Bundle; +import android.os.Looper; +import android.os.RemoteException; +import android.os.SystemClock; +import android.util.Log; + +import com.google.android.gms.common.api.CommonStatusCodes; +import com.google.android.gms.location.FusedLocationProviderApi; +import com.google.android.gms.location.Geofence; +import com.google.android.gms.location.GeofenceStatusCodes; +import com.google.android.gms.location.GeofencingEvent; +import com.google.android.gms.location.GeofencingRequest; +import com.google.android.gms.location.LocationListener; +import com.google.android.gms.location.LocationRequest; +import com.google.android.gms.location.internal.IGeofencerCallbacks; +import com.google.android.gms.location.internal.ParcelableGeofence; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static android.location.LocationManager.KEY_LOCATION_CHANGED; +import static android.location.LocationManager.KEY_PROXIMITY_ENTERING; + +@SuppressWarnings("MissingPermission") +public class NativeLocationClientImpl { + private final static String TAG = "GmsToNativeLocClient"; + private final static Criteria DEFAULT_CRITERIA = new Criteria(); + private final static Map pendingCount = new HashMap(); + private final static Map nativePendingMap = new HashMap(); + private static final String EXTRA_PENDING_INTENT = "pending_intent"; + + private final Context context; + private final LocationManager locationManager; + private final Map nativeListenerMap = new HashMap(); + + public NativeLocationClientImpl(LocationClientImpl client) { + context = client.getContext(); + locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE); + } + + private static Criteria makeNativeCriteria(LocationRequest request) { + Criteria criteria = new Criteria(); + switch (request.getPriority()) { + case LocationRequest.PRIORITY_HIGH_ACCURACY: + criteria.setAccuracy(Criteria.ACCURACY_FINE); + criteria.setPowerRequirement(Criteria.POWER_HIGH); + break; + case LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY: + default: + criteria.setAccuracy(Criteria.ACCURACY_COARSE); + criteria.setPowerRequirement(Criteria.POWER_MEDIUM); + break; + case LocationRequest.PRIORITY_NO_POWER: + case LocationRequest.PRIORITY_LOW_POWER: + criteria.setAccuracy(Criteria.ACCURACY_COARSE); + criteria.setPowerRequirement(Criteria.POWER_LOW); + } + return criteria; + } + + public void addGeofences(GeofencingRequest geofencingRequest, PendingIntent pendingIntent, IGeofencerCallbacks callbacks) throws RemoteException { + Log.d(TAG, "addGeofences(GeofencingRequest)"); + callbacks.onAddGeofenceResult(GeofenceStatusCodes.GEOFENCE_NOT_AVAILABLE, new String[0]); + } + + public void addGeofences(List geofences, PendingIntent pendingIntent, IGeofencerCallbacks callbacks) throws RemoteException { + Log.d(TAG, "addGeofences(List)"); + Intent i = new Intent(context, NativePendingIntentForwarder.class); + Bundle bundle = new Bundle(); + bundle.putParcelable(EXTRA_PENDING_INTENT, pendingIntent); + i.putExtras(bundle); + nativePendingMap.put(pendingIntent, PendingIntent.getActivity(context, 0, i, 0)); + List requestIds = new ArrayList(); + for (ParcelableGeofence geofence : geofences) { + locationManager.addProximityAlert(geofence.latitude, geofence.longitude, geofence.radius, + geofence.expirationTime - SystemClock.elapsedRealtime(), nativePendingMap.get(pendingIntent)); + requestIds.add(geofence.getRequestId()); + } + callbacks.onAddGeofenceResult(CommonStatusCodes.SUCCESS, requestIds.toArray(new String[requestIds.size()])); + } + + public void removeGeofences(List requestIds, IGeofencerCallbacks callbacks) throws RemoteException { + Log.d(TAG, "removeGeofences(List)"); + callbacks.onRemoveGeofencesByRequestIdsResult(GeofenceStatusCodes.ERROR, requestIds.toArray(new String[requestIds.size()])); + } + + public void removeGeofences(PendingIntent pendingIntent, IGeofencerCallbacks callbacks) throws RemoteException { + Log.d(TAG, "removeGeofences(PendingIntent)"); + locationManager.removeProximityAlert(nativePendingMap.get(pendingIntent)); + nativePendingMap.remove(pendingIntent); + callbacks.onRemoveGeofencesByPendingIntentResult(CommonStatusCodes.SUCCESS, pendingIntent); + } + + public Location getLastLocation() { + Log.d(TAG, "getLastLocation()"); + return locationManager.getLastKnownLocation(locationManager.getBestProvider(DEFAULT_CRITERIA, true)); + } + + public void requestLocationUpdates(LocationRequest request, LocationListener listener) { + requestLocationUpdates(request, listener, Looper.getMainLooper()); + } + + public void requestLocationUpdates(LocationRequest request, PendingIntent pendingIntent) { + Log.d(TAG, "requestLocationUpdates()"); + Intent i = new Intent(context, NativePendingIntentForwarder.class); + Bundle bundle = new Bundle(); + bundle.putParcelable(EXTRA_PENDING_INTENT, pendingIntent); + i.putExtras(bundle); + pendingCount.put(pendingIntent, request.getNumUpdates()); + nativePendingMap.put(pendingIntent, PendingIntent.getActivity(context, 0, i, 0)); + locationManager.requestLocationUpdates(request.getInterval(), request.getSmallestDesplacement(), + makeNativeCriteria(request), nativePendingMap.get(pendingIntent)); + } + + public void requestLocationUpdates(LocationRequest request, LocationListener listener, Looper + looper) { + Log.d(TAG, "requestLocationUpdates()"); + if (nativeListenerMap.containsKey(listener)) { + removeLocationUpdates(listener); + } + nativeListenerMap.put(listener, new NativeListener(listener, request.getNumUpdates())); + locationManager.requestLocationUpdates(request.getInterval(), + request.getSmallestDesplacement(), makeNativeCriteria(request), + nativeListenerMap.get(listener), looper); + } + + public void removeLocationUpdates(LocationListener listener) { + Log.d(TAG, "removeLocationUpdates()"); + locationManager.removeUpdates(nativeListenerMap.get(listener)); + nativeListenerMap.remove(listener); + } + + public void removeLocationUpdates(PendingIntent pendingIntent) { + Log.d(TAG, "removeLocationUpdates()"); + locationManager.removeUpdates(nativePendingMap.get(pendingIntent)); + nativePendingMap.remove(pendingIntent); + pendingCount.remove(pendingIntent); + } + + public void setMockMode(boolean isMockMode) { + Log.d(TAG, "setMockMode()"); + // not yet supported + } + + public void setMockLocation(Location mockLocation) { + Log.d(TAG, "setMockLocation()"); + // not yet supported + } + + public static class NativePendingIntentForwarder extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + if (intent.hasExtra(KEY_PROXIMITY_ENTERING)) { + PendingIntent pendingIntent = intent.getExtras().getParcelable(EXTRA_PENDING_INTENT); + try { + intent.putExtra(GeofencingEvent.EXTRA_TRANSITION, intent.getBooleanExtra(KEY_PROXIMITY_ENTERING, false) ? Geofence.GEOFENCE_TRANSITION_ENTER : Geofence.GEOFENCE_TRANSITION_EXIT); + pendingIntent.send(context, 0, intent); + } catch (PendingIntent.CanceledException e) { + nativePendingMap.remove(pendingIntent); + } + } else if (intent.hasExtra(KEY_LOCATION_CHANGED)) { + PendingIntent pendingIntent = intent.getExtras().getParcelable(EXTRA_PENDING_INTENT); + try { + intent.putExtra(FusedLocationProviderApi.KEY_LOCATION_CHANGED, + intent.getParcelableExtra(KEY_LOCATION_CHANGED)); + pendingIntent.send(context, 0, intent); + pendingCount.put(pendingIntent, pendingCount.get(pendingIntent) - 1); + if (pendingCount.get(pendingIntent) == 0) { + ((LocationManager) context.getSystemService(Context.LOCATION_SERVICE)) + .removeUpdates(nativePendingMap.get(pendingIntent)); + nativePendingMap.remove(pendingIntent); + pendingCount.remove(pendingIntent); + } + } catch (PendingIntent.CanceledException e) { + ((LocationManager) context.getSystemService(Context.LOCATION_SERVICE)) + .removeUpdates(nativePendingMap.get(pendingIntent)); + nativePendingMap.remove(pendingIntent); + pendingCount.remove(pendingIntent); + } + } + } + } + + public class NativeListener implements android.location.LocationListener { + + private final LocationListener listener; + private int count; + + private NativeListener(LocationListener listener, int count) { + this.listener = listener; + this.count = count; + } + + @Override + public void onLocationChanged(Location location) { + listener.onLocationChanged(location); + count--; + if (count == 0) { + locationManager.removeUpdates(this); + nativeListenerMap.remove(listener); + } + } + + @Override + public void onStatusChanged(String provider, int status, Bundle extras) { + + } + + @Override + public void onProviderEnabled(String provider) { + + } + + @Override + public void onProviderDisabled(String provider) { + + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + NativeListener that = (NativeListener) o; + + if (!listener.equals(that.listener)) return false; + + return true; + } + + @Override + public int hashCode() { + return listener.hashCode(); + } + } +} diff --git a/play-services-location/src/main/java/org/microg/gms/location/SettingsApiImpl.java b/play-services-location/src/main/java/org/microg/gms/location/SettingsApiImpl.java new file mode 100644 index 00000000..b8fe0ffe --- /dev/null +++ b/play-services-location/src/main/java/org/microg/gms/location/SettingsApiImpl.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 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.location; + +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.PendingResult; +import com.google.android.gms.common.api.Status; +import com.google.android.gms.location.LocationSettingsRequest; +import com.google.android.gms.location.LocationSettingsResult; +import com.google.android.gms.location.SettingsApi; + +import org.microg.gms.common.api.InstantPendingResult; + +public class SettingsApiImpl implements SettingsApi { + @Override + public PendingResult checkLocationSettings(GoogleApiClient client, LocationSettingsRequest locationSettingsRequest) { + return new InstantPendingResult(new LocationSettingsResult(Status.CANCELED)); + } +} diff --git a/play-services-tasks/build.gradle b/play-services-tasks/build.gradle new file mode 100644 index 00000000..b71cf599 --- /dev/null +++ b/play-services-tasks/build.gradle @@ -0,0 +1,46 @@ +/* + * 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') +} diff --git a/play-services-tasks/gradle.properties b/play-services-tasks/gradle.properties new file mode 100644 index 00000000..328b6fdd --- /dev/null +++ b/play-services-tasks/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 Tasks +POM_DESCRIPTION=Classes used by some Play Services Library modules to abstract tasks + +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-tasks/src/main/AndroidManifest.xml b/play-services-tasks/src/main/AndroidManifest.xml new file mode 100644 index 00000000..68340930 --- /dev/null +++ b/play-services-tasks/src/main/AndroidManifest.xml @@ -0,0 +1,18 @@ + + + + diff --git a/play-services-tasks/src/main/java/com/google/android/gms/tasks/Continuation.java b/play-services-tasks/src/main/java/com/google/android/gms/tasks/Continuation.java new file mode 100644 index 00000000..26b69a81 --- /dev/null +++ b/play-services-tasks/src/main/java/com/google/android/gms/tasks/Continuation.java @@ -0,0 +1,68 @@ +/* + * 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.tasks; + +import org.microg.gms.common.PublicApi; + +/** + * A function that is called to continue execution after completion of a {@link Task}. + * + * @see Task#continueWith(Continuation) + * @see Task#continueWithTask(Continuation) + */ +@PublicApi +public interface Continuation { + /** + * Returns the result of applying this Continuation to {@code task}. + *

+ * To propagate failure from the completed Task call {@link Task#getResult()} and allow the + * {@link RuntimeExecutionException} to propagate. The RuntimeExecutionException will be + * unwrapped such that the Task returned by {@link Task#continueWith(Continuation)} or + * {@link Task#continueWithTask(Continuation)} fails with the original exception. + *

+ * To suppress specific failures call {@link Task#getResult(Class)} and catch the exception + * types of interest: + *

task.continueWith(new Continuation() {
+     *     @Override
+     *     public String then(Task task) {
+     *         try {
+     *             return task.getResult(IOException.class);
+     *         } catch (FileNotFoundException e) {
+     *             return "Not found";
+     *         } catch (IOException e) {
+     *             return "Read failed";
+     *         }
+     *     }
+     * }
+ *

+ * To suppress all failures guard any calls to {@link Task#getResult()} with {@link Task#isSuccessful()}: + *

task.continueWith(new Continuation() {
+     *     @Override
+     *     public String then(Task task) {
+     *         if (task.isSuccessful()) {
+     *             return task.getResult();
+     *         } else {
+     *             return DEFAULT_VALUE;
+     *         }
+     *     }
+     * }
+ * + * @param task the completed Task. Never null + * @throws Exception if the result couldn't be produced + */ + TContinuationResult then(Task task); +} diff --git a/play-services-tasks/src/main/java/com/google/android/gms/tasks/OnCompleteListener.java b/play-services-tasks/src/main/java/com/google/android/gms/tasks/OnCompleteListener.java new file mode 100644 index 00000000..2035472c --- /dev/null +++ b/play-services-tasks/src/main/java/com/google/android/gms/tasks/OnCompleteListener.java @@ -0,0 +1,34 @@ +/* + * 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.tasks; + +import org.microg.gms.common.PublicApi; + +/** + * Listener called when a {@link Task} completes. + * + * @see Task#addOnCompleteListener(OnCompleteListener) + */ +@PublicApi +public interface OnCompleteListener { + /** + * Called when the Task completes. + * + * @param task the completed Task. Never null + */ + void onComplete(Task task); +} diff --git a/play-services-tasks/src/main/java/com/google/android/gms/tasks/OnFailureListener.java b/play-services-tasks/src/main/java/com/google/android/gms/tasks/OnFailureListener.java new file mode 100644 index 00000000..c6e1124b --- /dev/null +++ b/play-services-tasks/src/main/java/com/google/android/gms/tasks/OnFailureListener.java @@ -0,0 +1,35 @@ +/* + * 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.tasks; + +import org.microg.gms.common.PublicApi; + +/** + * Listener called when a {@link Task} fails with an exception. + * + * @see Task#addOnFailureListener(OnFailureListener) + */ +@PublicApi +public interface OnFailureListener { + + /** + * Called when the Task fails with an exception. + * + * @param e the exception that caused the Task to fail. Never null + */ + void onFailure(Exception e); +} diff --git a/play-services-tasks/src/main/java/com/google/android/gms/tasks/OnSuccessListener.java b/play-services-tasks/src/main/java/com/google/android/gms/tasks/OnSuccessListener.java new file mode 100644 index 00000000..f6e6fbdd --- /dev/null +++ b/play-services-tasks/src/main/java/com/google/android/gms/tasks/OnSuccessListener.java @@ -0,0 +1,34 @@ +/* + * 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.tasks; + +import org.microg.gms.common.PublicApi; + +/** + * Listener called when a {@link Task} completes successfully. + * + * @see Task#addOnSuccessListener(OnSuccessListener) + */ +@PublicApi +public interface OnSuccessListener { + /** + * Called when the {@link Task} completes successfully. + * + * @param result the result of the Task + */ + void onSuccess(TResult result); +} diff --git a/play-services-tasks/src/main/java/com/google/android/gms/tasks/RuntimeExecutionException.java b/play-services-tasks/src/main/java/com/google/android/gms/tasks/RuntimeExecutionException.java new file mode 100644 index 00000000..99482115 --- /dev/null +++ b/play-services-tasks/src/main/java/com/google/android/gms/tasks/RuntimeExecutionException.java @@ -0,0 +1,33 @@ +/* + * 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.tasks; + +import org.microg.gms.common.PublicApi; + +import java.util.concurrent.ExecutionException; + +/** + * Runtime version of {@link ExecutionException}. + * + * @see Task#getResult(Class) + */ +@PublicApi +public class RuntimeExecutionException extends RuntimeException { + public RuntimeExecutionException(Throwable cause) { + super(cause); + } +} diff --git a/play-services-tasks/src/main/java/com/google/android/gms/tasks/Task.java b/play-services-tasks/src/main/java/com/google/android/gms/tasks/Task.java new file mode 100644 index 00000000..b9963173 --- /dev/null +++ b/play-services-tasks/src/main/java/com/google/android/gms/tasks/Task.java @@ -0,0 +1,224 @@ +/* + * 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.tasks; + +import android.app.Activity; + +import org.microg.gms.common.PublicApi; + +import java.util.concurrent.Executor; + +/** + * Represents an asynchronous operation. + */ +@PublicApi +public abstract class Task { + + public Task() { + } + + /** + * Adds a listener that is called when the Task completes. + *

+ * The listener will be called on main application thread. If the Task is already complete, a + * call to the listener will be immediately scheduled. If multiple listeners are added, they + * will be called in the order in which they were added. + * + * @return this Task + */ + public Task addOnCompleteListener(OnCompleteListener listener) { + throw new UnsupportedOperationException("addOnCompleteListener is not implemented"); + } + + /** + * Adds an Activity-scoped listener that is called when the Task completes. + *

+ * The listener will be called on main application thread. If the Task is already complete, a + * call to the listener will be immediately scheduled. If multiple listeners are added, they + * will be called in the order in which they were added. + *

+ * The listener will be automatically removed during {@link Activity#onStop()}. + * + * @return this Task + */ + public Task addOnCompleteListener(Activity activity, OnCompleteListener listener) { + throw new UnsupportedOperationException("addOnCompleteListener is not implemented"); + } + + /** + * Adds a listener that is called when the Task completes. + *

+ * If the Task is already complete, a call to the listener will be immediately scheduled. If + * multiple listeners are added, they will be called in the order in which they were added. + * + * @param executor the executor to use to call the listener + * @return this Task + */ + public Task addOnCompleteListener(Executor executor, OnCompleteListener listener) { + throw new UnsupportedOperationException("addOnCompleteListener is not implemented"); + } + + /** + * Adds an Activity-scoped listener that is called if the Task fails. + *

+ * The listener will be called on main application thread. If the Task has already failed, a + * call to the listener will be immediately scheduled. If multiple listeners are added, they + * will be called in the order in which they were added. + *

+ * The listener will be automatically removed during {@link Activity#onStop()}. + * + * @return this Task + */ + public abstract Task addOnFailureListener(Activity activity, OnFailureListener listener); + + /** + * Adds an Activity-scoped listener that is called if the Task fails. + *

+ * The listener will be called on main application thread. If the Task has already failed, a + * call to the listener will be immediately scheduled. If multiple listeners are added, they + * will be called in the order in which they were added. + * + * @return this Task + */ + public abstract Task addOnFailureListener(OnFailureListener listener); + + /** + * Adds a listener that is called if the Task fails. + *

+ * If the Task has already failed, a call to the listener will be immediately scheduled. If + * multiple listeners are added, they will be called in the order in which they were added. + * + * @param executor the executor to use to call the listener + * @return this Task + */ + public abstract Task addOnFailureListener(Executor executor, OnFailureListener listener); + + + /** + * Adds a listener that is called if the Task completes successfully. + *

+ * If multiple listeners are added, they will be called in the order in which they were added. If + * the Task has already completed successfully, a call to the listener will be immediately scheduled. + * + * @param executor the executor to use to call the listener + * @return this Task + */ + public abstract Task addOnSuccessListener(Executor executor, OnSuccessListener listener); + + /** + * Adds a listener that is called if the Task completes successfully. + *

+ * The listener will be called on the main application thread. If the Task has already + * completed successfully, a call to the listener will be immediately scheduled. If multiple + * listeners are added, they will be called in the order in which they were added. + * + * @return this Task + */ + public abstract Task addOnSuccessListener(OnSuccessListener listener); + + /** + * Adds an Activity-scoped listener that is called if the Task completes successfully. + *

+ * The listener will be called on the main application thread. If the Task has already + * completed successfully, a call to the listener will be immediately scheduled. If multiple + * listeners are added, they will be called in the order in which they were added. + *

+ * The listener will be automatically removed during {@link Activity#onStop()}. + * + * @return this Task + */ + public abstract Task addOnSuccessListener(Activity activity, OnSuccessListener listener); + + + /** + * Returns a new Task that will be completed with the result of applying the specified + * Continuation to this Task. + *

+ * The Continuation will be called on the main application thread. + * + * @see Continuation#then(Task) + */ + public Task continueWith(Continuation continuation) { + throw new UnsupportedOperationException("continueWith is not implemented"); + } + + /** + * Returns a new Task that will be completed with the result of applying the specified Continuation to this Task. + * + * @param executor the executor to use to call the Continuation + * @see Continuation#then(Task) + */ + public Task continueWith(Executor executor, Continuation continuation) { + throw new UnsupportedOperationException("continueWith is not implemented"); + } + + /** + * Returns a new Task that will be completed with the result of applying the specified + * Continuation to this Task. + *

+ * The Continuation will be called on the main application thread. + * + * @see Continuation#then(Task) + */ + public Task continueWithTask(Continuation> continuation) { + throw new UnsupportedOperationException("continueWithTask is not implemented"); + } + + /** + * Returns a new Task that will be completed with the result of applying the specified Continuation to this Task. + * + * @param executor the executor to use to call the Continuation + * @see Continuation#then(Task) + */ + public Task continueWithTask(Executor executor, Continuation> var2) { + throw new UnsupportedOperationException("continueWithTask is not implemented"); + } + + /** + * Returns the exception that caused the Task to fail. Returns {@code null} if the Task is not + * yet complete, or completed successfully. + */ + public abstract Exception getException(); + + /** + * Gets the result of the Task, if it has already completed. + * + * @throws IllegalStateException if the Task is not yet complete + * @throws RuntimeExecutionException if the Task failed with an exception + */ + public abstract TResult getResult(); + + /** + * Gets the result of the Task, if it has already completed. + * + * @throws IllegalStateException if the Task is not yet complete + * @throws X if the Task failed with an exception of type X + * @throws RuntimeExecutionException if the Task failed with an exception that was not of type X + */ + public abstract TResult getResult(Class exceptionType) throws X; + + /** + * Returns {@code true} if the Task is complete; {@code false} otherwise. + */ + public abstract boolean isComplete(); + + /** + * Returns {@code true} if the Task has completed successfully; {@code false} otherwise. + */ + public abstract boolean isSuccessful(); + +} diff --git a/play-services-tasks/src/main/java/com/google/android/gms/tasks/TaskCompletionSource.java b/play-services-tasks/src/main/java/com/google/android/gms/tasks/TaskCompletionSource.java new file mode 100644 index 00000000..32def519 --- /dev/null +++ b/play-services-tasks/src/main/java/com/google/android/gms/tasks/TaskCompletionSource.java @@ -0,0 +1,52 @@ +/* + * 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.tasks; + +import org.microg.gms.common.PublicApi; + +/** + * Provides the ability to create an incomplete {@link Task} and later complete it by either + * calling {@link #setResult(TResult)} or {@link #setException(Exception)}. + */ +@PublicApi +public class TaskCompletionSource { + public TaskCompletionSource() { + } + + /** + * Returns the Task. + */ + public Task getTask() { + return null; + } + + /** + * Completes the Task with the specified exception. + * @throws IllegalStateException if the Task is already complete + */ + public void setException(Exception e) { + + } + + /** + * Completes the Task with the specified result. + * @throws IllegalStateException if the Task is already complete + */ + public void setResult(TResult result) { + + } +} diff --git a/play-services-tasks/src/main/java/com/google/android/gms/tasks/Tasks.java b/play-services-tasks/src/main/java/com/google/android/gms/tasks/Tasks.java new file mode 100644 index 00000000..41f31c7b --- /dev/null +++ b/play-services-tasks/src/main/java/com/google/android/gms/tasks/Tasks.java @@ -0,0 +1,116 @@ +/* + * 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.tasks; + +import org.microg.gms.common.PublicApi; + +import java.util.Arrays; +import java.util.Collection; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +/** + * {@link Task} utility methods. + */ +@PublicApi +public final class Tasks { + + /** + * Blocks until the specified Task is complete. + * + * @return the Task's result + * @throws ExecutionException if the Task fails + * @throws InterruptedException if an interrupt occurs while waiting for the Task to complete + * @throws TimeoutException if the specified timeout is reached before the Task completes + */ + public static TResult await(Task task, long timeout, TimeUnit unit) { + // TODO + return null; + } + + /** + * Blocks until the specified Task is complete. + * + * @return the Task's result + * @throws ExecutionException if the Task fails + * @throws InterruptedException if an interrupt occurs while waiting for the Task to complete + */ + public static TResult await(Task task) { + // TODO + return null; + } + + /** + * Returns a Task that will be completed with the result of the specified Callable. + *

+ * The Callable will be called on the main application thread. + */ + public static Task call(Callable callable) { + // TODO + return null; + } + + /** + * Returns a Task that will be completed with the result of the specified Callable. + * + * @param executor the Executor to use to call the Callable + */ + public static Task call(Executor executor, Callable callable) { + // TODO + return null; + } + + /** + * Returns a completed Task with the specified exception. + */ + public static Task forException(Exception e) { + // TODO + return null; + } + + /** + * Returns a completed Task with the specified result. + */ + public static Task forResult(TResult result) { + // TODO + return null; + } + + /** + * Returns a Task that completes successfully when all of the specified Tasks complete + * successfully. Does not accept nulls. + * + * @throws NullPointerException if any of the provided Tasks are null + */ + public static Task whenAll(Collection> tasks) { + // TODO + return null; + } + + /** + * Returns a Task that completes successfully when all of the specified Tasks complete + * successfully. Does not accept nulls. + * + * @throws NullPointerException if any of the provided Tasks are null + */ + public static Task whenAll(Task... tasks) { + return whenAll(Arrays.asList(tasks)); + } +} diff --git a/play-services-wearable/build.gradle b/play-services-wearable/build.gradle new file mode 100644 index 00000000..f0e5f8e8 --- /dev/null +++ b/play-services-wearable/build.gradle @@ -0,0 +1,58 @@ +/* + * 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() + } + + sourceSets { + main { + java.srcDirs += 'src/main/protos-java' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + lintOptions { + disable 'InvalidPackage' + } +} + +dependencies { + api project(':play-services-base') + api project(':play-services-wearable-api') + implementation 'com.squareup.wire:wire-runtime:1.6.1' +} diff --git a/play-services-wearable/gradle.properties b/play-services-wearable/gradle.properties new file mode 100644 index 00000000..036604f2 --- /dev/null +++ b/play-services-wearable/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 Wearable Library +POM_DESCRIPTION=The Play Services Library module to access the Wearable API + +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-wearable/src/main/AndroidManifest.xml b/play-services-wearable/src/main/AndroidManifest.xml new file mode 100644 index 00000000..c5f892ff --- /dev/null +++ b/play-services-wearable/src/main/AndroidManifest.xml @@ -0,0 +1,18 @@ + + + + diff --git a/play-services-wearable/src/main/java/com/google/android/gms/wearable/CapabilityApi.java b/play-services-wearable/src/main/java/com/google/android/gms/wearable/CapabilityApi.java new file mode 100644 index 00000000..fe320159 --- /dev/null +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/CapabilityApi.java @@ -0,0 +1,193 @@ +/* + * 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.wearable; + +import android.net.Uri; + +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.GoogleApiClient.Builder; +import com.google.android.gms.common.api.PendingResult; +import com.google.android.gms.common.api.Result; +import com.google.android.gms.common.api.Status; + +import org.microg.gms.common.PublicApi; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Map; + +/** + * Exposes an API to learn about capabilities provided by nodes on the Wear network. + *

+ * Capabilities are local to an application. + */ +@PublicApi +public interface CapabilityApi { + /** + * Capability changed action for use in manifest-based listener filters. + *

+ * Capability events do not support filtering by host, but can be filtered by path. + * + * @see WearableListenerService + */ + String ACTION_CAPABILITY_CHANGED = "com.google.android.gms.wearable.CAPABILITY_CHANGED"; + + /** + * Filter type for {@link #getCapability(GoogleApiClient, String, int)}, {@link #getAllCapabilities(GoogleApiClient, int)}: + * If this filter is set then the full set of nodes that declare the given capability will be + * included in the capability's CapabilityInfo. + */ + int FILTER_ALL = 0; + + /** + * Filter type for {@link #addListener(GoogleApiClient, CapabilityListener, Uri, int)}, if this + * filter is set, the given URI will be taken as a literal path, and the operation will apply + * to the matching capability only. + */ + int FILTER_LITERAL = 0; + + /** + * Filter type for {@link #addListener(GoogleApiClient, CapabilityListener, Uri, int)}, if this + * filter is set, the given URI will be taken as a path prefix, and the operation will apply + * to all matching capabilities. + */ + int FILTER_PREFIX = 1; + + /** + * Filter type for {@link #getCapability(GoogleApiClient, String, int)}, {@link #getAllCapabilities(GoogleApiClient, int): + * If this filter is set then only reachable nodes that declare the given capability will be + * included in the capability's CapabilityInfo. + */ + int FILTER_REACHABLE = 1; + + /** + * Registers a listener to be notified of a specific capability being added to or removed from + * the Wear network. Calls to this method should be balanced with {@link #removeCapabilityListener(GoogleApiClient, CapabilityListener, String)} + * to avoid leaking resources. + *

+ * 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 {@link WearableListenerService}. + */ + PendingResult addCapabilityListener(GoogleApiClient client, CapabilityListener listener, String capability); + + /** + * Registers a listener to be notified of capabilities being added to or removed from the Wear + * network. Calls to this method should be balanced with {@link #removeListener(GoogleApiClient, CapabilityListener)} + * to avoid leaking resources. + *

+ * {@code uri} and {@code filterType} can be used to filter the capability changes sent to the + * listener. For example, if {@code uri} and {@code filterType} create a prefix filter, then + * only capabilities matching that prefix will be notified. The {@code uri} follows the rules + * of the {@code } element of {@code }. The path is ignored if a URI host + * is not specified. To match capabilities by name or name prefix, the host must be {@code *}. i.e: + *

+ *

wear://* /
+ * 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 addListener(GoogleApiClient client, CapabilityListener listener, Uri uri, @CapabilityFilterType int filterType); + + /** + * Announces that a capability has become available on the local node. + */ + PendingResult addLocalCapability(GoogleApiClient client, String capability); + + /** + * Returns information about all capabilities, including the nodes that declare those + * capabilities. The filter parameter controls whether all nodes are returned, {@link #FILTER_ALL}, + * or only those that are currently reachable by this node, {@link #FILTER_REACHABLE}. + *

+ * The local node will never be returned in the set of nodes. + */ + PendingResult getAllCapabilities(GoogleApiClient client, @NodeFilterType int nodeFilter); + + /** + * Returns information about a capabilities, including the nodes that declare this capability. + * The filter parameter controls whether all nodes are returned, {@link #FILTER_ALL}, or only + * those that are currently reachable by this node, {@link #FILTER_REACHABLE}. + *

+ * The local node will never be returned in the set of nodes. + */ + PendingResult getCapability(GoogleApiClient client, String capability, @NodeFilterType int nodeFilter); + + /** + * Removes a listener which was previously added through {@link #addCapabilityListener(GoogleApiClient, CapabilityListener, String)}. + * The listener is only removed from listening for the capability provided and will continue to + * receive messages for any other capabilities it was previously registered for that have not + * also been removed. + */ + PendingResult removeCapabilityListener(GoogleApiClient client, CapabilityListener listener, String capability); + + /** + * Removes a listener which was previously added through {@link #addListener(GoogleApiClient, CapabilityListener, Uri, int)}. + * The listener is only removed from listening for the capability provided and will continue to + * receive messages for any other capabilities it was previously registered for that have not + * also been removed. + */ + PendingResult removeListener(GoogleApiClient client, CapabilityListener listener); + + /** + * Announces that a capability is no longer available on the local node. Note: this will not + * remove any capabilities announced in the Manifest for an app. + */ + PendingResult removeLocalCapability(GoogleApiClient client, String capability); + + /** + * Result returned from {@link #addLocalCapability(GoogleApiClient, String)} + */ + interface AddLocalCapabilityResult extends Result { + } + + @Retention(RetentionPolicy.SOURCE) + @interface CapabilityFilterType { + } + + /** + * Listener for changes in the reachable nodes providing a capability. + */ + interface CapabilityListener { + void onCapabilityChanged(CapabilityInfo capabilityInfo); + } + + /** + * Result returned from {@link #getAllCapabilities(GoogleApiClient, int)} + */ + interface GetAllCapabilitiesResult extends Result { + Map getAllCapabilities(); + } + + /** + * Result returned from {@link #getCapability(GoogleApiClient, String, int)} + */ + interface GetCapabilityResult extends Result { + CapabilityInfo getCapability(); + } + + @Retention(RetentionPolicy.SOURCE) + @interface NodeFilterType { + } + + /** + * Result returned from {@link #removeLocalCapability(GoogleApiClient, String)} + */ + interface RemoveLocalCapabilityResult extends Result { + } +} diff --git a/play-services-wearable/src/main/java/com/google/android/gms/wearable/Channel.java b/play-services-wearable/src/main/java/com/google/android/gms/wearable/Channel.java new file mode 100644 index 00000000..d6d01288 --- /dev/null +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/Channel.java @@ -0,0 +1,94 @@ +/* + * 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.wearable; + +import android.net.Uri; +import android.os.Parcelable; + +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.PendingResult; +import com.google.android.gms.common.api.Releasable; +import com.google.android.gms.common.api.Result; +import com.google.android.gms.common.api.Status; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * A channel created through {@link ChannelApi#openChannel(GoogleApiClient, String, String)}. + *

+ * The implementation of this interface is parcelable and immutable, and implements reasonable {@link #equals(Object)} + * and {@link #hashCode()} methods, so can be used in collections. + */ +public interface Channel extends Parcelable { + + PendingResult addListener(GoogleApiClient client, ChannelApi.ChannelListener listener); + + PendingResult close(GoogleApiClient client, int errorCode); + + PendingResult close(GoogleApiClient client); + + PendingResult getInputStream(GoogleApiClient client); + + PendingResult getOutputStream(GoogleApiClient client); + + String getPath(); + + PendingResult receiveFile(GoogleApiClient client, Uri uri, boolean append); + + PendingResult removeListener(GoogleApiClient client, ChannelApi.ChannelListener listener); + + PendingResult sendFile(GoogleApiClient client, Uri uri); + + PendingResult sendFile(GoogleApiClient client, Uri uri, long startOffset, long length); + + interface GetInputStreamResult extends Releasable, Result { + /** + * Returns an input stream which can read data from the remote node. The stream should be + * closed when no longer needed. This method will only return {@code null} if this result's + * {@linkplain #getStatus() status} was not {@linkplain Status#isSuccess() success}. + *

+ * The returned stream will throw {@link IOException} on read if any connection errors + * occur. This exception might be a {@link ChannelIOException}. + *

+ * Since data for this stream comes over the network, reads may block for a long time. + *

+ * Multiple calls to this method will return the same instance. + */ + InputStream getInputStream(); + } + + interface GetOutputStreamResult extends Releasable, Result { + /** + * Returns an output stream which can send data to a remote node. The stream should be + * closed when no longer needed. This method will only return {@code null} if this result's + * {@linkplain #getStatus() status} was not {@linkplain Status#isSuccess() success}. + *

+ * The returned stream will throw {@link IOException} on read if any connection errors + * occur. This exception might be a {@link ChannelIOException}. + *

+ * Since data for this stream comes over the network, reads may block for a long time. + *

+ * Data written to this stream is buffered. If you wish to send the current data without + * waiting for the buffer to fill up, {@linkplain OutputStream#flush() flush} the stream. + *

+ * Multiple calls to this method will return the same instance. + */ + OutputStream getOutputStream(); + } +} diff --git a/play-services-wearable/src/main/java/com/google/android/gms/wearable/ChannelApi.java b/play-services-wearable/src/main/java/com/google/android/gms/wearable/ChannelApi.java new file mode 100644 index 00000000..fcd62b7c --- /dev/null +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/ChannelApi.java @@ -0,0 +1,185 @@ +/* + * 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.wearable; + +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.GoogleApiClient.Builder; +import com.google.android.gms.common.api.PendingResult; +import com.google.android.gms.common.api.Result; +import com.google.android.gms.common.api.Status; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Client interface for Wearable Channel API. Allows apps on a wearable device to send and receive + * data from other wearable nodes. + *

+ * Channels are bidirectional. Each side, both the initiator and the receiver may both read and + * write to the channel by using {@link Channel#getOutputStream(GoogleApiClient)} and {@link Channel#getInputStream(GoogleApiClient)}. + * Once a channel is established, the API for the initiator and receiver are identical. + *

+ * Channels are only available when the wearable nodes are connected. When the remote node + * disconnects, all existing channels will be closed. Any listeners (added through {@link #addListener(GoogleApiClient, ChannelListener)} + * and any installed {@link WearableListenerService}) will be notified of the channel closing. + */ +public interface ChannelApi { + /** + * Channel action for use in listener filters. + * + * @see WearableListenerService + */ + String ACTION_CHANNEL_EVENT = "com.google.android.gms.wearable.CHANNEL_EVENT"; + + /** + * Registers a listener to be notified of channel events. Calls to this method should be + * balanced with calls to {@link #removeListener(GoogleApiClient, ChannelListener)} to avoid + * leaking resources. + *

+ * 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 {@link WearableListenerService}. + * + * @param client a connected client + * @param listener a listener which will be notified of changes to any channel + */ + PendingResult addListener(GoogleApiClient client, ChannelListener listener); + + /** + * Opens a channel to exchange data with a remote node. + *

+ * Channel which are no longer needed should be closed using {@link Channel#close(GoogleApiClient)}. + *

+ * This call involves a network round trip, so may be long running. {@code client} must remain + * connected during that time, or the request will be cancelled (like any other Play Services + * API calls). + * + * @param client a connected client + * @param nodeId the node ID of a wearable node, as returned from {@link NodeApi#getConnectedNodes(GoogleApiClient)} + * @param path an app-specific identifier for the channel + */ + PendingResult openChannel(GoogleApiClient client, String nodeId, String path); + + /** + * Removes a listener which was previously added through {@link #addListener(GoogleApiClient, ChannelListener)}. + * + * @param client a connected client + * @param listener a listener which was added using {@link #addListener(GoogleApiClient, ChannelListener)} + */ + PendingResult removeListener(GoogleApiClient client, ChannelListener listener); + + /** + * A listener which will be notified on changes to channels. + */ + interface ChannelListener { + /** + * Value passed to {@link #onChannelClosed(Channel, int, int)}, {@link #onInputClosed(Channel, int, int)} + * and {@link #onOutputClosed(Channel, int, int)} when the closing is due to a remote node + * being disconnected. + */ + int CLOSE_REASON_DISCONNECTED = 1; + + /** + * Value passed to {@link #onChannelClosed(Channel, int, int)}, {@link #onInputClosed(Channel, int, int)} + * and {@link #onOutputClosed(Channel, int, int)} when the stream is closed due to the + * local node calling {@link Channel#close(GoogleApiClient)} or {@link Channel#close(GoogleApiClient, int)}. + */ + int CLOSE_REASON_LOCAL_CLOSE = 3; + + /** + * Value passed to {@link #onInputClosed(Channel, int, int)} or {@link #onOutputClosed(Channel, int, int)} + * (but not {@link #onChannelClosed(Channel, int, int)}), when the stream was closed under + * normal conditions, e.g the whole file was read, or the OutputStream on the remote node + * was closed normally. + */ + int CLOSE_REASON_NORMAL = 0; + + /** + * Value passed to {@link #onChannelClosed(Channel, int, int)}, {@link #onInputClosed(Channel, int, int)} + * and {@link #onOutputClosed(Channel, int, int)} when the stream is closed due to the + * remote node calling {@link Channel#close(GoogleApiClient)} or {@link Channel#close(GoogleApiClient, int)}. + */ + int CLOSE_REASON_REMOTE_CLOSE = 2; + + /** + * Called when a channel is closed. This can happen through an explicit call to {@link Channel#close(GoogleApiClient)} + * or {@link #close(GoogleApiClient, int)} on either side of the connection, or due to + * disconnecting from the remote node. + * + * @param closeReason the reason for the channel closing. One of {@link #CLOSE_REASON_DISCONNECTED}, + * {@link #CLOSE_REASON_REMOTE_CLOSE}, or {@link #CLOSE_REASON_LOCAL_CLOSE}. + * @param appSpecificErrorCode the error code specified on {@link Channel#close(GoogleApiClient, int)}, + * or 0 if closeReason is {@link #CLOSE_REASON_DISCONNECTED}. + */ + void onChannelClosed(Channel channel, int closeReason, int appSpecificErrorCode); + + /** + * Called when a new channel is opened by a remote node. + */ + void onChannelOpened(Channel channel); + + /** + * Called when the input side of a channel is closed. + * + * @param closeReason the reason for the channel closing. One of {@link #CLOSE_REASON_DISCONNECTED}, + * {@link #CLOSE_REASON_REMOTE_CLOSE}, {@link #CLOSE_REASON_LOCAL_CLOSE} + * or {@link #CLOSE_REASON_NORMAL}. + * @param appSpecificErrorCode the error code specified on {@link Channel#close(GoogleApiClient, int)}, + * or 0 if closeReason is {@link #CLOSE_REASON_DISCONNECTED} or + * {@link #CLOSE_REASON_NORMAL}. + */ + void onInputClosed(Channel channel, @CloseReason int closeReason, int appSpecificErrorCode); + + /** + * Called when the output side of a channel is closed. + * + * @param closeReason the reason for the channel closing. One of {@link #CLOSE_REASON_DISCONNECTED}, + * {@link #CLOSE_REASON_REMOTE_CLOSE}, {@link #CLOSE_REASON_LOCAL_CLOSE} + * or {@link #CLOSE_REASON_NORMAL}. + * @param appSpecificErrorCode the error code specified on {@link Channel#close(GoogleApiClient, int)}, + * or 0 if closeReason is {@link #CLOSE_REASON_DISCONNECTED} or + * {@link #CLOSE_REASON_NORMAL}. + */ + void onOutputClosed(Channel channel, @CloseReason int closeReason, int appSpecificErrorCode); + } + + /** + * An annotation for values passed to {@link ChannelListener#onChannelClosed(Channel, int, int)}, + * and other methods on the {@link ChannelListener} interface. Annotated method parameters will + * always take one of the following values: + *

    + *
  • {@link ChannelListener#CLOSE_REASON_DISCONNECTED}
  • + *
  • {@link ChannelListener#CLOSE_REASON_NORMAL}
  • + *
  • {@link ChannelListener#CLOSE_REASON_LOCAL_CLOSE}
  • + *
  • {@link ChannelListener#CLOSE_REASON_REMOTE_CLOSE}
  • + *
+ */ + @Retention(RetentionPolicy.SOURCE) + @interface CloseReason { + } + + /** + * Result of {@link #openChannel(GoogleApiClient, String, String)}. + */ + interface OpenChannelResult extends Result { + /** + * Returns the newly created channel, or {@code null}, if the connection couldn't be opened. + */ + Channel getChannel(); + } +} diff --git a/play-services-wearable/src/main/java/com/google/android/gms/wearable/ChannelIOException.java b/play-services-wearable/src/main/java/com/google/android/gms/wearable/ChannelIOException.java new file mode 100644 index 00000000..f6143cdd --- /dev/null +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/ChannelIOException.java @@ -0,0 +1,54 @@ +/* + * 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.wearable; + +import com.google.android.gms.wearable.ChannelApi.ChannelListener; + +import java.io.IOException; + +/** + * A subclass of {@link IOException} which can be thrown from the streams returned by + * {@link Channel#getInputStream(GoogleApiClient)} and {@link Channel#getOutputStream(GoogleApiClient)}. + */ +public class ChannelIOException extends IOException { + + private int closeReason; + private int appSpecificErrorCode; + + public ChannelIOException(String message, int closeReason, int appSpecificErrorCode) { + super(message); + this.closeReason = closeReason; + this.appSpecificErrorCode = appSpecificErrorCode; + } + + /** + * Returns the app-specific error code passed to {@link Channel#close(GoogleApiClient, int)} if + * that's the reason for the stream closing, or {@code 0} otherwise. + */ + public int getAppSpecificErrorCode() { + return appSpecificErrorCode; + } + + /** + * Returns one of {@link ChannelListener#CLOSE_REASON_NORMAL}, {@link ChannelListener#CLOSE_REASON_DISCONNECTED}, + * {@link ChannelListener#CLOSE_REASON_REMOTE_CLOSE}, or {@link ChannelListener#CLOSE_REASON_LOCAL_CLOSE}, + * to indicate the reason for the stream closing. + */ + public int getCloseReason() { + return closeReason; + } +} diff --git a/play-services-wearable/src/main/java/com/google/android/gms/wearable/DataApi.java b/play-services-wearable/src/main/java/com/google/android/gms/wearable/DataApi.java new file mode 100644 index 00000000..4327b6c2 --- /dev/null +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/DataApi.java @@ -0,0 +1,166 @@ +/* + * 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.wearable; + +import android.net.Uri; +import android.os.ParcelFileDescriptor; + +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.PendingResult; +import com.google.android.gms.common.api.Result; +import com.google.android.gms.common.api.Status; +import com.google.android.gms.common.data.Freezable; +import com.google.android.gms.wearable.internal.PutDataRequest; + +import org.microg.gms.common.PublicApi; + +import java.io.InputStream; + +/** + * Exposes an API for components to read or write data items and assets. + *

+ * A {@link DataItem} is synchronized across all devices in an Android Wear network. It is possible + * to set data items while not connected to any nodes. Those data items will be synchronized when + * the nodes eventually come online. + *

+ * Data items are private to the application that created them, and are only accessible by that + * application on other nodes. They should generally be small in size, relying on assets for the + * transfer of larger, more persistent data objects such as images. + *

+ * Each data item is identified by a URI, accessible with {@link DataItem#getUri()}, that indicates + * the item's creator and path. Fully specified URIs follow the following format: + * {@code wear:///}, where is the node ID of the wearable node that + * created the data item, and is an application-defined path. This means that given a data + * item's URI, calling {@link Uri#getHost()} will return the creator's node ID. + *

+ * In some of the methods below (such as {@link #getDataItems(GoogleApiClient, Uri)}), it is + * possible to omit the node ID from the URI, and only leave a path. In that case, the URI may + * refer to multiple data items, since multiple nodes may create data items with the same path. + * Partially specified data item URIs follow the following format: + * {@ocde wear:/} + * Note the single / after wear:. + */ +@PublicApi +public interface DataApi { + /** + * Registers a listener to receive data item changed and deleted events. This call should be + * balanced with a call to {@link #removeListener(GoogleApiClient, DataListener)}, to avoid + * leaking resources. + *

+ * The listener will be notified of changes initiated by this node. + */ + PendingResult addListener(GoogleApiClient client, DataListener listener); + + /** + * Removes all specified data items from the Android Wear network. + *

+ * If uri is fully specified, this method will delete at most one data item. If {@code uri} + * contains no host, multiple data items may be deleted, since different nodes may create data + * items with the same path. See {@link DataApi} for details of the URI format. + */ + PendingResult deleteDataItems(GoogleApiClient client, Uri uri); + + /** + * Retrieves a single {@link DataItem} from the Android Wear network. A fully qualified URI + * must be specified. The URI's host must be the ID of the node that created the item. + *

+ * See {@link DataApi} for details of the URI format. + */ + PendingResult getDataItem(GoogleApiClient client, Uri uri); + + /** + * Retrieves all data items from the Android Wear network. + *

+ * Callers must call {@link DataItemBuffer#release()} on the returned buffer when finished + * processing results. + */ + PendingResult getDataItems(GoogleApiClient client); + + /** + * Retrieves all data items matching the provided URI, from the Android Wear network. + *

+ * The URI must contain a path. If {@code uri} is fully specified, at most one data item will + * be returned. If uri contains no host, multiple data items may be returned, since different + * nodes may create data items with the same path. See {@link DataApi} for details of the URI + * format. + *

+ * Callers must call {@link DataItemBuffer#release()} on the returned buffer when finished + * processing results. + */ + PendingResult getDataItems(GoogleApiClient client, Uri uri); + + /** + * Retrieves a {@link ParcelFileDescriptor} pointing at the bytes of an asset. Only assets + * previously stored in a {@link DataItem} may be retrieved. + */ + PendingResult getFdForAsset(GoogleApiClient client, DataItemAsset asset); + + /** + * Retrieves a {@link ParcelFileDescriptor} pointing at the bytes of an asset. Only assets + * previously stored in a {@link DataItem} may be retrieved. + */ + PendingResult getFdForAsset(GoogleApiClient client, Asset asset); + + /** + * Adds a {@link DataItem} to the Android Wear network. The updated item is synchronized across + * all devices. + */ + PendingResult putDataItem(GoogleApiClient client, PutDataRequest request); + + /** + * Removes a data listener which was previously added through + * {@link #addListener(GoogleApiClient, DataListener)}. + */ + PendingResult removeListener(GoogleApiClient client, DataListener listener); + + interface DataItemResult extends Result { + /** + * @return data item, or {@code null} if the item does not exit. + */ + DataItem getDataItem(); + } + + interface DataListener { + /** + * Notification that a set of data items have been changed or deleted. The data buffer is + * released upon completion of this method. If a caller wishes to use the events outside + * this callback, they should be sure to {@link Freezable#freeze()} the DataEvent objects + * they wish to use. + */ + void onDataChanged(DataEventBuffer dataEvents); + } + + interface DeleteDataItemsResult extends Result { + /** + * @return the number of items deleted by + * {@link DataApi#deleteDataItems(GoogleApiClient, Uri)}. + */ + int getNumDeleted(); + } + + interface GetFdForAssetResult extends Result { + /** + * @return a file descriptor for the requested asset. + */ + ParcelFileDescriptor getFd(); + + /** + * @return an input stream wrapping the file descriptor. When this input stream is closed, the file descriptor is, as well. + */ + InputStream getInputStream(); + } +} diff --git a/play-services-wearable/src/main/java/com/google/android/gms/wearable/DataEvent.java b/play-services-wearable/src/main/java/com/google/android/gms/wearable/DataEvent.java new file mode 100644 index 00000000..51e86397 --- /dev/null +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/DataEvent.java @@ -0,0 +1,50 @@ +/* + * 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.wearable; + +import com.google.android.gms.common.data.Freezable; + +import org.microg.gms.common.PublicApi; + +/** + * Data interface for data events. + */ +@PublicApi +public interface DataEvent extends Freezable { + + /** + * Indicates that the enclosing {@link DataEvent} was triggered by a data item being added or + * changed. + */ + int TYPE_CHANGED = 1; + + /** + * Indicates that the enclosing {@link DataEvent} was triggered by a data item being deleted. + */ + int TYPE_DELETED = 2; + + /** + * @return the data item modified in this event. An event of {@link #TYPE_DELETED} will only + * have its {@link DataItem#getUri} populated. + */ + DataItem getDataItem(); + + /** + * @return the type of event this is. One of {@link #TYPE_CHANGED}, {@link #TYPE_DELETED}. + */ + int getType(); +} diff --git a/play-services-wearable/src/main/java/com/google/android/gms/wearable/DataEventBuffer.java b/play-services-wearable/src/main/java/com/google/android/gms/wearable/DataEventBuffer.java new file mode 100644 index 00000000..1c6c98e0 --- /dev/null +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/DataEventBuffer.java @@ -0,0 +1,48 @@ +/* + * 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.wearable; + +import com.google.android.gms.common.api.Result; +import com.google.android.gms.common.api.Status; +import com.google.android.gms.common.data.DataBuffer; +import com.google.android.gms.common.data.DataHolder; + +import org.microg.gms.common.PublicApi; + +/** + * Data structure holding references to a set of events. + */ +@PublicApi +public class DataEventBuffer extends DataBuffer implements Result { + private Status status; + + @PublicApi(exclude = true) + public DataEventBuffer(DataHolder dataHolder) { + super(dataHolder); + status = new Status(dataHolder.getStatusCode()); + } + + @Override + public DataEvent get(int position) { + return null; + } + + @Override + public Status getStatus() { + return null; + } +} diff --git a/play-services-wearable/src/main/java/com/google/android/gms/wearable/DataItemBuffer.java b/play-services-wearable/src/main/java/com/google/android/gms/wearable/DataItemBuffer.java new file mode 100644 index 00000000..703d7ae3 --- /dev/null +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/DataItemBuffer.java @@ -0,0 +1,45 @@ +/* + * 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.wearable; + +import com.google.android.gms.common.api.Result; +import com.google.android.gms.common.api.Status; +import com.google.android.gms.common.data.DataBuffer; +import com.google.android.gms.common.data.DataHolder; + +import org.microg.gms.common.PublicApi; + +@PublicApi +public class DataItemBuffer extends DataBuffer implements Result { + private Status status; + + @PublicApi(exclude = true) + public DataItemBuffer(DataHolder dataHolder) { + super(dataHolder); + status = new Status(dataHolder.getStatusCode()); + } + + @Override + public DataItem get(int position) { + return null; + } + + @Override + public Status getStatus() { + return null; + } +} diff --git a/play-services-wearable/src/main/java/com/google/android/gms/wearable/DataMap.java b/play-services-wearable/src/main/java/com/google/android/gms/wearable/DataMap.java new file mode 100644 index 00000000..f46c4920 --- /dev/null +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/DataMap.java @@ -0,0 +1,463 @@ +/* + * 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.wearable; + +import android.os.Bundle; + +import org.microg.gms.common.PublicApi; +import org.microg.gms.wearable.databundle.DataBundleUtil; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import static org.microg.gms.wearable.databundle.DataBundleUtil.ASSET_TYPE_CODE; +import static org.microg.gms.wearable.databundle.DataBundleUtil.BOOLEAN_TYPE_CODE; +import static org.microg.gms.wearable.databundle.DataBundleUtil.BYTE_ARRAY_TYPE_CODE; +import static org.microg.gms.wearable.databundle.DataBundleUtil.BYTE_TYPE_CODE; +import static org.microg.gms.wearable.databundle.DataBundleUtil.DATAMAP_TYPE_CODE; +import static org.microg.gms.wearable.databundle.DataBundleUtil.DOUBLE_TYPE_CODE; +import static org.microg.gms.wearable.databundle.DataBundleUtil.FLOAT_ARRAY_TYPE_CODE; +import static org.microg.gms.wearable.databundle.DataBundleUtil.FLOAT_TYPE_CODE; +import static org.microg.gms.wearable.databundle.DataBundleUtil.INTEGER_TYPE_CODE; +import static org.microg.gms.wearable.databundle.DataBundleUtil.LIST_TYPE_CODE; +import static org.microg.gms.wearable.databundle.DataBundleUtil.LONG_ARRAY_TYPE_CODE; +import static org.microg.gms.wearable.databundle.DataBundleUtil.LONG_TYPE_CODE; +import static org.microg.gms.wearable.databundle.DataBundleUtil.STRING_ARRAY_TYPE_CODE; +import static org.microg.gms.wearable.databundle.DataBundleUtil.STRING_TYPE_CODE; + +/** + * A map of data supported by {@link PutDataMapRequest} and {@link DataMapItem}s. DataMap may + * convert to and from Bundles, but will drop any types not explicitly supported by DataMap in the + * conversion process. + */ +@PublicApi +public class DataMap { + public static String TAG = "GmsDataMap"; + + private Map data = new HashMap(); + private Map types = new HashMap(); + + public DataMap() { + + } + + /** + * @return an ArrayList of DataMaps from an ArrayList of Bundles. Any elements in the Bundles not supported by DataMap will be dropped. + */ + public static ArrayList arrayListFromBundleArrayList(ArrayList bundleArrayList) { + ArrayList res = new ArrayList(); + for (Bundle bundle : bundleArrayList) { + res.add(fromBundle(bundle)); + } + return res; + } + + /** + * Removes all elements from the mapping of this DataMap. + */ + public void clear() { + data.clear(); + } + + /** + * @return true if the given key is contained in the mapping of this DataMap. + */ + public boolean containsKey(String key) { + return data.containsKey(key); + } + + /** + * @return true if the given Object is a DataMap equivalent to this one. + */ + @Override + public boolean equals(Object o) { + return o instanceof DataMap && data.equals(((DataMap) o).data); + } + + public StoredType getType(String key) { + return types.get(key); + } + + @PublicApi(exclude = true) + public enum StoredType { + Asset(ASSET_TYPE_CODE), Boolean(BOOLEAN_TYPE_CODE), Byte(BYTE_TYPE_CODE), + ByteArray(BYTE_ARRAY_TYPE_CODE), DataMap(DATAMAP_TYPE_CODE), DataMapArrayList(DataMap), + Double(DOUBLE_TYPE_CODE), Float(FLOAT_TYPE_CODE), FloatArray(FLOAT_ARRAY_TYPE_CODE), + Integer(INTEGER_TYPE_CODE), IntegerArrayList(Integer), Long(LONG_TYPE_CODE), + LongArray(LONG_ARRAY_TYPE_CODE), String(STRING_TYPE_CODE), + StringArray(STRING_ARRAY_TYPE_CODE), StringArrayList(String); + + private int typeCode; + private StoredType listType; + + StoredType(int typeCode) { + this.typeCode = typeCode; + } + + StoredType(StoredType listType) { + this.typeCode = LIST_TYPE_CODE; + this.listType = listType; + } + + public int getTypeCode() { + return typeCode; + } + + public StoredType getListType() { + return listType; + } + } + + /** + * @return a DataMap from a Bundle. The input Bundle is expected to contain only elements + * supported by DataMap. Any elements in the Bundle not supported by DataMap will be dropped. + */ + public static DataMap fromBundle(Bundle bundle) { + DataMap res = new DataMap(); + if (bundle != null) { + for (String key : bundle.keySet()) { + Object val = bundle.get(key); + if (val instanceof Asset) { + res.putAsset(key, (Asset) val); + } else if (val instanceof Boolean) { + res.putBoolean(key, (Boolean) val); + } else if (val instanceof Byte) { + res.putByte(key, (Byte) val); + } else if (val instanceof byte[]) { + res.putByteArray(key, (byte[]) val); + } else if (val instanceof Bundle) { + res.putDataMap(key, DataMap.fromBundle((Bundle) val)); + } else if (val instanceof Double) { + res.putDouble(key, (Double) val); + } else if (val instanceof Float) { + res.putFloat(key, (Float) val); + } else if (val instanceof float[]) { + res.putFloatArray(key, (float[]) val); + } else if (val instanceof Integer) { + res.putInt(key, (Integer) val); + } else if (val instanceof Long) { + res.putLong(key, (Long) val); + } else if (val instanceof long[]) { + res.putLongArray(key, (long[]) val); + } else if (val instanceof String) { + res.putString(key, (String) val); + } else if (val instanceof String[]) { + res.putStringArray(key, (String[]) val); + } else if (val instanceof ArrayList) { + if (((ArrayList) val).isEmpty() || ((ArrayList) val).get(0) instanceof String) { + res.putStringArrayList(key, (ArrayList) val); + } else if (((ArrayList) val).get(0) instanceof Bundle) { + ArrayList dataMaps = new ArrayList(); + for (Bundle b : ((ArrayList) val)) { + dataMaps.add(DataMap.fromBundle(b)); + } + res.putDataMapArrayList(key, dataMaps); + } else if (((ArrayList) val).get(0) instanceof Integer) { + res.putIntegerArrayList(key, (ArrayList) val); + } + } + } + } + return res; + } + + /** + * @return a DataMap from a byte[]. + */ + public static DataMap fromByteArray(byte[] bytes) { + return DataBundleUtil.readDataMap(bytes, Collections.emptyList()); + } + + /** + * @return the entry with the given key as an object, or null + */ + public T get(String key) { + return (T) data.get(key); + } + + public Asset getAsset(String key) { + return types.get(key) == StoredType.Asset ? (Asset) data.get(key) : null; + } + + public boolean getBoolean(String key) { + return getBoolean(key, false); + } + + public boolean getBoolean(String key, boolean defaultValue) { + return types.get(key) == StoredType.Boolean ? (Boolean) data.get(key) : defaultValue; + } + + public byte getByte(String key) { + return getByte(key, (byte) 0); + } + + public byte getByte(String key, byte defaultValue) { + return types.get(key) == StoredType.Byte ? (Byte) data.get(key) : defaultValue; + } + + public byte[] getByteArray(String key) { + return types.get(key) == StoredType.ByteArray ? (byte[]) data.get(key) : null; + } + + public DataMap getDataMap(String key) { + return types.get(key) == StoredType.DataMap ? (DataMap) data.get(key) : null; + } + + public ArrayList getDataMapArrayList(String key) { + return types.get(key) == StoredType.DataMapArrayList ? (ArrayList) data.get(key) : null; + } + + public double getDouble(String key) { + return getDouble(key, 0.0); + } + + public double getDouble(String key, double defaultValue) { + return types.get(key) == StoredType.Double ? (Double) data.get(key) : defaultValue; + } + + public float getFloat(String key) { + return getFloat(key, 0.0f); + } + + public float getFloat(String key, float defaultValue) { + return types.get(key) == StoredType.Float ? (Float) data.get(key) : defaultValue; + } + + public float[] getFloatArray(String key) { + return types.get(key) == StoredType.FloatArray ? (float[]) data.get(key) : null; + } + + public int getInt(String key) { + return getInt(key, 0); + } + + public int getInt(String key, int defaultValue) { + return types.get(key) == StoredType.Integer ? (Integer) data.get(key) : defaultValue; + } + + public ArrayList getIntegerArrayList(String key) { + return types.get(key) == StoredType.IntegerArrayList ? (ArrayList) data.get(key) : null; + } + + public long getLong(String key) { + return getLong(key, 0L); + } + + public long getLong(String key, long defaultValue) { + return types.get(key) == StoredType.Long ? (Long) data.get(key) : defaultValue; + } + + public long[] getLongArray(String key) { + return types.get(key) == StoredType.LongArray ? (long[]) data.get(key) : null; + } + + public String getString(String key) { + return getString(key, null); + } + + public String getString(String key, String defaultValue) { + return types.get(key) == StoredType.String ? (String) data.get(key) : defaultValue; + } + + public String[] getStringArray(String key) { + return types.get(key) == StoredType.StringArray ? (String[]) data.get(key) : null; + } + + public ArrayList getStringArrayList(String key) { + return types.get(key) == StoredType.StringArrayList ? (ArrayList) data.get(key) : null; + } + + public int hashCode() { + return data.hashCode(); + } + + public boolean isEmpty() { + return data.isEmpty(); + } + + public Set keySet() { + return data.keySet(); + } + + public void putAll(DataMap dataMap) { + for (String key : dataMap.keySet()) { + data.put(key, dataMap.data.get(key)); + types.put(key, dataMap.types.get(key)); + } + } + + public void putAsset(String key, Asset value) { + data.put(key, value); + types.put(key, StoredType.Asset); + } + + public void putBoolean(String key, boolean value) { + data.put(key, value); + types.put(key, StoredType.Boolean); + } + + public void putByte(String key, byte value) { + data.put(key, value); + types.put(key, StoredType.Byte); + } + + public void putByteArray(String key, byte[] value) { + data.put(key, value); + types.put(key, StoredType.ByteArray); + } + + public void putDataMap(String key, DataMap value) { + data.put(key, value); + types.put(key, StoredType.DataMap); + } + + public void putDataMapArrayList(String key, ArrayList value) { + data.put(key, value); + types.put(key, StoredType.DataMapArrayList); + } + + public void putDouble(String key, double value) { + data.put(key, value); + types.put(key, StoredType.Double); + } + + public void putFloat(String key, float value) { + data.put(key, value); + types.put(key, StoredType.Float); + } + + public void putFloatArray(String key, float[] value) { + data.put(key, value); + types.put(key, StoredType.FloatArray); + } + + public void putInt(String key, int value) { + data.put(key, value); + types.put(key, StoredType.Integer); + } + + public void putIntegerArrayList(String key, ArrayList value) { + data.put(key, value); + types.put(key, StoredType.IntegerArrayList); + } + + public void putLong(String key, long value) { + data.put(key, value); + types.put(key, StoredType.Long); + } + + public void putLongArray(String key, long[] value) { + data.put(key, value); + types.put(key, StoredType.LongArray); + } + + public void putString(String key, String value) { + data.put(key, value); + types.put(key, StoredType.String); + } + + public void putStringArray(String key, String[] value) { + data.put(key, value); + types.put(key, StoredType.StringArray); + } + + public void putStringArrayList(String key, ArrayList value) { + data.put(key, value); + types.put(key, StoredType.StringArrayList); + } + + public Object remove(String key) { + types.remove(key); + return data.remove(key); + } + + public int size() { + return data.size(); + } + + public Bundle toBundle() { + Bundle bundle = new Bundle(); + for (String key : data.keySet()) { + switch (types.get(key)) { + case Asset: + bundle.putParcelable(key, (Asset) data.get(key)); + break; + case Boolean: + bundle.putBoolean(key, (Boolean) data.get(key)); + break; + case Byte: + bundle.putByte(key, (Byte) data.get(key)); + break; + case ByteArray: + bundle.putByteArray(key, (byte[]) data.get(key)); + break; + case DataMap: + bundle.putBundle(key, ((DataMap) data.get(key)).toBundle()); + break; + case DataMapArrayList: + ArrayList bundles = new ArrayList(); + for (DataMap dataMap : ((ArrayList) data.get(key))) { + bundles.add(dataMap.toBundle()); + } + bundle.putParcelableArrayList(key, bundles); + break; + case Double: + bundle.putDouble(key, (Double) data.get(key)); + break; + case Float: + bundle.putFloat(key, (Float) data.get(key)); + break; + case FloatArray: + bundle.putFloatArray(key, (float[]) data.get(key)); + break; + case Integer: + bundle.putInt(key, (Integer) data.get(key)); + break; + case IntegerArrayList: + bundle.putIntegerArrayList(key, (ArrayList) data.get(key)); + break; + case Long: + bundle.putLong(key, (Long) data.get(key)); + break; + case LongArray: + bundle.putLongArray(key, (long[]) data.get(key)); + break; + case String: + bundle.putString(key, (String) data.get(key)); + break; + case StringArray: + bundle.putStringArray(key, (String[]) data.get(key)); + break; + case StringArrayList: + bundle.putStringArrayList(key, (ArrayList) data.get(key)); + break; + } + } + return bundle; + } + + public byte[] toByteArray() { + return DataBundleUtil.createBytes(this); + } + + public String toString() { + return "DataMap{size=" + size() + "}"; + } +} diff --git a/play-services-wearable/src/main/java/com/google/android/gms/wearable/DataMapItem.java b/play-services-wearable/src/main/java/com/google/android/gms/wearable/DataMapItem.java new file mode 100644 index 00000000..670260d4 --- /dev/null +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/DataMapItem.java @@ -0,0 +1,45 @@ +/* + * 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.wearable; + +import android.net.Uri; + +import org.microg.gms.common.PublicApi; + +/** + * Creates a new dataItem-like object containing structured and serializable data. + */ +@PublicApi +public class DataMapItem { + /** + * Provides a {@link DataMapItem} wrapping a dataItem. + * + * @param dataItem the base for the wrapped {@link DataMapItem}. {@code dataItem} should not + * be modified after wrapping it. + */ + public static DataMapItem fromDataItem(DataItem dataItem) { + return null; + } + + public DataMap getDataMap() { + return null; + } + + public Uri getUri() { + return null; + } +} diff --git a/play-services-wearable/src/main/java/com/google/android/gms/wearable/MessageApi.java b/play-services-wearable/src/main/java/com/google/android/gms/wearable/MessageApi.java new file mode 100644 index 00000000..1e68ad1d --- /dev/null +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/MessageApi.java @@ -0,0 +1,88 @@ +/* + * 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.wearable; + +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.PendingResult; +import com.google.android.gms.common.api.Result; +import com.google.android.gms.common.api.Status; + +import org.microg.gms.common.PublicApi; + +@PublicApi +public interface MessageApi { + + /** + * A value returned by {@link SendMessageResult#getRequestId()} when + * {@link #sendMessage(GoogleApiClient, String, String, byte[])} fails. + */ + int UNKNOWN_REQUEST_ID = -1; + + /** + * Registers a listener to be notified of received messages. Calls to this method should + * balanced with {@link #removeListener(GoogleApiClient, MessageListener)} to avoid leaking + * resources. + *

+ * Callers wishing to be notified of events in the background should use {@link WearableListenerService}. + */ + PendingResult addListener(GoogleApiClient client, MessageListener listener); + + /** + * Removes a message listener which was previously added through + * {@link #addListener(GoogleApiClient, MessageListener)}. + */ + PendingResult removeListener(GoogleApiClient client, MessageListener listener); + + /** + * Sends {@code byte[]} data to the specified node. + * + * @param nodeId identifier for a particular node on the Android Wear network. Valid targets + * may be obtained through {@link NodeApi#getConnectedNodes(GoogleApiClient)} + * or from the host in {@link DataItem#getUri()}. + * @param path identifier used to specify a particular endpoint at the receiving node + * @param data small array of information to pass to the target node. Generally not larger + * than 100k + */ + PendingResult sendMessage(GoogleApiClient client, String nodeId, + String path, byte[] data); + + /** + * Used with {@link MessageApi#addListener(GoogleApiClient, MessageListener)} to receive + * message events. + *

+ * Callers wishing to be notified of events in the background should use + * {@link WearableListenerService}. + */ + interface MessageListener { + /** + * Notification that a message has been received. + */ + void onMessageReceived(MessageEvent messageEvent); + } + + /** + * Contains the request id assigned to the message. On failure, the id will be + * {@link MessageApi#UNKNOWN_REQUEST_ID} and the status will be unsuccessful. + */ + interface SendMessageResult extends Result { + /** + * @return an ID used to identify the sent message. If {@link #getStatus()} is not + * successful, this value will be {@link MessageApi#UNKNOWN_REQUEST_ID}. + */ + int getRequestId(); + } +} diff --git a/play-services-wearable/src/main/java/com/google/android/gms/wearable/NodeApi.java b/play-services-wearable/src/main/java/com/google/android/gms/wearable/NodeApi.java new file mode 100644 index 00000000..6c6a188f --- /dev/null +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/NodeApi.java @@ -0,0 +1,98 @@ +/* + * 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.wearable; + +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.PendingResult; +import com.google.android.gms.common.api.Result; +import com.google.android.gms.common.api.Status; + +import org.microg.gms.common.PublicApi; + +import java.util.List; + +/** + * Exposes an API for to learn about local or connected Nodes. + *

+ * Node events are delivered to all applications on a device. + */ +@PublicApi +public interface NodeApi { + + /** + * Registers a listener to receive all node events. Calls to this method should balanced with + * {@link #removeListener(GoogleApiClient, NodeListener)}, to avoid leaking resources. + *

+ * Callers wishing to be notified of node events in the background should use WearableListenerService. + */ + PendingResult addListener(GoogleApiClient client, NodeListener listener); + + /** + * Gets a list of nodes to which this device is currently connected. + *

+ * The returned list will not include the {@link #getLocalNode(GoogleApiClient) local node}. + */ + PendingResult getConnectedNodes(GoogleApiClient client); + + /** + * Gets the {@link Node} that refers to this device. The information in the returned Node + * can be passed to other devices using the {@link MessageApi}, for example. + */ + PendingResult getLocalNode(GoogleApiClient client); + + /** + * Removes a listener which was previously added through + * {@link #addListener(GoogleApiClient, NodeListener)}. + */ + PendingResult removeListener(GoogleApiClient client, NodeListener listener); + + + /** + * Contains a list of connected nodes. + */ + interface GetConnectedNodesResult extends Result { + /** + * @return a list of connected nodes. This list doesn't include the local node. + */ + List getNodes(); + } + + /** + * Contains the name and id that represents this device. + */ + interface GetLocalNodeResult extends Result { + /** + * @return a {@link Node} object which represents this device. + */ + Node getNode(); + } + + /** + * Used with {@link NodeApi#addListener(GoogleApiClient, NodeListener)} to receive node events. + */ + interface NodeListener { + /** + * Notification that a peer has been connected. + */ + void onPeerConnected(Node peer); + + /** + * Notification that a peer has been disconnected. + */ + void onPeerDisconnected(Node peer); + } +} diff --git a/play-services-wearable/src/main/java/com/google/android/gms/wearable/PutDataMapRequest.java b/play-services-wearable/src/main/java/com/google/android/gms/wearable/PutDataMapRequest.java new file mode 100644 index 00000000..d92167cc --- /dev/null +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/PutDataMapRequest.java @@ -0,0 +1,85 @@ +/* + * 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.wearable; + +import android.net.Uri; + +import com.google.android.gms.wearable.internal.PutDataRequest; + +import org.microg.gms.common.PublicApi; + +/** + * PutDataMapRequest is a DataMap-aware version of {@link PutDataRequest}. + */ +@PublicApi +public class PutDataMapRequest { + + private DataMapItem dataMapItem; + + private PutDataMapRequest(DataMapItem dataMapItem) { + this.dataMapItem = dataMapItem; + } + + /** + * Creates a {@link PutDataRequest} containing the data and assets in this + * {@link PutDataMapRequest}. + */ + public PutDataRequest asPutDataRequest() { + // TODO + return PutDataRequest.create((Uri) null); + } + + /** + * Creates a {@link PutDataMapRequest} with the provided, complete, path. + */ + public static PutDataMapRequest create(String path) { + // TODO + return new PutDataMapRequest(null); + } + + /** + * Creates a {@link PutDataMapRequest} from a {@link DataMapItem} using the provided source. + */ + public static PutDataMapRequest createFromDataMapItem(DataMapItem source) { + return new PutDataMapRequest(source); + } + + /** + * Creates a {@link PutDataMapRequest} with a randomly generated id prefixed with the provided + * path. + */ + public static PutDataMapRequest createWithAutoAppendedId(String pathPrefix) { + // TODO + return new PutDataMapRequest(null); + } + + /** + * @return the structured data associated with this data item. + */ + public DataMap getDataMap() { + return dataMapItem.getDataMap(); + } + + /** + * @return a {@link Uri} for the pending data item. If this is a modification of an existing + * data item, {@link Uri#getHost()} will return the id of the node that originally created it. + * Otherwise, a new data item will be created with the requesting device's node. + */ + public Uri getUri() { + return dataMapItem.getUri(); + } +} diff --git a/play-services-wearable/src/main/java/com/google/android/gms/wearable/Wearable.java b/play-services-wearable/src/main/java/com/google/android/gms/wearable/Wearable.java new file mode 100644 index 00000000..88899989 --- /dev/null +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/Wearable.java @@ -0,0 +1,55 @@ +/* + * 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.wearable; + +import com.google.android.gms.common.api.Api; +import com.google.android.gms.common.api.GoogleApiClient; + +import org.microg.gms.common.PublicApi; +import org.microg.gms.wearable.DataApiImpl; +import org.microg.gms.wearable.MessageApiImpl; +import org.microg.gms.wearable.NodeApiImpl; +import org.microg.gms.wearable.WearableApiBuilder; + +/** + * An API for the Android Wear platform. + */ +@PublicApi +public class Wearable { + /** + * Token to pass to {@link GoogleApiClient.Builder#addApi(Api)} to enable the Wearable features. + */ + public static final Api API = new Api(new WearableApiBuilder()); + + public static final DataApi DataApi = new DataApiImpl(); + public static final MessageApi MessageApi = new MessageApiImpl(); + public static final NodeApi NodeApi = new NodeApiImpl(); + + public static class WearableOptions implements Api.ApiOptions.Optional { + /** + * Special option for microG to allow implementation of a FOSS first party Android Wear app + */ + @PublicApi(exclude = true) + public boolean firstPartyMode = false; + + public static class Builder { + public WearableOptions build() { + return new WearableOptions(); + } + } + } +} diff --git a/play-services-wearable/src/main/java/com/google/android/gms/wearable/WearableListenerService.java b/play-services-wearable/src/main/java/com/google/android/gms/wearable/WearableListenerService.java new file mode 100644 index 00000000..8aa5b091 --- /dev/null +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/WearableListenerService.java @@ -0,0 +1,269 @@ +/* + * 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.wearable; + +import android.app.Service; +import android.content.Intent; +import android.os.Binder; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.IBinder; +import android.os.Looper; +import android.os.RemoteException; +import android.util.Log; + +import com.google.android.gms.common.data.DataHolder; +import com.google.android.gms.wearable.internal.AmsEntityUpdateParcelable; +import com.google.android.gms.wearable.internal.AncsNotificationParcelable; +import com.google.android.gms.wearable.internal.CapabilityInfoParcelable; +import com.google.android.gms.wearable.internal.ChannelEventParcelable; +import com.google.android.gms.wearable.internal.IWearableListener; +import com.google.android.gms.wearable.internal.MessageEventParcelable; +import com.google.android.gms.wearable.internal.NodeParcelable; + +import org.microg.gms.common.PublicApi; +import org.microg.gms.wearable.ChannelImpl; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static org.microg.gms.common.Constants.GMS_PACKAGE_NAME; + +@PublicApi +public abstract class WearableListenerService extends Service implements CapabilityApi.CapabilityListener, ChannelApi.ChannelListener, DataApi.DataListener, MessageApi.MessageListener, NodeApi.NodeListener { + private static final String BIND_LISTENER_INTENT_ACTION = "com.google.android.gms.wearable.BIND_LISTENER"; + private static final String TAG = "GmsWearListenerSvc"; + + private HandlerThread handlerThread; + private IWearableListener listener; + private ServiceHandler serviceHandler; + private Object lock = new Object(); + private boolean disconnected = false; + + @Override + public IBinder onBind(Intent intent) { + if (BIND_LISTENER_INTENT_ACTION.equals(intent.getAction())) { + return listener.asBinder(); + } + return null; + } + + @Override + public void onCapabilityChanged(CapabilityInfo capabilityInfo) { + } + + public void onConnectedNodes(List connectedNodes) { + } + + @Override + public void onChannelClosed(Channel channel, int closeReason, int appSpecificErrorCode) { + } + + @Override + public void onChannelOpened(Channel channel) { + } + + @Override + public void onCreate() { + super.onCreate(); + handlerThread = new HandlerThread("WearableListenerService"); + handlerThread.start(); + serviceHandler = new ServiceHandler(handlerThread.getLooper()); + listener = new Listener(); + } + + @Override + public void onDataChanged(DataEventBuffer dataEvents) { + } + + @Override + public void onDestroy() { + synchronized (lock) { + if (serviceHandler == null) { + throw new IllegalStateException("serviceHandler not set, did you override onCreate() but forget to call super.onCreate()?"); + } + serviceHandler.getLooper().quit(); + } + super.onDestroy(); + } + + @PublicApi(exclude = true) + public void onEntityUpdate(AmsEntityUpdate entityUpdate) { + } + + @Override + public void onInputClosed(Channel channel, @ChannelApi.CloseReason int closeReason, int appSpecificErrorCode) { + } + + @Override + public void onMessageReceived(MessageEvent messageEvent) { + } + + @PublicApi(exclude = true) + public void onNotificationReceived(AncsNotification notification) { + } + + @Override + public void onOutputClosed(Channel channel, @ChannelApi.CloseReason int closeReason, int appSpecificErrorCode) { + } + + @Override + public void onPeerConnected(Node peer) { + } + + @Override + public void onPeerDisconnected(Node peer) { + } + + private class Listener extends IWearableListener.Stub { + private int knownGoodUid = -1; + + private boolean post(Runnable runnable) { + int callingUid = Binder.getCallingUid(); + if (callingUid != knownGoodUid) { + // TODO: Verify Gms is calling + String[] packagesForUid = getPackageManager().getPackagesForUid(callingUid); + if (packagesForUid != null) { + if (Arrays.asList(packagesForUid).contains(GMS_PACKAGE_NAME)) { + knownGoodUid = callingUid; + } else { + throw new SecurityException("Caller is not Services Core"); + } + } + } + synchronized (lock) { + if (disconnected) { + return false; + } + serviceHandler.post(runnable); + return true; + } + } + + @Override + public void onDataChanged(final DataHolder data) throws RemoteException { + post(new Runnable() { + @Override + public void run() { + WearableListenerService.this.onDataChanged(new DataEventBuffer(data)); + } + }); + } + + @Override + public void onMessageReceived(final MessageEventParcelable messageEvent) throws RemoteException { + post(new Runnable() { + @Override + public void run() { + WearableListenerService.this.onMessageReceived(messageEvent); + } + }); + } + + @Override + public void onPeerConnected(final NodeParcelable node) throws RemoteException { + post(new Runnable() { + @Override + public void run() { + WearableListenerService.this.onPeerConnected(node); + } + }); + } + + @Override + public void onPeerDisconnected(final NodeParcelable node) throws RemoteException { + post(new Runnable() { + @Override + public void run() { + WearableListenerService.this.onPeerDisconnected(node); + } + }); + } + + @Override + public void onConnectedNodes(final List nodes) throws RemoteException { + post(new Runnable() { + @Override + public void run() { + WearableListenerService.this.onConnectedNodes(new ArrayList(nodes)); + } + }); + } + + @Override + public void onConnectedCapabilityChanged(final CapabilityInfoParcelable capabilityInfo) throws RemoteException { + post(new Runnable() { + @Override + public void run() { + WearableListenerService.this.onCapabilityChanged(capabilityInfo); + } + }); + } + + @Override + public void onNotificationReceived(final AncsNotificationParcelable notification) throws RemoteException { + post(new Runnable() { + @Override + public void run() { + WearableListenerService.this.onNotificationReceived(notification); + } + }); + } + + @Override + public void onEntityUpdate(final AmsEntityUpdateParcelable update) throws RemoteException { + post(new Runnable() { + @Override + public void run() { + WearableListenerService.this.onEntityUpdate(update); + } + }); + } + + @Override + public void onChannelEvent(final ChannelEventParcelable channelEvent) throws RemoteException { + post(new Runnable() { + @Override + public void run() { + switch (channelEvent.eventType) { + case 1: + WearableListenerService.this.onChannelOpened(new ChannelImpl(channelEvent.channel)); + break; + case 2: + WearableListenerService.this.onChannelClosed(new ChannelImpl(channelEvent.channel), channelEvent.closeReason, channelEvent.appSpecificErrorCode); + break; + case 3: + WearableListenerService.this.onInputClosed(new ChannelImpl(channelEvent.channel), channelEvent.closeReason, channelEvent.appSpecificErrorCode); + break; + case 4: + WearableListenerService.this.onOutputClosed(new ChannelImpl(channelEvent.channel), channelEvent.closeReason, channelEvent.appSpecificErrorCode); + break; + default: + Log.w(TAG, "Unknown ChannelEvent.eventType"); + } + } + }); + } + } + + private class ServiceHandler extends Handler { + public ServiceHandler(Looper looper) { + super(looper); + } + } +} diff --git a/play-services-wearable/src/main/java/org/microg/gms/wearable/BaseWearableCallbacks.java b/play-services-wearable/src/main/java/org/microg/gms/wearable/BaseWearableCallbacks.java new file mode 100644 index 00000000..14778527 --- /dev/null +++ b/play-services-wearable/src/main/java/org/microg/gms/wearable/BaseWearableCallbacks.java @@ -0,0 +1,201 @@ +/* + * 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.wearable; + +import android.os.RemoteException; +import android.util.Log; + +import com.google.android.gms.common.api.Status; +import com.google.android.gms.common.data.DataHolder; +import com.google.android.gms.wearable.internal.AddLocalCapabilityResponse; +import com.google.android.gms.wearable.internal.ChannelReceiveFileResponse; +import com.google.android.gms.wearable.internal.ChannelSendFileResponse; +import com.google.android.gms.wearable.internal.CloseChannelResponse; +import com.google.android.gms.wearable.internal.DeleteDataItemsResponse; +import com.google.android.gms.wearable.internal.GetAllCapabilitiesResponse; +import com.google.android.gms.wearable.internal.GetCapabilityResponse; +import com.google.android.gms.wearable.internal.GetChannelInputStreamResponse; +import com.google.android.gms.wearable.internal.GetChannelOutputStreamResponse; +import com.google.android.gms.wearable.internal.GetCloudSyncOptInOutDoneResponse; +import com.google.android.gms.wearable.internal.GetCloudSyncOptInStatusResponse; +import com.google.android.gms.wearable.internal.GetCloudSyncSettingResponse; +import com.google.android.gms.wearable.internal.GetConfigResponse; +import com.google.android.gms.wearable.internal.GetConfigsResponse; +import com.google.android.gms.wearable.internal.GetConnectedNodesResponse; +import com.google.android.gms.wearable.internal.GetDataItemResponse; +import com.google.android.gms.wearable.internal.GetFdForAssetResponse; +import com.google.android.gms.wearable.internal.GetLocalNodeResponse; +import com.google.android.gms.wearable.internal.IWearableCallbacks; +import com.google.android.gms.wearable.internal.OpenChannelResponse; +import com.google.android.gms.wearable.internal.PutDataResponse; +import com.google.android.gms.wearable.internal.RemoveLocalCapabilityResponse; +import com.google.android.gms.wearable.internal.SendMessageResponse; +import com.google.android.gms.wearable.internal.StorageInfoResponse; + +public class BaseWearableCallbacks extends IWearableCallbacks.Stub { + private static final String TAG = "GmsWearBaseCallback"; + + @Override + public void onGetConfigResponse(GetConfigResponse response) throws RemoteException { + Log.d(TAG, "unimplemented Method: onGetConfigResponse"); + + } + + @Override + public void onPutDataResponse(PutDataResponse response) throws RemoteException { + Log.d(TAG, "unimplemented Method: onPutDataResponse"); + + } + + @Override + public void onGetDataItemResponse(GetDataItemResponse response) throws RemoteException { + Log.d(TAG, "unimplemented Method: onGetDataItemResponse"); + + } + + @Override + public void onDataItemChanged(DataHolder dataHolder) throws RemoteException { + Log.d(TAG, "unimplemented Method: onDataItemChanged"); + + } + + @Override + public void onDeleteDataItemsResponse(DeleteDataItemsResponse response) throws RemoteException { + Log.d(TAG, "unimplemented Method: onDeleteDataItemsResponse"); + + } + + @Override + public void onSendMessageResponse(SendMessageResponse response) throws RemoteException { + Log.d(TAG, "unimplemented Method: onSendMessageResponse"); + + } + + @Override + public void onGetFdForAssetResponse(GetFdForAssetResponse response) throws RemoteException { + Log.d(TAG, "unimplemented Method: onGetFdForAssetResponse"); + + } + + @Override + public void onGetLocalNodeResponse(GetLocalNodeResponse response) throws RemoteException { + Log.d(TAG, "unimplemented Method: onGetLocalNodeResponse"); + + } + + @Override + public void onGetConnectedNodesResponse(GetConnectedNodesResponse response) throws RemoteException { + Log.d(TAG, "unimplemented Method: onGetConnectedNodesResponse"); + + } + + @Override + public void onOpenChannelResponse(OpenChannelResponse response) throws RemoteException { + Log.d(TAG, "unimplemented Method: onOpenChannelResponse"); + + } + + @Override + public void onCloseChannelResponse(CloseChannelResponse response) throws RemoteException { + Log.d(TAG, "unimplemented Method: onCloseChannelResponse"); + + } + + @Override + public void onGetChannelInputStreamResponse(GetChannelInputStreamResponse response) throws RemoteException { + Log.d(TAG, "unimplemented Method: onGetChannelInputStreamResponse"); + + } + + @Override + public void onGetChannelOutputStreamResponse(GetChannelOutputStreamResponse response) throws RemoteException { + Log.d(TAG, "unimplemented Method: onGetChannelOutputStreamResponse"); + + } + + @Override + public void onChannelReceiveFileResponse(ChannelReceiveFileResponse response) throws RemoteException { + Log.d(TAG, "unimplemented Method: onChannelReceiveFileResponse"); + + } + + @Override + public void onChannelSendFileResponse(ChannelSendFileResponse response) throws RemoteException { + Log.d(TAG, "unimplemented Method: onChannelSendFileResponse"); + + } + + @Override + public void onStatus(Status status) throws RemoteException { + Log.d(TAG, "unimplemented Method: onStatus"); + + } + + @Override + public void onStorageInfoResponse(StorageInfoResponse response) throws RemoteException { + Log.d(TAG, "unimplemented Method: onStorageInfoResponse"); + + } + + @Override + public void onGetCapabilityResponse(GetCapabilityResponse response) throws RemoteException { + Log.d(TAG, "unimplemented Method: onGetCapabilityResponse"); + + } + + @Override + public void onGetAllCapabilitiesResponse(GetAllCapabilitiesResponse response) throws RemoteException { + Log.d(TAG, "unimplemented Method: onGetAllCapabilitiesResponse"); + + } + + @Override + public void onAddLocalCapabilityResponse(AddLocalCapabilityResponse response) throws RemoteException { + Log.d(TAG, "unimplemented Method: onAddLocalCapabilityResponse"); + + } + + @Override + public void onRemoveLocalCapabilityResponse(RemoveLocalCapabilityResponse response) throws RemoteException { + Log.d(TAG, "unimplemented Method: onRemoveLocalCapabilityResponse"); + + } + + @Override + public void onGetConfigsResponse(GetConfigsResponse response) throws RemoteException { + Log.d(TAG, "unimplemented Method: onGetConfigsResponse"); + + } + + @Override + public void onGetCloudSyncOptInOutDoneResponse(GetCloudSyncOptInOutDoneResponse response) throws RemoteException { + Log.d(TAG, "unimplemented Method: onGetCloudSyncOptInOutDoneResponse"); + + } + + @Override + public void onGetCloudSyncSettingResponse(GetCloudSyncSettingResponse response) throws RemoteException { + Log.d(TAG, "unimplemented Method: onGetCloudSyncSettingResponse"); + + } + + @Override + public void onGetCloudSyncOptInStatusResponse(GetCloudSyncOptInStatusResponse response) throws RemoteException { + Log.d(TAG, "unimplemented Method: onGetCloudSyncOptInStatusResponse"); + + } +} diff --git a/play-services-wearable/src/main/java/org/microg/gms/wearable/ChannelImpl.java b/play-services-wearable/src/main/java/org/microg/gms/wearable/ChannelImpl.java new file mode 100644 index 00000000..ddf4fff5 --- /dev/null +++ b/play-services-wearable/src/main/java/org/microg/gms/wearable/ChannelImpl.java @@ -0,0 +1,107 @@ +/* + * 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.wearable; + +import android.net.Uri; +import android.util.Log; + +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.PendingResult; +import com.google.android.gms.common.api.Status; +import com.google.android.gms.wearable.Channel; +import com.google.android.gms.wearable.ChannelApi; +import com.google.android.gms.wearable.internal.ChannelParcelable; + +public class ChannelImpl extends ChannelParcelable implements Channel { + private static final String TAG = "GmsWearChannelImpl"; + + public ChannelImpl(String token, String nodeId, String path) { + super(token, nodeId, path); + } + + public ChannelImpl(ChannelParcelable wrapped) { + this(wrapped.token, wrapped.nodeId, wrapped.path); + } + + + @Override + public PendingResult addListener(GoogleApiClient client, ChannelApi.ChannelListener listener) { + Log.d(TAG, "unimplemented Method: addListener"); + return null; + } + + @Override + public PendingResult close(GoogleApiClient client, int errorCode) { + Log.d(TAG, "unimplemented Method: close"); + return null; + } + + @Override + public PendingResult close(GoogleApiClient client) { + Log.d(TAG, "unimplemented Method: close"); + return null; + } + + @Override + public PendingResult getInputStream(GoogleApiClient client) { + Log.d(TAG, "unimplemented Method: getInputStream"); + return null; + } + + @Override + public PendingResult getOutputStream(GoogleApiClient client) { + Log.d(TAG, "unimplemented Method: getOutputStream"); + return null; + } + + public String getNodeId() { + return nodeId; + } + + @Override + public String getPath() { + return path; + } + + public String getToken() { + return token; + } + + @Override + public PendingResult receiveFile(GoogleApiClient client, Uri uri, boolean append) { + Log.d(TAG, "unimplemented Method: receiveFile"); + return null; + } + + @Override + public PendingResult removeListener(GoogleApiClient client, ChannelApi.ChannelListener listener) { + Log.d(TAG, "unimplemented Method: removeListener"); + return null; + } + + @Override + public PendingResult sendFile(GoogleApiClient client, Uri uri) { + Log.d(TAG, "unimplemented Method: sendFile"); + return null; + } + + @Override + public PendingResult sendFile(GoogleApiClient client, Uri uri, long startOffset, long length) { + Log.d(TAG, "unimplemented Method: sendFile"); + return null; + } +} diff --git a/play-services-wearable/src/main/java/org/microg/gms/wearable/DataApiImpl.java b/play-services-wearable/src/main/java/org/microg/gms/wearable/DataApiImpl.java new file mode 100644 index 00000000..25b9c835 --- /dev/null +++ b/play-services-wearable/src/main/java/org/microg/gms/wearable/DataApiImpl.java @@ -0,0 +1,75 @@ +/* + * 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.wearable; + +import android.net.Uri; + +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.PendingResult; +import com.google.android.gms.common.api.Status; +import com.google.android.gms.wearable.Asset; +import com.google.android.gms.wearable.DataApi; +import com.google.android.gms.wearable.DataItemAsset; +import com.google.android.gms.wearable.DataItemBuffer; +import com.google.android.gms.wearable.internal.PutDataRequest; + +public class DataApiImpl implements DataApi { + @Override + public PendingResult addListener(GoogleApiClient client, DataListener listener) { + throw new UnsupportedOperationException(); + } + + @Override + public PendingResult deleteDataItems(GoogleApiClient client, Uri uri) { + throw new UnsupportedOperationException(); + } + + @Override + public PendingResult getDataItem(GoogleApiClient client, Uri uri) { + throw new UnsupportedOperationException(); + } + + @Override + public PendingResult getDataItems(GoogleApiClient client) { + throw new UnsupportedOperationException(); + } + + @Override + public PendingResult getDataItems(GoogleApiClient client, Uri uri) { + throw new UnsupportedOperationException(); + } + + @Override + public PendingResult getFdForAsset(GoogleApiClient client, DataItemAsset asset) { + throw new UnsupportedOperationException(); + } + + @Override + public PendingResult getFdForAsset(GoogleApiClient client, Asset asset) { + throw new UnsupportedOperationException(); + } + + @Override + public PendingResult putDataItem(GoogleApiClient client, PutDataRequest request) { + throw new UnsupportedOperationException(); + } + + @Override + public PendingResult removeListener(GoogleApiClient client, DataListener listener) { + throw new UnsupportedOperationException(); + } +} diff --git a/play-services-wearable/src/main/java/org/microg/gms/wearable/MessageApiImpl.java b/play-services-wearable/src/main/java/org/microg/gms/wearable/MessageApiImpl.java new file mode 100644 index 00000000..7f9d5fc1 --- /dev/null +++ b/play-services-wearable/src/main/java/org/microg/gms/wearable/MessageApiImpl.java @@ -0,0 +1,73 @@ +/* + * 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.wearable; + +import android.os.RemoteException; + +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.PendingResult; +import com.google.android.gms.common.api.Status; +import com.google.android.gms.wearable.MessageApi; +import com.google.android.gms.wearable.Wearable; +import com.google.android.gms.wearable.internal.SendMessageResponse; + +import org.microg.gms.common.GmsConnector; + +public class MessageApiImpl implements MessageApi { + @Override + public PendingResult addListener(GoogleApiClient client, MessageListener listener) { + throw new UnsupportedOperationException(); + } + + @Override + public PendingResult removeListener(GoogleApiClient client, MessageListener listener) { + throw new UnsupportedOperationException(); + } + + @Override + public PendingResult sendMessage(GoogleApiClient client, final String nodeId, final String path, final byte[] data) { + return GmsConnector.call(client, Wearable.API, new GmsConnector.Callback() { + @Override + public void onClientAvailable(WearableClientImpl client, final ResultProvider resultProvider) throws RemoteException { + client.getServiceInterface().sendMessage(new BaseWearableCallbacks() { + @Override + public void onSendMessageResponse(SendMessageResponse response) throws RemoteException { + resultProvider.onResultAvailable(new SendMessageResultImpl(response)); + } + }, nodeId, path, data); + } + }); + } + + public static class SendMessageResultImpl implements SendMessageResult { + private SendMessageResponse response; + + public SendMessageResultImpl(SendMessageResponse response) { + this.response = response; + } + + @Override + public int getRequestId() { + return response.requestId; + } + + @Override + public Status getStatus() { + return new Status(response.statusCode); + } + } +} diff --git a/play-services-wearable/src/main/java/org/microg/gms/wearable/NodeApiImpl.java b/play-services-wearable/src/main/java/org/microg/gms/wearable/NodeApiImpl.java new file mode 100644 index 00000000..197b6e81 --- /dev/null +++ b/play-services-wearable/src/main/java/org/microg/gms/wearable/NodeApiImpl.java @@ -0,0 +1,44 @@ +/* + * 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.wearable; + +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.PendingResult; +import com.google.android.gms.common.api.Status; +import com.google.android.gms.wearable.NodeApi; + +public class NodeApiImpl implements NodeApi { + @Override + public PendingResult addListener(GoogleApiClient client, NodeListener listener) { + throw new UnsupportedOperationException(); + } + + @Override + public PendingResult getConnectedNodes(GoogleApiClient client) { + throw new UnsupportedOperationException(); + } + + @Override + public PendingResult getLocalNode(GoogleApiClient client) { + throw new UnsupportedOperationException(); + } + + @Override + public PendingResult removeListener(GoogleApiClient client, NodeListener listener) { + throw new UnsupportedOperationException(); + } +} diff --git a/play-services-wearable/src/main/java/org/microg/gms/wearable/WearableApiBuilder.java b/play-services-wearable/src/main/java/org/microg/gms/wearable/WearableApiBuilder.java new file mode 100644 index 00000000..3c20481b --- /dev/null +++ b/play-services-wearable/src/main/java/org/microg/gms/wearable/WearableApiBuilder.java @@ -0,0 +1,38 @@ +/* + * 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.wearable; + +import android.content.Context; +import android.os.Looper; + +import com.google.android.gms.common.api.AccountInfo; +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.wearable.Wearable; + +import org.microg.gms.common.api.ApiBuilder; +import org.microg.gms.common.api.ApiConnection; + +public class WearableApiBuilder implements ApiBuilder { + private static final String TAG = "GmsWearableApi"; + + @Override + public ApiConnection build(Context context, Looper looper, Wearable.WearableOptions options, + AccountInfo accountInfo, GoogleApiClient.ConnectionCallbacks callbacks, + GoogleApiClient.OnConnectionFailedListener connectionFailedListener) { + return new WearableClientImpl(context, options, callbacks, connectionFailedListener); + } +} diff --git a/play-services-wearable/src/main/java/org/microg/gms/wearable/WearableClientImpl.java b/play-services-wearable/src/main/java/org/microg/gms/wearable/WearableClientImpl.java new file mode 100644 index 00000000..5a667f58 --- /dev/null +++ b/play-services-wearable/src/main/java/org/microg/gms/wearable/WearableClientImpl.java @@ -0,0 +1,53 @@ +/* + * 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.wearable; + +import android.content.Context; +import android.os.IBinder; +import android.util.Log; + +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.wearable.Wearable; +import com.google.android.gms.wearable.internal.IWearableService; + +import org.microg.gms.common.GmsClient; +import org.microg.gms.common.GmsService; +import org.microg.gms.common.api.GoogleApiClientImpl; + +public class WearableClientImpl extends GmsClient { + private static final String TAG = "GmsWearClient"; + + public WearableClientImpl(Context context, Wearable.WearableOptions options, GoogleApiClient.ConnectionCallbacks callbacks, GoogleApiClient.OnConnectionFailedListener connectionFailedListener) { + super(context, callbacks, connectionFailedListener, GmsService.WEARABLE.ACTION); + serviceId = GmsService.WEARABLE.SERVICE_ID; + if (options != null && options.firstPartyMode) + extras.putBoolean("firstPartyMode", true); + Log.d(TAG, ""); + } + + @Override + protected IWearableService interfaceFromBinder(IBinder binder) { + return IWearableService.Stub.asInterface(binder); + } + + public static WearableClientImpl get(GoogleApiClient apiClient) { + if (apiClient instanceof GoogleApiClientImpl) { + return (WearableClientImpl) ((GoogleApiClientImpl) apiClient).getApiConnection(Wearable.API); + } + return null; + } +} diff --git a/play-services-wearable/src/main/java/org/microg/gms/wearable/databundle/DataBundleUtil.java b/play-services-wearable/src/main/java/org/microg/gms/wearable/databundle/DataBundleUtil.java new file mode 100644 index 00000000..10975f9d --- /dev/null +++ b/play-services-wearable/src/main/java/org/microg/gms/wearable/databundle/DataBundleUtil.java @@ -0,0 +1,658 @@ +/* + * 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.wearable.databundle; + +import android.util.SparseArray; + +import com.google.android.gms.wearable.Asset; +import com.google.android.gms.wearable.DataMap; +import com.squareup.wire.Wire; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import okio.ByteString; + +public class DataBundleUtil { + + public static DataMap readDataMap(byte[] bytes, List assets) { + try { + return readDataMap(new Wire().parseFrom(bytes, DataBundle.class), assets); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public static DataMap readDataMap(DataBundle dataBundle, List assets) { + return readDataMap(dataBundle.entries, assets); + } + + public static DataMap readDataMap(List entries, List assets) { + DataMap dataMap = new DataMap(); + for (DataBundleEntry entry : entries) { + readAndStore(dataMap, entry.key, entry.typedValue, assets); + } + return dataMap; + } + + public static byte[] createBytes(DataMap dataMap) { + AssetAnnotatedDataBundle dataBundle = createDataBundle(dataMap); + if (!dataBundle.getAssets().isEmpty()) { + throw new UnsupportedOperationException(); + } + return dataBundle.getData(); + } + + public static AssetAnnotatedDataBundle createDataBundle(DataMap dataMap) { + AssetAnnotatedDataBundle dataBundle = new AssetAnnotatedDataBundle(); + dataBundle.assets = new ArrayList(); + dataBundle.dataBundle = new DataBundle(createEntryList(dataMap, dataBundle.assets)); + return dataBundle; + } + + private static List createEntryList(DataMap dataMap, List assets) { + List entries = new ArrayList(); + for (String key : dataMap.keySet()) { + entries.add(getTypeHelper(dataMap.getType(key)).loadAndCreateEntry(dataMap, key, assets)); + } + return entries; + } + + private static void readAndStore(DataMap dataMap, String key, DataBundleTypedValue value, List assets) { + if (value.type == null) return; + getTypeHelper(value.type).readAndStore(dataMap, key, value, assets); + } + + + private static SparseArray typeHelperByCode; + + private static void rememberTypeReader(TypeHelper typeHelper) { + typeHelperByCode.put(typeHelper.type, typeHelper); + } + + static TypeHelper getTypeHelper(int type) { + if (typeHelperByCode.get(type) != null) { + return typeHelperByCode.get(type); + } else { + throw new IllegalArgumentException(); + } + } + + static TypeHelper getTypeHelper(DataMap.StoredType type) { + return getTypeHelper(type.getTypeCode()); + } + + static TypeHelper getListInnerTypeHelper(DataMap.StoredType type) { + return getTypeHelper(type.getListType()); + } + + public static final int BYTE_ARRAY_TYPE_CODE = 1; + static TypeHelper BYTEARRAY = new TypeHelper(BYTE_ARRAY_TYPE_CODE) { + @Override + byte[] read(DataBundleValue value, List assets) { + return value.byteArray.toByteArray(); + } + + @Override + DataBundleValue create(byte[] value, List assets) { + return new DataBundleValue.Builder().byteArray(ByteString.of(value)).build(); + } + + @Override + void store(DataMap dataMap, String key, byte[] value) { + dataMap.putByteArray(key, value); + } + + @Override + byte[] load(DataMap dataMap, String key) { + return dataMap.getByteArray(key); + } + }; + + public static final int STRING_TYPE_CODE = 2; + static TypeHelper STRING = new TypeHelper(STRING_TYPE_CODE) { + @Override + String read(DataBundleValue value, List assets) { + return value.stringVal; + } + + @Override + DataBundleValue create(String value, List assets) { + return new DataBundleValue.Builder().stringVal(value).build(); + } + + @Override + void store(DataMap dataMap, String key, String value) { + if (value != null) dataMap.putString(key, value); + } + + @Override + void storeList(DataMap dataMap, String key, ArrayList valueList) { + dataMap.putStringArrayList(key, valueList); + } + + @Override + String load(DataMap dataMap, String key) { + return dataMap.getString(key); + } + + @Override + AnnotatedArrayList loadList(DataMap dataMap, String key) { + AnnotatedArrayList list = new AnnotatedArrayList(this); + list.addAll(dataMap.getStringArrayList(key)); + return list; + } + }; + + public static final int DOUBLE_TYPE_CODE = 3; + static TypeHelper DOUBLE = new TypeHelper(DOUBLE_TYPE_CODE) { + @Override + Double read(DataBundleValue value, List assets) { + return value.doubleVal; + } + + @Override + DataBundleValue create(Double value, List assets) { + return new DataBundleValue.Builder().doubleVal(value).build(); + } + + @Override + void store(DataMap dataMap, String key, Double value) { + if (value != null) dataMap.putDouble(key, value); + } + + @Override + Double load(DataMap dataMap, String key) { + return dataMap.getDouble(key); + } + }; + + public static final int FLOAT_TYPE_CODE = 4; + static TypeHelper FLOAT = new TypeHelper(FLOAT_TYPE_CODE) { + @Override + Float read(DataBundleValue value, List assets) { + return value.floatVal; + } + + @Override + DataBundleValue create(Float value, List assets) { + return new DataBundleValue.Builder().floatVal(value).build(); + } + + @Override + void store(DataMap dataMap, String key, Float value) { + if (value != null) dataMap.putFloat(key, value); + } + + @Override + Float load(DataMap dataMap, String key) { + return dataMap.getFloat(key); + } + }; + + public static final int LONG_TYPE_CODE = 5; + static TypeHelper LONG = new TypeHelper(LONG_TYPE_CODE) { + @Override + Long read(DataBundleValue value, List assets) { + return value.longVal; + } + + @Override + DataBundleValue create(Long value, List assets) { + return new DataBundleValue.Builder().longVal(value).build(); + } + + @Override + void store(DataMap dataMap, String key, Long value) { + if (value != null) dataMap.putLong(key, value); + } + + @Override + Long load(DataMap dataMap, String key) { + return dataMap.getLong(key); + } + }; + + public static final int INTEGER_TYPE_CODE = 6; + static TypeHelper INTEGER = new TypeHelper(INTEGER_TYPE_CODE) { + @Override + Integer read(DataBundleValue value, List assets) { + return value.intVal; + } + + @Override + DataBundleValue create(Integer value, List assets) { + return new DataBundleValue.Builder().intVal(value).build(); + } + + @Override + void store(DataMap dataMap, String key, Integer value) { + if (value != null) dataMap.putInt(key, value); + } + + @Override + void storeList(DataMap dataMap, String key, ArrayList valueList) { + dataMap.putIntegerArrayList(key, valueList); + } + + @Override + Integer load(DataMap dataMap, String key) { + return dataMap.getInt(key); + } + + @Override + AnnotatedArrayList loadList(DataMap dataMap, String key) { + AnnotatedArrayList list = new AnnotatedArrayList(this); + list.addAll(dataMap.getIntegerArrayList(key)); + return list; + } + }; + + public static final int BYTE_TYPE_CODE = 7; + static TypeHelper BYTE = new TypeHelper(BYTE_TYPE_CODE) { + @Override + Byte read(DataBundleValue value, List assets) { + return (byte) (int) value.byteVal; + } + + @Override + DataBundleValue create(Byte value, List assets) { + return new DataBundleValue.Builder().byteVal((int) (byte) value).build(); + } + + @Override + void store(DataMap dataMap, String key, Byte value) { + if (value != null) dataMap.putByte(key, value); + } + + @Override + Byte load(DataMap dataMap, String key) { + return dataMap.getByte(key); + } + }; + + public static final int BOOLEAN_TYPE_CODE = 8; + static TypeHelper BOOLEAN = new TypeHelper(BOOLEAN_TYPE_CODE) { + @Override + Boolean read(DataBundleValue value, List assets) { + return value.booleanVal; + } + + @Override + DataBundleValue create(Boolean value, List assets) { + return new DataBundleValue.Builder().booleanVal(value).build(); + } + + @Override + void store(DataMap dataMap, String key, Boolean value) { + if (value != null) dataMap.putBoolean(key, value); + } + + @Override + Boolean load(DataMap dataMap, String key) { + return dataMap.getBoolean(key); + } + }; + + public static final int DATAMAP_TYPE_CODE = 9; + static TypeHelper DATAMAP = new TypeHelper(DATAMAP_TYPE_CODE) { + @Override + DataMap read(DataBundleValue value, List assets) { + return readDataMap(value.map, assets); + } + + @Override + DataBundleValue create(DataMap value, List assets) { + return new DataBundleValue.Builder().map(createEntryList(value, assets)).build(); + } + + @Override + void store(DataMap dataMap, String key, DataMap value) { + dataMap.putDataMap(key, value); + } + + @Override + void storeList(DataMap dataMap, String key, ArrayList valueList) { + dataMap.putDataMapArrayList(key, valueList); + } + + @Override + DataMap load(DataMap dataMap, String key) { + return dataMap.getDataMap(key); + } + + @Override + AnnotatedArrayList loadList(DataMap dataMap, String key) { + AnnotatedArrayList list = new AnnotatedArrayList(this); + list.addAll(dataMap.getDataMapArrayList(key)); + return list; + } + }; + + public static final int LIST_TYPE_CODE = 10; + static TypeHelper> ARRAYLIST = new TypeHelper>(LIST_TYPE_CODE) { + @Override + AnnotatedArrayList read(DataBundleValue value, List assets) { + TypeHelper innerTypeHelper = NULL; + for (DataBundleTypedValue typedValue : value.list) { + if (innerTypeHelper == NULL) { + innerTypeHelper = getTypeHelper(typedValue.type); + } else if (typedValue.type != innerTypeHelper.type && typedValue.type != NULL.type) { + throw new IllegalArgumentException("List has elements of different types: " + innerTypeHelper.type + " and " + typedValue.type); + } + } + return innerTypeHelper.readList(value.list, assets); + } + + @Override + DataBundleValue create(AnnotatedArrayList value, List assets) { + return new DataBundleValue.Builder().list(value.createList(assets)).build(); + } + + @Override + void store(DataMap dataMap, String key, AnnotatedArrayList value) { + value.store(dataMap, key); + } + + @Override + AnnotatedArrayList load(DataMap dataMap, String key) { + return getListInnerTypeHelper(dataMap.getType(key)).loadList(dataMap, key); + } + }; + + public static final int STRING_ARRAY_TYPE_CODE = 11; + static TypeHelper STRINGARRAY = new TypeHelper(STRING_ARRAY_TYPE_CODE) { + @Override + String[] read(DataBundleValue value, List assets) { + return value.stringArray.toArray(new String[value.stringArray.size()]); + } + + @Override + DataBundleValue create(String[] value, List assets) { + return new DataBundleValue.Builder().stringArray(Arrays.asList(value)).build(); + } + + @Override + void store(DataMap dataMap, String key, String[] value) { + dataMap.putStringArray(key, value); + } + + @Override + String[] load(DataMap dataMap, String key) { + return dataMap.getStringArray(key); + } + }; + + public static final int LONG_ARRAY_TYPE_CODE = 12; + static TypeHelper LONGARRAY = new TypeHelper(LONG_ARRAY_TYPE_CODE) { + @Override + long[] read(DataBundleValue value, List assets) { + long[] longArr = new long[value.longArray.size()]; + for (int i = 0; i < value.longArray.size(); i++) { + longArr[i] = value.longArray.get(i); + } + return longArr; + } + + @Override + DataBundleValue create(long[] value, List assets) { + List longList = new ArrayList(value.length); + for (long l : value) { + longList.add(l); + } + return new DataBundleValue.Builder().longArray(longList).build(); + } + + @Override + void store(DataMap dataMap, String key, long[] value) { + dataMap.putLongArray(key, value); + } + + @Override + long[] load(DataMap dataMap, String key) { + return dataMap.getLongArray(key); + } + }; + + public static final int ASSET_TYPE_CODE = 13; + static TypeHelper ASSET = new TypeHelper(ASSET_TYPE_CODE) { + @Override + Asset read(DataBundleValue value, List assets) { + return assets.get(value.assetIndex); + } + + @Override + DataBundleValue create(Asset value, List assets) { + int index; + if (assets.contains(value)) { + index = assets.indexOf(value); + } else { + index = assets.size(); + assets.add(value); + } + return new DataBundleValue.Builder().assetIndex(index).build(); + } + + @Override + void store(DataMap dataMap, String key, Asset value) { + dataMap.putAsset(key, value); + } + + @Override + Asset load(DataMap dataMap, String key) { + return dataMap.getAsset(key); + } + }; + + public static final int NULL_TYPE_CODE = 14; + static TypeHelper NULL = new TypeHelper(NULL_TYPE_CODE) { + @Override + String read(DataBundleValue value, List assets) { + return null; + } + + @Override + DataBundleValue create(String value, List assets) { + return new DataBundleValue.Builder().build(); + } + + @Override + void store(DataMap dataMap, String key, String value) { + dataMap.putString(key, value); + } + + @Override + void storeList(DataMap dataMap, String key, ArrayList valueList) { + dataMap.putStringArrayList(key, valueList); + } + + @Override + String load(DataMap dataMap, String key) { + return null; + } + + @Override + AnnotatedArrayList loadList(DataMap dataMap, String key) { + AnnotatedArrayList list = new AnnotatedArrayList(this); + list.addAll(dataMap.getStringArrayList(key)); + return list; + } + }; + + public static final int FLOAT_ARRAY_TYPE_CODE = 15; + static TypeHelper FLOATARRAY = new TypeHelper(FLOAT_ARRAY_TYPE_CODE) { + @Override + float[] read(DataBundleValue value, List assets) { + float[] floatArr = new float[value.floatArray.size()]; + for (int i = 0; i < value.floatArray.size(); i++) { + floatArr[i] = value.floatArray.get(i); + } + return floatArr; + } + + @Override + DataBundleValue create(float[] value, List assets) { + List floatList = new ArrayList(value.length); + for (float f : value) { + floatList.add(f); + } + return new DataBundleValue.Builder().floatArray(floatList).build(); + } + + @Override + void store(DataMap dataMap, String key, float[] value) { + dataMap.putFloatArray(key, value); + } + + @Override + float[] load(DataMap dataMap, String key) { + return dataMap.getFloatArray(key); + } + }; + + static { + typeHelperByCode = new SparseArray(); + rememberTypeReader(BYTEARRAY); + rememberTypeReader(STRING); + rememberTypeReader(DOUBLE); + rememberTypeReader(FLOAT); + rememberTypeReader(LONG); + rememberTypeReader(INTEGER); + rememberTypeReader(BYTE); + rememberTypeReader(BOOLEAN); + rememberTypeReader(DATAMAP); + rememberTypeReader(ARRAYLIST); + rememberTypeReader(STRINGARRAY); + rememberTypeReader(LONGARRAY); + rememberTypeReader(ASSET); + rememberTypeReader(NULL); + rememberTypeReader(FLOATARRAY); + } + + static class AssetAnnotatedDataBundle { + private DataBundle dataBundle; + private List assets; + + public List getAssets() { + return assets; + } + + public byte[] getData() { + return dataBundle.toByteArray(); + } + } + + static class AnnotatedArrayList extends ArrayList { + private TypeHelper innerType; + + public AnnotatedArrayList(TypeHelper innerType) { + this.innerType = innerType; + } + + void store(DataMap dataMap, String key) { + innerType.storeList(dataMap, key, this); + } + + public List createList(List assets) { + return innerType.createList(this, assets); + } + } + + static abstract class TypeHelper { + private int type; + + public TypeHelper(int type) { + this.type = type; + } + + abstract T read(DataBundleValue value, List assets); + + abstract DataBundleValue create(T value, List assets); + + T read(DataBundleTypedValue value, List assets) { + if (value.type == NULL_TYPE_CODE) { + return null; + } else if (value.type == type) { + return read(value.value, assets); + } else { + throw new IllegalArgumentException(); + } + } + + abstract void store(DataMap dataMap, String key, T value); + + void storeList(DataMap dataMap, String key, ArrayList valueList) { + throw new UnsupportedOperationException(); + } + + abstract T load(DataMap dataMap, String key); + + AnnotatedArrayList loadList(DataMap dataMap, String key) { + throw new UnsupportedOperationException(); + } + + void readAndStore(DataMap dataMap, String key, DataBundleValue value, List assets) { + store(dataMap, key, read(value, assets)); + } + + void readAndStore(DataMap dataMap, String key, DataBundleTypedValue value, List assets) { + store(dataMap, key, read(value, assets)); + } + + void readAndStore(DataMap dataMap, DataBundleEntry entry, List assets) { + readAndStore(dataMap, entry.key, entry.typedValue, assets); + } + + AnnotatedArrayList readList(List values, List assets) { + AnnotatedArrayList list = new AnnotatedArrayList(this); + for (DataBundleTypedValue value : values) { + list.add(read(value, assets)); + } + return list; + } + + List createList(AnnotatedArrayList value, List assets) { + List list = new ArrayList(); + for (T val : value) { + list.add(createTyped(val, assets)); + } + return list; + } + + void readAndStore(DataMap dataMap, String key, List values, List assets) { + storeList(dataMap, key, readList(values, assets)); + } + + DataBundleTypedValue createTyped(T value, List assets) { + return new DataBundleTypedValue(type, create(value, assets)); + } + + DataBundleValue loadAndCreate(DataMap dataMap, String key, List assets) { + return create(load(dataMap, key), assets); + } + + DataBundleTypedValue loadAndCreateTyped(DataMap dataMap, String key, List assets) { + return createTyped(load(dataMap, key), assets); + } + + DataBundleEntry loadAndCreateEntry(DataMap dataMap, String key, List assets) { + return new DataBundleEntry(key, loadAndCreateTyped(dataMap, key, assets)); + } + } +} diff --git a/play-services-wearable/src/main/protos-java/org/microg/gms/wearable/databundle/DataBundle.java b/play-services-wearable/src/main/protos-java/org/microg/gms/wearable/databundle/DataBundle.java new file mode 100644 index 00000000..ba388652 --- /dev/null +++ b/play-services-wearable/src/main/protos-java/org/microg/gms/wearable/databundle/DataBundle.java @@ -0,0 +1,64 @@ +// Code generated by Wire protocol buffer compiler, do not edit. +// Source file: protos-repo/databundle.proto +package org.microg.gms.wearable.databundle; + +import com.squareup.wire.Message; +import com.squareup.wire.ProtoField; +import java.util.Collections; +import java.util.List; + +import static com.squareup.wire.Message.Label.REPEATED; + +public final class DataBundle extends Message { + + public static final List DEFAULT_ENTRIES = Collections.emptyList(); + + @ProtoField(tag = 1, label = REPEATED, messageType = DataBundleEntry.class) + public final List entries; + + public DataBundle(List entries) { + this.entries = immutableCopyOf(entries); + } + + private DataBundle(Builder builder) { + this(builder.entries); + setBuilder(builder); + } + + @Override + public boolean equals(Object other) { + if (other == this) return true; + if (!(other instanceof DataBundle)) return false; + return equals(entries, ((DataBundle) other).entries); + } + + @Override + public int hashCode() { + int result = hashCode; + return result != 0 ? result : (hashCode = entries != null ? entries.hashCode() : 1); + } + + public static final class Builder extends Message.Builder { + + public List entries; + + public Builder() { + } + + public Builder(DataBundle message) { + super(message); + if (message == null) return; + this.entries = copyOf(message.entries); + } + + public Builder entries(List entries) { + this.entries = checkForNulls(entries); + return this; + } + + @Override + public DataBundle build() { + return new DataBundle(this); + } + } +} diff --git a/play-services-wearable/src/main/protos-java/org/microg/gms/wearable/databundle/DataBundleEntry.java b/play-services-wearable/src/main/protos-java/org/microg/gms/wearable/databundle/DataBundleEntry.java new file mode 100644 index 00000000..a5f52d38 --- /dev/null +++ b/play-services-wearable/src/main/protos-java/org/microg/gms/wearable/databundle/DataBundleEntry.java @@ -0,0 +1,80 @@ +// Code generated by Wire protocol buffer compiler, do not edit. +// Source file: protos-repo/databundle.proto +package org.microg.gms.wearable.databundle; + +import com.squareup.wire.Message; +import com.squareup.wire.ProtoField; + +import static com.squareup.wire.Message.Datatype.STRING; + +public final class DataBundleEntry extends Message { + + public static final String DEFAULT_KEY = ""; + + @ProtoField(tag = 1, type = STRING) + public final String key; + + @ProtoField(tag = 2) + public final DataBundleTypedValue typedValue; + + public DataBundleEntry(String key, DataBundleTypedValue typedValue) { + this.key = key; + this.typedValue = typedValue; + } + + private DataBundleEntry(Builder builder) { + this(builder.key, builder.typedValue); + setBuilder(builder); + } + + @Override + public boolean equals(Object other) { + if (other == this) return true; + if (!(other instanceof DataBundleEntry)) return false; + DataBundleEntry o = (DataBundleEntry) other; + return equals(key, o.key) + && equals(typedValue, o.typedValue); + } + + @Override + public int hashCode() { + int result = hashCode; + if (result == 0) { + result = key != null ? key.hashCode() : 0; + result = result * 37 + (typedValue != null ? typedValue.hashCode() : 0); + hashCode = result; + } + return result; + } + + public static final class Builder extends Message.Builder { + + public String key; + public DataBundleTypedValue typedValue; + + public Builder() { + } + + public Builder(DataBundleEntry message) { + super(message); + if (message == null) return; + this.key = message.key; + this.typedValue = message.typedValue; + } + + public Builder key(String key) { + this.key = key; + return this; + } + + public Builder typedValue(DataBundleTypedValue typedValue) { + this.typedValue = typedValue; + return this; + } + + @Override + public DataBundleEntry build() { + return new DataBundleEntry(this); + } + } +} diff --git a/play-services-wearable/src/main/protos-java/org/microg/gms/wearable/databundle/DataBundleTypedValue.java b/play-services-wearable/src/main/protos-java/org/microg/gms/wearable/databundle/DataBundleTypedValue.java new file mode 100644 index 00000000..9a7d08f6 --- /dev/null +++ b/play-services-wearable/src/main/protos-java/org/microg/gms/wearable/databundle/DataBundleTypedValue.java @@ -0,0 +1,80 @@ +// Code generated by Wire protocol buffer compiler, do not edit. +// Source file: protos-repo/databundle.proto +package org.microg.gms.wearable.databundle; + +import com.squareup.wire.Message; +import com.squareup.wire.ProtoField; + +import static com.squareup.wire.Message.Datatype.INT32; + +public final class DataBundleTypedValue extends Message { + + public static final Integer DEFAULT_TYPE = 0; + + @ProtoField(tag = 1, type = INT32) + public final Integer type; + + @ProtoField(tag = 2) + public final DataBundleValue value; + + public DataBundleTypedValue(Integer type, DataBundleValue value) { + this.type = type; + this.value = value; + } + + private DataBundleTypedValue(Builder builder) { + this(builder.type, builder.value); + setBuilder(builder); + } + + @Override + public boolean equals(Object other) { + if (other == this) return true; + if (!(other instanceof DataBundleTypedValue)) return false; + DataBundleTypedValue o = (DataBundleTypedValue) other; + return equals(type, o.type) + && equals(value, o.value); + } + + @Override + public int hashCode() { + int result = hashCode; + if (result == 0) { + result = type != null ? type.hashCode() : 0; + result = result * 37 + (value != null ? value.hashCode() : 0); + hashCode = result; + } + return result; + } + + public static final class Builder extends Message.Builder { + + public Integer type; + public DataBundleValue value; + + public Builder() { + } + + public Builder(DataBundleTypedValue message) { + super(message); + if (message == null) return; + this.type = message.type; + this.value = message.value; + } + + public Builder type(Integer type) { + this.type = type; + return this; + } + + public Builder value(DataBundleValue value) { + this.value = value; + return this; + } + + @Override + public DataBundleTypedValue build() { + return new DataBundleTypedValue(this); + } + } +} diff --git a/play-services-wearable/src/main/protos-java/org/microg/gms/wearable/databundle/DataBundleValue.java b/play-services-wearable/src/main/protos-java/org/microg/gms/wearable/databundle/DataBundleValue.java new file mode 100644 index 00000000..4b9a120a --- /dev/null +++ b/play-services-wearable/src/main/protos-java/org/microg/gms/wearable/databundle/DataBundleValue.java @@ -0,0 +1,259 @@ +// Code generated by Wire protocol buffer compiler, do not edit. +// Source file: protos-repo/databundle.proto +package org.microg.gms.wearable.databundle; + +import com.squareup.wire.Message; +import com.squareup.wire.ProtoField; +import java.util.Collections; +import java.util.List; +import okio.ByteString; + +import static com.squareup.wire.Message.Datatype.BOOL; +import static com.squareup.wire.Message.Datatype.BYTES; +import static com.squareup.wire.Message.Datatype.DOUBLE; +import static com.squareup.wire.Message.Datatype.FLOAT; +import static com.squareup.wire.Message.Datatype.INT32; +import static com.squareup.wire.Message.Datatype.INT64; +import static com.squareup.wire.Message.Datatype.STRING; +import static com.squareup.wire.Message.Label.REPEATED; + +public final class DataBundleValue extends Message { + + public static final ByteString DEFAULT_BYTEARRAY = ByteString.EMPTY; + public static final String DEFAULT_STRINGVAL = ""; + public static final Double DEFAULT_DOUBLEVAL = 0D; + public static final Float DEFAULT_FLOATVAL = 0F; + public static final Long DEFAULT_LONGVAL = 0L; + public static final Integer DEFAULT_INTVAL = 0; + public static final Integer DEFAULT_BYTEVAL = 0; + public static final Boolean DEFAULT_BOOLEANVAL = false; + public static final List DEFAULT_MAP = Collections.emptyList(); + public static final List DEFAULT_LIST = Collections.emptyList(); + public static final List DEFAULT_STRINGARRAY = Collections.emptyList(); + public static final List DEFAULT_LONGARRAY = Collections.emptyList(); + public static final Integer DEFAULT_ASSETINDEX = 0; + public static final List DEFAULT_FLOATARRAY = Collections.emptyList(); + + @ProtoField(tag = 1, type = BYTES) + public final ByteString byteArray; + + @ProtoField(tag = 2, type = STRING) + public final String stringVal; + + @ProtoField(tag = 3, type = DOUBLE) + public final Double doubleVal; + + @ProtoField(tag = 4, type = FLOAT) + public final Float floatVal; + + @ProtoField(tag = 5, type = INT64) + public final Long longVal; + + @ProtoField(tag = 6, type = INT32) + public final Integer intVal; + + @ProtoField(tag = 7, type = INT32) + public final Integer byteVal; + + @ProtoField(tag = 8, type = BOOL) + public final Boolean booleanVal; + + @ProtoField(tag = 9, label = REPEATED, messageType = DataBundleEntry.class) + public final List map; + + @ProtoField(tag = 10, label = REPEATED, messageType = DataBundleTypedValue.class) + public final List list; + + @ProtoField(tag = 11, type = STRING, label = REPEATED) + public final List stringArray; + + @ProtoField(tag = 12, type = INT64, label = REPEATED) + public final List longArray; + + @ProtoField(tag = 13, type = INT32) + public final Integer assetIndex; + + @ProtoField(tag = 14, type = FLOAT, label = REPEATED) + public final List floatArray; + + public DataBundleValue(ByteString byteArray, String stringVal, Double doubleVal, Float floatVal, Long longVal, Integer intVal, Integer byteVal, Boolean booleanVal, List map, List list, List stringArray, List longArray, Integer assetIndex, List floatArray) { + this.byteArray = byteArray; + this.stringVal = stringVal; + this.doubleVal = doubleVal; + this.floatVal = floatVal; + this.longVal = longVal; + this.intVal = intVal; + this.byteVal = byteVal; + this.booleanVal = booleanVal; + this.map = immutableCopyOf(map); + this.list = immutableCopyOf(list); + this.stringArray = immutableCopyOf(stringArray); + this.longArray = immutableCopyOf(longArray); + this.assetIndex = assetIndex; + this.floatArray = immutableCopyOf(floatArray); + } + + private DataBundleValue(Builder builder) { + this(builder.byteArray, builder.stringVal, builder.doubleVal, builder.floatVal, builder.longVal, builder.intVal, builder.byteVal, builder.booleanVal, builder.map, builder.list, builder.stringArray, builder.longArray, builder.assetIndex, builder.floatArray); + setBuilder(builder); + } + + @Override + public boolean equals(Object other) { + if (other == this) return true; + if (!(other instanceof DataBundleValue)) return false; + DataBundleValue o = (DataBundleValue) other; + return equals(byteArray, o.byteArray) + && equals(stringVal, o.stringVal) + && equals(doubleVal, o.doubleVal) + && equals(floatVal, o.floatVal) + && equals(longVal, o.longVal) + && equals(intVal, o.intVal) + && equals(byteVal, o.byteVal) + && equals(booleanVal, o.booleanVal) + && equals(map, o.map) + && equals(list, o.list) + && equals(stringArray, o.stringArray) + && equals(longArray, o.longArray) + && equals(assetIndex, o.assetIndex) + && equals(floatArray, o.floatArray); + } + + @Override + public int hashCode() { + int result = hashCode; + if (result == 0) { + result = byteArray != null ? byteArray.hashCode() : 0; + result = result * 37 + (stringVal != null ? stringVal.hashCode() : 0); + result = result * 37 + (doubleVal != null ? doubleVal.hashCode() : 0); + result = result * 37 + (floatVal != null ? floatVal.hashCode() : 0); + result = result * 37 + (longVal != null ? longVal.hashCode() : 0); + result = result * 37 + (intVal != null ? intVal.hashCode() : 0); + result = result * 37 + (byteVal != null ? byteVal.hashCode() : 0); + result = result * 37 + (booleanVal != null ? booleanVal.hashCode() : 0); + result = result * 37 + (map != null ? map.hashCode() : 1); + result = result * 37 + (list != null ? list.hashCode() : 1); + result = result * 37 + (stringArray != null ? stringArray.hashCode() : 1); + result = result * 37 + (longArray != null ? longArray.hashCode() : 1); + result = result * 37 + (assetIndex != null ? assetIndex.hashCode() : 0); + result = result * 37 + (floatArray != null ? floatArray.hashCode() : 1); + hashCode = result; + } + return result; + } + + public static final class Builder extends Message.Builder { + + public ByteString byteArray; + public String stringVal; + public Double doubleVal; + public Float floatVal; + public Long longVal; + public Integer intVal; + public Integer byteVal; + public Boolean booleanVal; + public List map; + public List list; + public List stringArray; + public List longArray; + public Integer assetIndex; + public List floatArray; + + public Builder() { + } + + public Builder(DataBundleValue message) { + super(message); + if (message == null) return; + this.byteArray = message.byteArray; + this.stringVal = message.stringVal; + this.doubleVal = message.doubleVal; + this.floatVal = message.floatVal; + this.longVal = message.longVal; + this.intVal = message.intVal; + this.byteVal = message.byteVal; + this.booleanVal = message.booleanVal; + this.map = copyOf(message.map); + this.list = copyOf(message.list); + this.stringArray = copyOf(message.stringArray); + this.longArray = copyOf(message.longArray); + this.assetIndex = message.assetIndex; + this.floatArray = copyOf(message.floatArray); + } + + public Builder byteArray(ByteString byteArray) { + this.byteArray = byteArray; + return this; + } + + public Builder stringVal(String stringVal) { + this.stringVal = stringVal; + return this; + } + + public Builder doubleVal(Double doubleVal) { + this.doubleVal = doubleVal; + return this; + } + + public Builder floatVal(Float floatVal) { + this.floatVal = floatVal; + return this; + } + + public Builder longVal(Long longVal) { + this.longVal = longVal; + return this; + } + + public Builder intVal(Integer intVal) { + this.intVal = intVal; + return this; + } + + public Builder byteVal(Integer byteVal) { + this.byteVal = byteVal; + return this; + } + + public Builder booleanVal(Boolean booleanVal) { + this.booleanVal = booleanVal; + return this; + } + + public Builder map(List map) { + this.map = checkForNulls(map); + return this; + } + + public Builder list(List list) { + this.list = checkForNulls(list); + return this; + } + + public Builder stringArray(List stringArray) { + this.stringArray = checkForNulls(stringArray); + return this; + } + + public Builder longArray(List longArray) { + this.longArray = checkForNulls(longArray); + return this; + } + + public Builder assetIndex(Integer assetIndex) { + this.assetIndex = assetIndex; + return this; + } + + public Builder floatArray(List floatArray) { + this.floatArray = checkForNulls(floatArray); + return this; + } + + @Override + public DataBundleValue build() { + return new DataBundleValue(this); + } + } +} diff --git a/play-services-wearable/src/main/protos-repo/databundle.proto b/play-services-wearable/src/main/protos-repo/databundle.proto new file mode 100644 index 00000000..bc8cb317 --- /dev/null +++ b/play-services-wearable/src/main/protos-repo/databundle.proto @@ -0,0 +1,33 @@ +option java_package = "org.microg.gms.wearable.databundle"; +option java_outer_classname = "DataBundleProto"; + +message DataBundle { + repeated DataBundleEntry entries = 1; +} + +message DataBundleEntry { + optional string key = 1; + optional DataBundleTypedValue typedValue = 2; +} + +message DataBundleTypedValue { + optional int32 type = 1; + optional DataBundleValue value = 2; +} + +message DataBundleValue { + optional bytes byteArray = 1; + optional string stringVal = 2; + optional double doubleVal = 3; + optional float floatVal = 4; + optional int64 longVal = 5; + optional int32 intVal = 6; + optional int32 byteVal = 7; + optional bool booleanVal = 8; + repeated DataBundleEntry map = 9; + repeated DataBundleTypedValue list = 10; + repeated string stringArray = 11; + repeated int64 longArray = 12; + optional int32 assetIndex = 13; + repeated float floatArray = 14; +} diff --git a/play-services/build.gradle b/play-services/build.gradle new file mode 100644 index 00000000..488c0632 --- /dev/null +++ b/play-services/build.gradle @@ -0,0 +1,54 @@ +/* + * 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 + } + + lintOptions { + disable 'InvalidPackage' + } +} + +dependencies { + api project(':play-services-base') + api project(':play-services-cast') + api project(':play-services-gcm') + api project(':play-services-location') + api project(':play-services-wearable') +} diff --git a/play-services/gradle.properties b/play-services/gradle.properties new file mode 100644 index 00000000..b8e4412a --- /dev/null +++ b/play-services/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 +POM_DESCRIPTION=The whole package of Play Services libraries. Use play-services-* artifacts if you only need certain parts + +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/src/main/AndroidManifest.xml b/play-services/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a18fdb94 --- /dev/null +++ b/play-services/src/main/AndroidManifest.xml @@ -0,0 +1,22 @@ + + + + + + +