blob: 26c5e330c0f2ee9a0c8f9f136b30b83dbe43859e [file] [log] [blame]
/*
* Copyright 2020 The Android Open Source Project
*
* 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 androidx.wear.watchface;
import android.app.Activity;
import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.support.wearable.complications.ComplicationData;
import android.support.wearable.complications.ComplicationProviderInfo;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.core.app.ActivityCompat;
import androidx.wear.watchface.complications.ComplicationDataSourceUpdateRequesterConstants;
import androidx.wear.watchface.complications.data.ComplicationType;
import java.util.Collection;
import java.util.Objects;
/**
* Activity to handle permission requests for complications.
*
* <p>This can be used to start the complication data source chooser, making a permission request
* if necessary, or to just make a permission request, and update all active complications if the
* permission is granted.
*
* <p>To use, add this activity to your app, and also add the {@code
* com.google.android.wearable.permission.RECEIVE_COMPLICATION_DATA} permission.
*
* <p>Then, to start the complication data source chooser chooser, use
* {@link #createComplicationDataSourceChooserHelperIntent} to obtain an intent. If the
* permission has not yet been granted, the permission will be requested and the complication
* data source chooser chooser will only be started if the request is accepted by the user.
*
* <p>Or, to request the permission, for instance if {@link ComplicationData} of {@link
* ComplicationData#TYPE_NO_PERMISSION TYPE_NO_PERMISSION} has been received and tapped on, use
* {@link #createPermissionRequestHelperIntent}.
*
* @hide
*/
@RequiresApi(Build.VERSION_CODES.N)
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@SuppressWarnings("ForbiddenSuperClass")
public final class ComplicationHelperActivity extends Activity
implements ActivityCompat.OnRequestPermissionsResultCallback {
/**
* Whether to invoke a specified activity instead of the system's complication data source
* chooser.
*
* To be used in tests.
*/
public static boolean useTestComplicationDataSourceChooserActivity = false;
/**
* Whether to skip th permission check and directly attempt to invoke the complication data
* source chooser.
*
* To be used in tests.
*/
public static boolean skipPermissionCheck = false;
/** @hide */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public static final String ACTION_REQUEST_UPDATE_ALL_ACTIVE =
"android.support.wearable.complications.ACTION_REQUEST_UPDATE_ALL_ACTIVE";
/** @hide */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public static final String EXTRA_WATCH_FACE_COMPONENT =
"android.support.wearable.complications.EXTRA_WATCH_FACE_COMPONENT";
/** @hide */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public static final String ACTION_START_PROVIDER_CHOOSER =
"android.support.wearable.complications.ACTION_START_PROVIDER_CHOOSER";
/** @hide */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public static final String ACTION_PERMISSION_REQUEST_ONLY =
"android.support.wearable.complications.ACTION_PERMISSION_REQUEST_ONLY";
/** The package of the service that accepts complication data source requests. */
private static final String UPDATE_REQUEST_RECEIVER_PACKAGE = "com.google.android.wearable.app";
private static final int START_REQUEST_CODE_PROVIDER_CHOOSER = 1;
private static final int PERMISSION_REQUEST_CODE_PROVIDER_CHOOSER = 1;
private static final int PERMISSION_REQUEST_CODE_REQUEST_ONLY = 2;
private static final String COMPLICATIONS_PERMISSION =
"com.google.android.wearable.permission.RECEIVE_COMPLICATION_DATA";
private static final String COMPLICATIONS_PERMISSION_PRIVILEGED =
"com.google.android.wearable.permission.RECEIVE_COMPLICATION_DATA_PRIVILEGED";
@Nullable
private ComponentName mWatchFace;
private int mWfComplicationId;
@Nullable
private Bundle mAdditionalExtras;
@Nullable
@ComplicationData.ComplicationType
private int[] mTypes;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent intent = getIntent();
switch (Objects.requireNonNull(intent.getAction())) {
case ACTION_START_PROVIDER_CHOOSER:
mWatchFace = intent.getParcelableExtra(
ComplicationDataSourceChooserIntent.EXTRA_WATCH_FACE_COMPONENT_NAME);
mWfComplicationId =
intent.getIntExtra(
ComplicationDataSourceChooserIntent.EXTRA_COMPLICATION_ID, 0);
mTypes = intent.getIntArrayExtra(
ComplicationDataSourceChooserIntent.EXTRA_SUPPORTED_TYPES);
mAdditionalExtras = getAdditionalExtras(intent);
if (checkPermission()) {
startComplicationDataSourceChooser();
} else {
ActivityCompat.requestPermissions(
this,
new String[]{COMPLICATIONS_PERMISSION},
PERMISSION_REQUEST_CODE_PROVIDER_CHOOSER);
}
break;
case ACTION_PERMISSION_REQUEST_ONLY:
mWatchFace = intent.getParcelableExtra(
ComplicationDataSourceChooserIntent.EXTRA_WATCH_FACE_COMPONENT_NAME);
if (checkPermission()) {
finish();
} else {
ActivityCompat.requestPermissions(
this,
new String[]{COMPLICATIONS_PERMISSION},
PERMISSION_REQUEST_CODE_REQUEST_ONLY);
}
break;
default:
throw new IllegalStateException("Unrecognised intent action.");
}
}
@Override
public void onRequestPermissionsResult(
int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
if (grantResults.length == 0) {
// Request was cancelled.
finish();
return;
}
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
if (requestCode == PERMISSION_REQUEST_CODE_PROVIDER_CHOOSER) {
startComplicationDataSourceChooser();
} else {
finish();
}
requestUpdateAll(mWatchFace);
} else {
finish();
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
if (requestCode == START_REQUEST_CODE_PROVIDER_CHOOSER) {
setResult(resultCode, data);
finish();
}
}
private boolean checkPermission() {
return ActivityCompat.checkSelfPermission(this, COMPLICATIONS_PERMISSION_PRIVILEGED)
== PackageManager.PERMISSION_GRANTED
|| ActivityCompat.checkSelfPermission(this, COMPLICATIONS_PERMISSION)
== PackageManager.PERMISSION_GRANTED
|| skipPermissionCheck;
}
/**
* Returns an intent that may be used to start the complication data source chooser activity via
* the ComplicationHelperActivity. This allows the required permission to be checked before the
* complication data source chooser is displayed.
*
* <p>To use this, the ComplicationHelperActivity must be added to your app, and your app must
* include the {@code com.google.android.wearable.permission.RECEIVE_COMPLICATION_DATA}
* permission in its manifest.
*
* <p>The complication data source chooser activity will show a list of all complication data
* sources that can supply data of at least one of the {@code supportedTypes}.
*
* <p>When the user chooses a complication data source, the configuration will be set up in the
* complications system - the watch face does not need to do anything else.
*
* <p>The activity may be started using {@link Activity#startActivityForResult}. The result
* delivered back to your activity will have a result code of {@link Activity#RESULT_OK
* RESULT_OK} if a complication data source was successfully set, or a result code of {@link
* Activity#RESULT_CANCELED RESULT_CANCELED} if no complication data source was set. In the case
* where a complication data source was set, {@link ComplicationProviderInfo} for the chosen
* complication data source will be included in the data intent of the result, as an extra
* with the key android.support.wearable.complications.EXTRA_PROVIDER_INFO.
*
* <p>The package of the calling app must match the package of the watch face, or this will not
* work.
*
* <p>From android R onwards this API can only be called during an editing session.
*
* @param context context for the current app, that must contain a
* ComplicationHelperActivity
* @param watchFace the ComponentName of the WatchFaceService being configured.
* @param watchFaceComplicationId the watch face's id for the complication being configured.
* This must match the id passed in when the watch face calls
* WatchFaceService.Engine#setActiveComplications.
* @param supportedTypes the types supported by the complication, in decreasing
* order of
* preference. If a complication data source can supply data for
* more than one of these types, the type chosen will be
* whichever was specified first.
* @param watchFaceInstanceId The ID of the watchface being edited.
*/
@NonNull
public static Intent createComplicationDataSourceChooserHelperIntent(
@NonNull Context context,
@NonNull ComponentName watchFace,
int watchFaceComplicationId,
@NonNull Collection<ComplicationType> supportedTypes,
@Nullable String watchFaceInstanceId) {
Intent intent = new Intent(context, ComplicationHelperActivity.class);
intent.setAction(ACTION_START_PROVIDER_CHOOSER);
intent.putExtra(
ComplicationDataSourceChooserIntent.EXTRA_WATCH_FACE_COMPONENT_NAME, watchFace);
intent.putExtra(
ComplicationDataSourceChooserIntent.EXTRA_COMPLICATION_ID, watchFaceComplicationId);
if (watchFaceInstanceId != null) {
intent.putExtra(ComplicationDataSourceChooserIntent.EXTRA_WATCHFACE_INSTANCE_ID,
watchFaceInstanceId);
}
int[] wireSupportedTypes = new int[supportedTypes.size()];
int i = 0;
for (ComplicationType supportedType : supportedTypes) {
wireSupportedTypes[i++] = supportedType.toWireComplicationType();
}
intent.putExtra(ComplicationDataSourceChooserIntent.EXTRA_SUPPORTED_TYPES,
wireSupportedTypes);
return intent;
}
/**
* Returns an intent that may be used to start this activity in order to request the permission
* required to receive complication data.
*
* <p>To use this, the ComplicationHelperActivity must be added to your app, and your app must
* include the {@code com.google.android.wearable.permission.RECEIVE_COMPLICATION_DATA}
* permission in its manifest.
*
* <p>If the current app has already been granted this permission, the activity will finish
* immediately.
*
* <p>If the current app has not been granted this permission, a permission request will be
* made. If the permission is granted by the user, an update of all complications on the current
* watch face will be triggered. The provided {@code watchFace} must match the current watch
* face for this to occur.
*
* @param context context for the current app, that must contain a ComplicationHelperActivity
* @param watchFace the ComponentName of the WatchFaceService for the current watch face
*/
@NonNull
public static Intent createPermissionRequestHelperIntent(
@NonNull Context context, @NonNull ComponentName watchFace) {
Intent intent = new Intent(context, ComplicationHelperActivity.class);
intent.setAction(ACTION_PERMISSION_REQUEST_ONLY);
intent.putExtra(ComplicationDataSourceChooserIntent.EXTRA_WATCH_FACE_COMPONENT_NAME,
watchFace);
return intent;
}
private void startComplicationDataSourceChooser() {
Intent intent =
ComplicationDataSourceChooserIntent.createComplicationDataSourceChooserIntent(
mWatchFace, mWfComplicationId, mTypes);
// Add the extras that were provided to the ComplicationHelperActivity. This is done by
// first taking the additional extras and adding to that anything that was set in the
// chooser intent, and setting them back on the intent itself to avoid the additional
// extras being able to override anything that was set by the chooser intent.
Bundle extras = new Bundle(mAdditionalExtras);
extras.putAll(intent.getExtras());
intent.replaceExtras(extras);
if (useTestComplicationDataSourceChooserActivity) {
intent.setComponent(new ComponentName(
"androidx.wear.watchface.editor.test",
"androidx.wear.watchface.editor.TestComplicationDataSourceChooserActivity"));
}
startActivityForResult(intent, START_REQUEST_CODE_PROVIDER_CHOOSER);
}
/** Requests that the system update all active complications on the watch face. */
private void requestUpdateAll(ComponentName watchFaceComponent) {
Intent intent = new Intent(ACTION_REQUEST_UPDATE_ALL_ACTIVE);
intent.setPackage(UPDATE_REQUEST_RECEIVER_PACKAGE);
intent.putExtra(EXTRA_WATCH_FACE_COMPONENT, watchFaceComponent);
// Add a placeholder PendingIntent to allow the UID to be checked.
intent.putExtra(
ComplicationDataSourceUpdateRequesterConstants.EXTRA_PENDING_INTENT,
PendingIntent.getActivity(
this, 0, new Intent(""), PendingIntent.FLAG_IMMUTABLE));
sendBroadcast(intent);
}
/**
* Returns any extras that were not handled by the activity itself.
*
* <p>These will be forwarded to the chooser activity.
*/
private Bundle getAdditionalExtras(Intent intent) {
Bundle extras = intent.getExtras();
extras.remove(ComplicationDataSourceChooserIntent.EXTRA_WATCH_FACE_COMPONENT_NAME);
extras.remove(ComplicationDataSourceChooserIntent.EXTRA_COMPLICATION_ID);
extras.remove(ComplicationDataSourceChooserIntent.EXTRA_SUPPORTED_TYPES);
return extras;
}
}