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