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