/* * Copyright (C) 2019 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 android.service.controls; import android.Manifest; import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; import android.app.Service; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.os.DeadObjectException; import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.os.Message; import android.os.RemoteException; import android.service.controls.actions.ControlAction; import android.service.controls.actions.ControlActionWrapper; import android.service.controls.flags.Flags; import android.service.controls.templates.ControlTemplate; import android.text.TextUtils; import android.util.Log; import com.android.internal.util.Preconditions; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.List; import java.util.concurrent.Flow.Publisher; import java.util.concurrent.Flow.Subscriber; import java.util.concurrent.Flow.Subscription; import java.util.function.Consumer; /** * Service implementation allowing applications to contribute controls to the * System UI. */ public abstract class ControlsProviderService extends Service { @SdkConstant(SdkConstantType.SERVICE_ACTION) public static final String SERVICE_CONTROLS = "android.service.controls.ControlsProviderService"; /** * Manifest metadata to show a custom embedded activity as part of device controls. * * The value of this metadata must be the {@link ComponentName} as a string of an activity in * the same package that will be launched embedded in the device controls space. * * The activity must be exported, enabled and protected by * {@link Manifest.permission#BIND_CONTROLS}. * * It is recommended that the activity is declared {@code android:resizeableActivity="true"}. */ public static final String META_DATA_PANEL_ACTIVITY = "android.service.controls.META_DATA_PANEL_ACTIVITY"; /** * Boolean extra containing the value of the setting allowing actions on a locked device. * * This corresponds to the setting that indicates whether the user has * consented to allow actions on devices that declare {@link Control#isAuthRequired()} as * {@code false} when the device is locked. * * This is passed with the intent when the panel specified by {@link #META_DATA_PANEL_ACTIVITY} * is launched. */ public static final String EXTRA_LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS = "android.service.controls.extra.LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS"; /** @hide */ @Retention(RetentionPolicy.SOURCE) @IntDef({CONTROLS_SURFACE_ACTIVITY_PANEL, CONTROLS_SURFACE_DREAM}) public @interface ControlsSurface { } /** * Controls are being shown on the device controls activity panel. */ @FlaggedApi(Flags.FLAG_HOME_PANEL_DREAM) public static final int CONTROLS_SURFACE_ACTIVITY_PANEL = 0; /** * Controls are being shown as a dream, while the device is idle. */ @FlaggedApi(Flags.FLAG_HOME_PANEL_DREAM) public static final int CONTROLS_SURFACE_DREAM = 1; /** * Integer extra whose value specifies the surface which controls are being displayed on. *

* The possible values are: *

* * This is passed with the intent when the panel specified by {@link #META_DATA_PANEL_ACTIVITY} * is launched. */ @FlaggedApi(Flags.FLAG_HOME_PANEL_DREAM) public static final String EXTRA_CONTROLS_SURFACE = "android.service.controls.extra.CONTROLS_SURFACE"; /** * @hide */ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) public static final String ACTION_ADD_CONTROL = "android.service.controls.action.ADD_CONTROL"; /** * @hide */ public static final String EXTRA_CONTROL = "android.service.controls.extra.CONTROL"; /** * @hide */ public static final String CALLBACK_BUNDLE = "CALLBACK_BUNDLE"; /** * @hide */ public static final String CALLBACK_TOKEN = "CALLBACK_TOKEN"; public static final @NonNull String TAG = "ControlsProviderService"; private IBinder mToken; private RequestHandler mHandler; /** * Publisher for all available controls * * Retrieve all available controls. Use the stateless builder {@link Control.StatelessBuilder} * to build each Control. Call {@link Subscriber#onComplete} when done loading all unique * controls, or {@link Subscriber#onError} for error scenarios. Duplicate Controls will * replace the original. */ @NonNull public abstract Publisher createPublisherForAllAvailable(); /** * (Optional) Publisher for suggested controls * * The service may be asked to provide a small number of recommended controls, in * order to suggest some controls to the user for favoriting. The controls shall be built using * the stateless builder {@link Control.StatelessBuilder}. The total number of controls * requested through {@link Subscription#request} will be restricted to a maximum. Within this * larger limit, only 6 controls per structure will be loaded. Therefore, it is advisable to * seed multiple structures if they exist. Any control sent over this limit will be discarded. * Call {@link Subscriber#onComplete} when done, or {@link Subscriber#onError} for error * scenarios. */ @Nullable public Publisher createPublisherForSuggested() { return null; } /** * Return a valid Publisher for the given controlIds. This publisher will be asked to provide * updates for the given list of controlIds as long as the {@link Subscription} is valid. * Calls to {@link Subscriber#onComplete} will not be expected. Instead, wait for the call from * {@link Subscription#cancel} to indicate that updates are no longer required. It is expected * that controls provided by this publisher were created using {@link Control.StatefulBuilder}. * * By default, all controls require the device to be unlocked in order for the user to interact * with it. This can be modified per Control by {@link Control.StatefulBuilder#setAuthRequired}. */ @NonNull public abstract Publisher createPublisherFor(@NonNull List controlIds); /** * The user has interacted with a Control. The action is dictated by the type of * {@link ControlAction} that was sent. A response can be sent via * {@link Consumer#accept}, with the Integer argument being one of the provided * {@link ControlAction} response results. The Integer should indicate whether the action * was received successfully, or if additional prompts should be presented to * the user. Any visual control updates should be sent via the Publisher. * By default, all invocations of this method will require the device be unlocked. This can * be modified per Control by {@link Control.StatefulBuilder#setAuthRequired}. */ public abstract void performControlAction(@NonNull String controlId, @NonNull ControlAction action, @NonNull Consumer consumer); @Override @NonNull public final IBinder onBind(@NonNull Intent intent) { mHandler = new RequestHandler(Looper.getMainLooper()); Bundle bundle = intent.getBundleExtra(CALLBACK_BUNDLE); mToken = bundle.getBinder(CALLBACK_TOKEN); return new IControlsProvider.Stub() { public void load(IControlsSubscriber subscriber) { mHandler.obtainMessage(RequestHandler.MSG_LOAD, subscriber).sendToTarget(); } public void loadSuggested(IControlsSubscriber subscriber) { mHandler.obtainMessage(RequestHandler.MSG_LOAD_SUGGESTED, subscriber) .sendToTarget(); } public void subscribe(List controlIds, IControlsSubscriber subscriber) { SubscribeMessage msg = new SubscribeMessage(controlIds, subscriber); mHandler.obtainMessage(RequestHandler.MSG_SUBSCRIBE, msg).sendToTarget(); } public void action(String controlId, ControlActionWrapper action, IControlsActionCallback cb) { ActionMessage msg = new ActionMessage(controlId, action.getWrappedAction(), cb); mHandler.obtainMessage(RequestHandler.MSG_ACTION, msg).sendToTarget(); } }; } @Override public final boolean onUnbind(@NonNull Intent intent) { mHandler = null; return true; } private class RequestHandler extends Handler { private static final int MSG_LOAD = 1; private static final int MSG_SUBSCRIBE = 2; private static final int MSG_ACTION = 3; private static final int MSG_LOAD_SUGGESTED = 4; RequestHandler(Looper looper) { super(looper); } public void handleMessage(Message msg) { switch(msg.what) { case MSG_LOAD: { final IControlsSubscriber cs = (IControlsSubscriber) msg.obj; final SubscriberProxy proxy = new SubscriberProxy(true, mToken, cs); ControlsProviderService.this.createPublisherForAllAvailable().subscribe(proxy); break; } case MSG_LOAD_SUGGESTED: { final IControlsSubscriber cs = (IControlsSubscriber) msg.obj; final SubscriberProxy proxy = new SubscriberProxy(true, mToken, cs); Publisher publisher = ControlsProviderService.this.createPublisherForSuggested(); if (publisher == null) { Log.i(TAG, "No publisher provided for suggested controls"); proxy.onComplete(); } else { publisher.subscribe(proxy); } break; } case MSG_SUBSCRIBE: { final SubscribeMessage sMsg = (SubscribeMessage) msg.obj; final SubscriberProxy proxy = new SubscriberProxy( ControlsProviderService.this, false, mToken, sMsg.mSubscriber); ControlsProviderService.this.createPublisherFor(sMsg.mControlIds) .subscribe(proxy); break; } case MSG_ACTION: { final ActionMessage aMsg = (ActionMessage) msg.obj; ControlsProviderService.this.performControlAction(aMsg.mControlId, aMsg.mAction, consumerFor(aMsg.mControlId, aMsg.mCb)); break; } } } private Consumer consumerFor(final String controlId, final IControlsActionCallback cb) { return (@NonNull Integer response) -> { Preconditions.checkNotNull(response); if (!ControlAction.isValidResponse(response)) { Log.e(TAG, "Not valid response result: " + response); response = ControlAction.RESPONSE_UNKNOWN; } try { cb.accept(mToken, controlId, response); } catch (RemoteException ex) { ex.rethrowAsRuntimeException(); } }; } } private static boolean isStatelessControl(Control control) { return (control.getStatus() == Control.STATUS_UNKNOWN && control.getControlTemplate().getTemplateType() == ControlTemplate.TYPE_NO_TEMPLATE && TextUtils.isEmpty(control.getStatusText())); } private static class SubscriberProxy implements Subscriber { private IBinder mToken; private IControlsSubscriber mCs; private boolean mEnforceStateless; private Context mContext; private SubscriptionAdapter mSubscription; SubscriberProxy(boolean enforceStateless, IBinder token, IControlsSubscriber cs) { mEnforceStateless = enforceStateless; mToken = token; mCs = cs; } SubscriberProxy(Context context, boolean enforceStateless, IBinder token, IControlsSubscriber cs) { this(enforceStateless, token, cs); mContext = context; } public void onSubscribe(Subscription subscription) { try { SubscriptionAdapter subscriptionAdapter = new SubscriptionAdapter(subscription); mCs.onSubscribe(mToken, subscriptionAdapter); mSubscription = subscriptionAdapter; } catch (RemoteException ex) { handleRemoteException(ex); } } public void onNext(@NonNull Control control) { Preconditions.checkNotNull(control); try { if (mEnforceStateless && !isStatelessControl(control)) { Log.w(TAG, "onNext(): control is not stateless. Use the " + "Control.StatelessBuilder() to build the control."); control = new Control.StatelessBuilder(control).build(); } if (mContext != null) { control.getControlTemplate().prepareTemplateForBinder(mContext); } mCs.onNext(mToken, control); } catch (RemoteException ex) { handleRemoteException(ex); } } public void onError(Throwable t) { try { mCs.onError(mToken, t.toString()); mSubscription = null; } catch (RemoteException ex) { handleRemoteException(ex); } } public void onComplete() { try { mCs.onComplete(mToken); mSubscription = null; } catch (RemoteException ex) { handleRemoteException(ex); } } private void handleRemoteException(RemoteException ex) { if (ex instanceof DeadObjectException) { // System UI crashed or is restarting. There is no need to rethrow this SubscriptionAdapter subscriptionAdapter = mSubscription; if (subscriptionAdapter != null) { subscriptionAdapter.cancel(); } } else { ex.rethrowAsRuntimeException(); } } } /** * Request SystemUI to prompt the user to add a control to favorites. *
* SystemUI may not honor this request in some cases, for example if the requested * {@link Control} is already a favorite, or the requesting package is not currently in the * foreground. * * @param context A context * @param componentName Component name of the {@link ControlsProviderService} * @param control A stateless control to show to the user */ public static void requestAddControl(@NonNull Context context, @NonNull ComponentName componentName, @NonNull Control control) { Preconditions.checkNotNull(context); Preconditions.checkNotNull(componentName); Preconditions.checkNotNull(control); final String controlsPackage = context.getString( com.android.internal.R.string.config_controlsPackage); Intent intent = new Intent(ACTION_ADD_CONTROL); intent.putExtra(Intent.EXTRA_COMPONENT_NAME, componentName); intent.setPackage(controlsPackage); if (isStatelessControl(control)) { intent.putExtra(EXTRA_CONTROL, control); } else { intent.putExtra(EXTRA_CONTROL, new Control.StatelessBuilder(control).build()); } context.sendBroadcast(intent, Manifest.permission.BIND_CONTROLS); } private static class SubscriptionAdapter extends IControlsSubscription.Stub { final Subscription mSubscription; SubscriptionAdapter(Subscription s) { this.mSubscription = s; } public void request(long n) { mSubscription.request(n); } public void cancel() { mSubscription.cancel(); } } private static class ActionMessage { final String mControlId; final ControlAction mAction; final IControlsActionCallback mCb; ActionMessage(String controlId, ControlAction action, IControlsActionCallback cb) { this.mControlId = controlId; this.mAction = action; this.mCb = cb; } } private static class SubscribeMessage { final List mControlIds; final IControlsSubscriber mSubscriber; SubscribeMessage(List controlIds, IControlsSubscriber subscriber) { this.mControlIds = controlIds; this.mSubscriber = subscriber; } } }