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