1 /* 2 * Copyright (C) 2019 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package android.service.controls; 17 18 import android.Manifest; 19 import android.annotation.FlaggedApi; 20 import android.annotation.IntDef; 21 import android.annotation.NonNull; 22 import android.annotation.Nullable; 23 import android.annotation.SdkConstant; 24 import android.annotation.SdkConstant.SdkConstantType; 25 import android.app.Service; 26 import android.content.ComponentName; 27 import android.content.Context; 28 import android.content.Intent; 29 import android.os.Bundle; 30 import android.os.DeadObjectException; 31 import android.os.Handler; 32 import android.os.IBinder; 33 import android.os.Looper; 34 import android.os.Message; 35 import android.os.RemoteException; 36 import android.service.controls.actions.ControlAction; 37 import android.service.controls.actions.ControlActionWrapper; 38 import android.service.controls.flags.Flags; 39 import android.service.controls.templates.ControlTemplate; 40 import android.text.TextUtils; 41 import android.util.Log; 42 43 import com.android.internal.util.Preconditions; 44 45 import java.lang.annotation.Retention; 46 import java.lang.annotation.RetentionPolicy; 47 import java.util.List; 48 import java.util.concurrent.Flow.Publisher; 49 import java.util.concurrent.Flow.Subscriber; 50 import java.util.concurrent.Flow.Subscription; 51 import java.util.function.Consumer; 52 53 /** 54 * Service implementation allowing applications to contribute controls to the 55 * System UI. 56 */ 57 public abstract class ControlsProviderService extends Service { 58 59 @SdkConstant(SdkConstantType.SERVICE_ACTION) 60 public static final String SERVICE_CONTROLS = 61 "android.service.controls.ControlsProviderService"; 62 63 /** 64 * Manifest metadata to show a custom embedded activity as part of device controls. 65 * 66 * The value of this metadata must be the {@link ComponentName} as a string of an activity in 67 * the same package that will be launched embedded in the device controls space. 68 * 69 * The activity must be exported, enabled and protected by 70 * {@link Manifest.permission#BIND_CONTROLS}. 71 * 72 * It is recommended that the activity is declared {@code android:resizeableActivity="true"}. 73 */ 74 public static final String META_DATA_PANEL_ACTIVITY = 75 "android.service.controls.META_DATA_PANEL_ACTIVITY"; 76 77 /** 78 * Boolean extra containing the value of the setting allowing actions on a locked device. 79 * 80 * This corresponds to the setting that indicates whether the user has 81 * consented to allow actions on devices that declare {@link Control#isAuthRequired()} as 82 * {@code false} when the device is locked. 83 * 84 * This is passed with the intent when the panel specified by {@link #META_DATA_PANEL_ACTIVITY} 85 * is launched. 86 */ 87 public static final String EXTRA_LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS = 88 "android.service.controls.extra.LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS"; 89 90 /** @hide */ 91 @Retention(RetentionPolicy.SOURCE) 92 @IntDef({CONTROLS_SURFACE_ACTIVITY_PANEL, CONTROLS_SURFACE_DREAM}) 93 public @interface ControlsSurface { 94 } 95 96 /** 97 * Controls are being shown on the device controls activity panel. 98 */ 99 @FlaggedApi(Flags.FLAG_HOME_PANEL_DREAM) 100 public static final int CONTROLS_SURFACE_ACTIVITY_PANEL = 0; 101 102 /** 103 * Controls are being shown as a dream, while the device is idle. 104 */ 105 @FlaggedApi(Flags.FLAG_HOME_PANEL_DREAM) 106 public static final int CONTROLS_SURFACE_DREAM = 1; 107 108 /** 109 * Integer extra whose value specifies the surface which controls are being displayed on. 110 * <p> 111 * The possible values are: 112 * <ul> 113 * <li>{@link #CONTROLS_SURFACE_ACTIVITY_PANEL} 114 * <li>{@link #CONTROLS_SURFACE_DREAM} 115 * </ul> 116 * 117 * This is passed with the intent when the panel specified by {@link #META_DATA_PANEL_ACTIVITY} 118 * is launched. 119 */ 120 @FlaggedApi(Flags.FLAG_HOME_PANEL_DREAM) 121 public static final String EXTRA_CONTROLS_SURFACE = 122 "android.service.controls.extra.CONTROLS_SURFACE"; 123 124 /** 125 * @hide 126 */ 127 @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) 128 public static final String ACTION_ADD_CONTROL = 129 "android.service.controls.action.ADD_CONTROL"; 130 131 /** 132 * @hide 133 */ 134 public static final String EXTRA_CONTROL = 135 "android.service.controls.extra.CONTROL"; 136 137 /** 138 * @hide 139 */ 140 public static final String CALLBACK_BUNDLE = "CALLBACK_BUNDLE"; 141 142 /** 143 * @hide 144 */ 145 public static final String CALLBACK_TOKEN = "CALLBACK_TOKEN"; 146 147 public static final @NonNull String TAG = "ControlsProviderService"; 148 149 private IBinder mToken; 150 private RequestHandler mHandler; 151 152 /** 153 * Publisher for all available controls 154 * 155 * Retrieve all available controls. Use the stateless builder {@link Control.StatelessBuilder} 156 * to build each Control. Call {@link Subscriber#onComplete} when done loading all unique 157 * controls, or {@link Subscriber#onError} for error scenarios. Duplicate Controls will 158 * replace the original. 159 */ 160 @NonNull createPublisherForAllAvailable()161 public abstract Publisher<Control> createPublisherForAllAvailable(); 162 163 /** 164 * (Optional) Publisher for suggested controls 165 * 166 * The service may be asked to provide a small number of recommended controls, in 167 * order to suggest some controls to the user for favoriting. The controls shall be built using 168 * the stateless builder {@link Control.StatelessBuilder}. The total number of controls 169 * requested through {@link Subscription#request} will be restricted to a maximum. Within this 170 * larger limit, only 6 controls per structure will be loaded. Therefore, it is advisable to 171 * seed multiple structures if they exist. Any control sent over this limit will be discarded. 172 * Call {@link Subscriber#onComplete} when done, or {@link Subscriber#onError} for error 173 * scenarios. 174 */ 175 @Nullable createPublisherForSuggested()176 public Publisher<Control> createPublisherForSuggested() { 177 return null; 178 } 179 180 /** 181 * Return a valid Publisher for the given controlIds. This publisher will be asked to provide 182 * updates for the given list of controlIds as long as the {@link Subscription} is valid. 183 * Calls to {@link Subscriber#onComplete} will not be expected. Instead, wait for the call from 184 * {@link Subscription#cancel} to indicate that updates are no longer required. It is expected 185 * that controls provided by this publisher were created using {@link Control.StatefulBuilder}. 186 * 187 * By default, all controls require the device to be unlocked in order for the user to interact 188 * with it. This can be modified per Control by {@link Control.StatefulBuilder#setAuthRequired}. 189 */ 190 @NonNull createPublisherFor(@onNull List<String> controlIds)191 public abstract Publisher<Control> createPublisherFor(@NonNull List<String> controlIds); 192 193 /** 194 * The user has interacted with a Control. The action is dictated by the type of 195 * {@link ControlAction} that was sent. A response can be sent via 196 * {@link Consumer#accept}, with the Integer argument being one of the provided 197 * {@link ControlAction} response results. The Integer should indicate whether the action 198 * was received successfully, or if additional prompts should be presented to 199 * the user. Any visual control updates should be sent via the Publisher. 200 201 * By default, all invocations of this method will require the device be unlocked. This can 202 * be modified per Control by {@link Control.StatefulBuilder#setAuthRequired}. 203 */ performControlAction(@onNull String controlId, @NonNull ControlAction action, @NonNull Consumer<Integer> consumer)204 public abstract void performControlAction(@NonNull String controlId, 205 @NonNull ControlAction action, @NonNull Consumer<Integer> consumer); 206 207 @Override 208 @NonNull onBind(@onNull Intent intent)209 public final IBinder onBind(@NonNull Intent intent) { 210 mHandler = new RequestHandler(Looper.getMainLooper()); 211 212 Bundle bundle = intent.getBundleExtra(CALLBACK_BUNDLE); 213 mToken = bundle.getBinder(CALLBACK_TOKEN); 214 215 return new IControlsProvider.Stub() { 216 public void load(IControlsSubscriber subscriber) { 217 mHandler.obtainMessage(RequestHandler.MSG_LOAD, subscriber).sendToTarget(); 218 } 219 220 public void loadSuggested(IControlsSubscriber subscriber) { 221 mHandler.obtainMessage(RequestHandler.MSG_LOAD_SUGGESTED, subscriber) 222 .sendToTarget(); 223 } 224 225 public void subscribe(List<String> controlIds, 226 IControlsSubscriber subscriber) { 227 SubscribeMessage msg = new SubscribeMessage(controlIds, subscriber); 228 mHandler.obtainMessage(RequestHandler.MSG_SUBSCRIBE, msg).sendToTarget(); 229 } 230 231 public void action(String controlId, ControlActionWrapper action, 232 IControlsActionCallback cb) { 233 ActionMessage msg = new ActionMessage(controlId, action.getWrappedAction(), cb); 234 mHandler.obtainMessage(RequestHandler.MSG_ACTION, msg).sendToTarget(); 235 } 236 }; 237 } 238 239 @Override 240 public final boolean onUnbind(@NonNull Intent intent) { 241 mHandler = null; 242 return true; 243 } 244 245 private class RequestHandler extends Handler { 246 private static final int MSG_LOAD = 1; 247 private static final int MSG_SUBSCRIBE = 2; 248 private static final int MSG_ACTION = 3; 249 private static final int MSG_LOAD_SUGGESTED = 4; 250 251 RequestHandler(Looper looper) { 252 super(looper); 253 } 254 255 public void handleMessage(Message msg) { 256 switch(msg.what) { 257 case MSG_LOAD: { 258 final IControlsSubscriber cs = (IControlsSubscriber) msg.obj; 259 final SubscriberProxy proxy = new SubscriberProxy(true, mToken, cs); 260 261 ControlsProviderService.this.createPublisherForAllAvailable().subscribe(proxy); 262 break; 263 } 264 265 case MSG_LOAD_SUGGESTED: { 266 final IControlsSubscriber cs = (IControlsSubscriber) msg.obj; 267 final SubscriberProxy proxy = new SubscriberProxy(true, mToken, cs); 268 269 Publisher<Control> publisher = 270 ControlsProviderService.this.createPublisherForSuggested(); 271 if (publisher == null) { 272 Log.i(TAG, "No publisher provided for suggested controls"); 273 proxy.onComplete(); 274 } else { 275 publisher.subscribe(proxy); 276 } 277 break; 278 } 279 280 case MSG_SUBSCRIBE: { 281 final SubscribeMessage sMsg = (SubscribeMessage) msg.obj; 282 final SubscriberProxy proxy = new SubscriberProxy( 283 ControlsProviderService.this, false, mToken, sMsg.mSubscriber); 284 285 ControlsProviderService.this.createPublisherFor(sMsg.mControlIds) 286 .subscribe(proxy); 287 break; 288 } 289 290 case MSG_ACTION: { 291 final ActionMessage aMsg = (ActionMessage) msg.obj; 292 ControlsProviderService.this.performControlAction(aMsg.mControlId, 293 aMsg.mAction, consumerFor(aMsg.mControlId, aMsg.mCb)); 294 break; 295 } 296 } 297 } 298 299 private Consumer<Integer> consumerFor(final String controlId, 300 final IControlsActionCallback cb) { 301 return (@NonNull Integer response) -> { 302 Preconditions.checkNotNull(response); 303 if (!ControlAction.isValidResponse(response)) { 304 Log.e(TAG, "Not valid response result: " + response); 305 response = ControlAction.RESPONSE_UNKNOWN; 306 } 307 try { 308 cb.accept(mToken, controlId, response); 309 } catch (RemoteException ex) { 310 ex.rethrowAsRuntimeException(); 311 } 312 }; 313 } 314 } 315 316 private static boolean isStatelessControl(Control control) { 317 return (control.getStatus() == Control.STATUS_UNKNOWN 318 && control.getControlTemplate().getTemplateType() 319 == ControlTemplate.TYPE_NO_TEMPLATE 320 && TextUtils.isEmpty(control.getStatusText())); 321 } 322 323 private static class SubscriberProxy implements Subscriber<Control> { 324 private IBinder mToken; 325 private IControlsSubscriber mCs; 326 private boolean mEnforceStateless; 327 private Context mContext; 328 private SubscriptionAdapter mSubscription; 329 330 SubscriberProxy(boolean enforceStateless, IBinder token, IControlsSubscriber cs) { 331 mEnforceStateless = enforceStateless; 332 mToken = token; 333 mCs = cs; 334 } 335 336 SubscriberProxy(Context context, boolean enforceStateless, IBinder token, 337 IControlsSubscriber cs) { 338 this(enforceStateless, token, cs); 339 mContext = context; 340 } 341 342 public void onSubscribe(Subscription subscription) { 343 try { 344 SubscriptionAdapter subscriptionAdapter = new SubscriptionAdapter(subscription); 345 mCs.onSubscribe(mToken, subscriptionAdapter); 346 mSubscription = subscriptionAdapter; 347 } catch (RemoteException ex) { 348 handleRemoteException(ex); 349 } 350 } 351 352 public void onNext(@NonNull Control control) { 353 Preconditions.checkNotNull(control); 354 try { 355 if (mEnforceStateless && !isStatelessControl(control)) { 356 Log.w(TAG, "onNext(): control is not stateless. Use the " 357 + "Control.StatelessBuilder() to build the control."); 358 control = new Control.StatelessBuilder(control).build(); 359 } 360 if (mContext != null) { 361 control.getControlTemplate().prepareTemplateForBinder(mContext); 362 } 363 mCs.onNext(mToken, control); 364 } catch (RemoteException ex) { 365 handleRemoteException(ex); 366 } 367 } 368 369 public void onError(Throwable t) { 370 try { 371 mCs.onError(mToken, t.toString()); 372 mSubscription = null; 373 } catch (RemoteException ex) { 374 handleRemoteException(ex); 375 } 376 } 377 378 public void onComplete() { 379 try { 380 mCs.onComplete(mToken); 381 mSubscription = null; 382 } catch (RemoteException ex) { 383 handleRemoteException(ex); 384 } 385 } 386 387 private void handleRemoteException(RemoteException ex) { 388 if (ex instanceof DeadObjectException) { 389 // System UI crashed or is restarting. There is no need to rethrow this 390 SubscriptionAdapter subscriptionAdapter = mSubscription; 391 if (subscriptionAdapter != null) { 392 subscriptionAdapter.cancel(); 393 } 394 } else { 395 ex.rethrowAsRuntimeException(); 396 } 397 } 398 } 399 400 /** 401 * Request SystemUI to prompt the user to add a control to favorites. 402 * <br> 403 * SystemUI may not honor this request in some cases, for example if the requested 404 * {@link Control} is already a favorite, or the requesting package is not currently in the 405 * foreground. 406 * 407 * @param context A context 408 * @param componentName Component name of the {@link ControlsProviderService} 409 * @param control A stateless control to show to the user 410 */ 411 public static void requestAddControl(@NonNull Context context, 412 @NonNull ComponentName componentName, 413 @NonNull Control control) { 414 Preconditions.checkNotNull(context); 415 Preconditions.checkNotNull(componentName); 416 Preconditions.checkNotNull(control); 417 final String controlsPackage = context.getString( 418 com.android.internal.R.string.config_controlsPackage); 419 Intent intent = new Intent(ACTION_ADD_CONTROL); 420 intent.putExtra(Intent.EXTRA_COMPONENT_NAME, componentName); 421 intent.setPackage(controlsPackage); 422 if (isStatelessControl(control)) { 423 intent.putExtra(EXTRA_CONTROL, control); 424 } else { 425 intent.putExtra(EXTRA_CONTROL, new Control.StatelessBuilder(control).build()); 426 } 427 context.sendBroadcast(intent, Manifest.permission.BIND_CONTROLS); 428 } 429 430 private static class SubscriptionAdapter extends IControlsSubscription.Stub { 431 final Subscription mSubscription; 432 433 SubscriptionAdapter(Subscription s) { 434 this.mSubscription = s; 435 } 436 437 public void request(long n) { 438 mSubscription.request(n); 439 } 440 441 public void cancel() { 442 mSubscription.cancel(); 443 } 444 } 445 446 private static class ActionMessage { 447 final String mControlId; 448 final ControlAction mAction; 449 final IControlsActionCallback mCb; 450 451 ActionMessage(String controlId, ControlAction action, IControlsActionCallback cb) { 452 this.mControlId = controlId; 453 this.mAction = action; 454 this.mCb = cb; 455 } 456 } 457 458 private static class SubscribeMessage { 459 final List<String> mControlIds; 460 final IControlsSubscriber mSubscriber; 461 462 SubscribeMessage(List<String> controlIds, IControlsSubscriber subscriber) { 463 this.mControlIds = controlIds; 464 this.mSubscriber = subscriber; 465 } 466 } 467 } 468