blob: 4262c402213181919da1a4ba76cf819ab8d13913 [file] [log] [blame]
Matt Pietal10eae312019-12-10 08:46:45 -05001/*
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 */
16package android.service.controls;
17
Fabian Kozynskid6f48402020-02-05 13:01:04 -050018import android.Manifest;
Matt Pietal10eae312019-12-10 08:46:45 -050019import android.annotation.NonNull;
Matt Pietald0553b02020-02-13 07:08:58 -050020import android.annotation.Nullable;
Matt Pietal10eae312019-12-10 08:46:45 -050021import android.annotation.SdkConstant;
22import android.annotation.SdkConstant.SdkConstantType;
23import android.app.Service;
Fabian Kozynskid6f48402020-02-05 13:01:04 -050024import android.content.ComponentName;
25import android.content.Context;
Matt Pietal10eae312019-12-10 08:46:45 -050026import android.content.Intent;
27import android.os.Bundle;
28import android.os.Handler;
29import android.os.IBinder;
30import android.os.Looper;
31import android.os.Message;
Fabian Kozynskiee57f492019-12-30 12:30:24 -050032import android.os.RemoteException;
Fabian Kozynski2d212122019-12-17 12:22:46 -050033import android.service.controls.actions.ControlAction;
Fabian Kozynski1bb26b52020-01-08 18:20:36 -050034import android.service.controls.actions.ControlActionWrapper;
Fabian Kozynskiee57f492019-12-30 12:30:24 -050035import android.service.controls.templates.ControlTemplate;
36import android.text.TextUtils;
37import android.util.Log;
Matt Pietal10eae312019-12-10 08:46:45 -050038
Fabian Kozynskiee57f492019-12-30 12:30:24 -050039import com.android.internal.util.Preconditions;
40
Matt Pietal10eae312019-12-10 08:46:45 -050041import java.util.List;
Fabian Kozynski1bb26b52020-01-08 18:20:36 -050042import java.util.concurrent.Flow.Publisher;
43import java.util.concurrent.Flow.Subscriber;
44import java.util.concurrent.Flow.Subscription;
45import java.util.function.Consumer;
Matt Pietal10eae312019-12-10 08:46:45 -050046
47/**
48 * Service implementation allowing applications to contribute controls to the
49 * System UI.
Matt Pietal10eae312019-12-10 08:46:45 -050050 */
51public abstract class ControlsProviderService extends Service {
52
53 @SdkConstant(SdkConstantType.SERVICE_ACTION)
Fabian Kozynski1bb26b52020-01-08 18:20:36 -050054 public static final String SERVICE_CONTROLS =
55 "android.service.controls.ControlsProviderService";
Fabian Kozynskid6f48402020-02-05 13:01:04 -050056
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
Fabian Kozynski1bb26b52020-01-08 18:20:36 -050070 /**
71 * @hide
72 */
Fabian Kozynskiee57f492019-12-30 12:30:24 -050073 public static final String CALLBACK_BUNDLE = "CALLBACK_BUNDLE";
Fabian Kozynski1bb26b52020-01-08 18:20:36 -050074
75 /**
76 * @hide
77 */
Fabian Kozynskiee57f492019-12-30 12:30:24 -050078 public static final String CALLBACK_TOKEN = "CALLBACK_TOKEN";
79
Fabian Kozynski1bb26b52020-01-08 18:20:36 -050080 public static final @NonNull String TAG = "ControlsProviderService";
Matt Pietal10eae312019-12-10 08:46:45 -050081
Fabian Kozynskiee57f492019-12-30 12:30:24 -050082 private IBinder mToken;
Matt Pietal10eae312019-12-10 08:46:45 -050083 private RequestHandler mHandler;
84
85 /**
Matt Pietald0553b02020-02-13 07:08:58 -050086 * 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 */
Matt Pietalbb3be652020-02-24 08:51:19 -050093 @NonNull
Matt Pietale44217b2020-02-26 08:48:37 -050094 public abstract Publisher<Control> createPublisherForAllAvailable();
Matt Pietald0553b02020-02-13 07:08:58 -050095
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 number of controls requested
102 * through {@link Subscription#request} will be limited. Call {@link Subscriber#onComplete}
103 * when done, or {@link Subscriber#onError} for error scenarios.
104 */
105 @Nullable
Matt Pietale44217b2020-02-26 08:48:37 -0500106 public Publisher<Control> createPublisherForSuggested() {
Matt Pietald0553b02020-02-13 07:08:58 -0500107 return null;
108 }
109
110 /**
111 * Return a valid Publisher for the given controlIds. This publisher will be asked to provide
112 * updates for the given list of controlIds as long as the {@link Subscription} is valid.
113 * Calls to {@link Subscriber#onComplete} will not be expected. Instead, wait for the call from
Matt Pietale44217b2020-02-26 08:48:37 -0500114 * {@link Subscription#cancel} to indicate that updates are no longer required. It is expected
115 * that controls provided by this publisher were created using {@link Control.StatefulBuilder}.
Matt Pietal10eae312019-12-10 08:46:45 -0500116 */
Fabian Kozynski1bb26b52020-01-08 18:20:36 -0500117 @NonNull
Matt Pietale44217b2020-02-26 08:48:37 -0500118 public abstract Publisher<Control> createPublisherFor(@NonNull List<String> controlIds);
Matt Pietal10eae312019-12-10 08:46:45 -0500119
120 /**
121 * The user has interacted with a Control. The action is dictated by the type of
Fabian Kozynski1bb26b52020-01-08 18:20:36 -0500122 * {@link ControlAction} that was sent. A response can be sent via
123 * {@link Consumer#accept}, with the Integer argument being one of the provided
124 * {@link ControlAction.ResponseResult}. The Integer should indicate whether the action
125 * was received successfully, or if additional prompts should be presented to
126 * the user. Any visual control updates should be sent via the Publisher.
Matt Pietal10eae312019-12-10 08:46:45 -0500127 */
Fabian Kozynski1bb26b52020-01-08 18:20:36 -0500128 public abstract void performControlAction(@NonNull String controlId,
129 @NonNull ControlAction action, @NonNull Consumer<Integer> consumer);
Matt Pietal10eae312019-12-10 08:46:45 -0500130
131 @Override
Fabian Kozynski1bb26b52020-01-08 18:20:36 -0500132 @NonNull
133 public final IBinder onBind(@NonNull Intent intent) {
Matt Pietal10eae312019-12-10 08:46:45 -0500134 mHandler = new RequestHandler(Looper.getMainLooper());
135
Fabian Kozynskiee57f492019-12-30 12:30:24 -0500136 Bundle bundle = intent.getBundleExtra(CALLBACK_BUNDLE);
Fabian Kozynskiee57f492019-12-30 12:30:24 -0500137 mToken = bundle.getBinder(CALLBACK_TOKEN);
Matt Pietal10eae312019-12-10 08:46:45 -0500138
139 return new IControlsProvider.Stub() {
Matt Pietald0553b02020-02-13 07:08:58 -0500140 public void load(IControlsSubscriber subscriber) {
141 mHandler.obtainMessage(RequestHandler.MSG_LOAD, subscriber).sendToTarget();
Matt Pietal10eae312019-12-10 08:46:45 -0500142 }
143
Matt Pietald0553b02020-02-13 07:08:58 -0500144 public void loadSuggested(IControlsSubscriber subscriber) {
145 mHandler.obtainMessage(RequestHandler.MSG_LOAD_SUGGESTED, subscriber)
146 .sendToTarget();
Matt Pietal587a5f72020-02-07 09:34:49 -0500147 }
148
Fabian Kozynski1bb26b52020-01-08 18:20:36 -0500149 public void subscribe(List<String> controlIds,
150 IControlsSubscriber subscriber) {
151 SubscribeMessage msg = new SubscribeMessage(controlIds, subscriber);
152 mHandler.obtainMessage(RequestHandler.MSG_SUBSCRIBE, msg).sendToTarget();
Matt Pietal10eae312019-12-10 08:46:45 -0500153 }
154
Fabian Kozynski1bb26b52020-01-08 18:20:36 -0500155 public void action(String controlId, ControlActionWrapper action,
156 IControlsActionCallback cb) {
157 ActionMessage msg = new ActionMessage(controlId, action.getWrappedAction(), cb);
158 mHandler.obtainMessage(RequestHandler.MSG_ACTION, msg).sendToTarget();
Matt Pietal10eae312019-12-10 08:46:45 -0500159 }
160 };
161 }
162
163 @Override
Matt Pietale44217b2020-02-26 08:48:37 -0500164 public final boolean onUnbind(@NonNull Intent intent) {
Matt Pietal10eae312019-12-10 08:46:45 -0500165 mHandler = null;
166 return true;
167 }
168
169 private class RequestHandler extends Handler {
170 private static final int MSG_LOAD = 1;
171 private static final int MSG_SUBSCRIBE = 2;
Fabian Kozynski1bb26b52020-01-08 18:20:36 -0500172 private static final int MSG_ACTION = 3;
Matt Pietal587a5f72020-02-07 09:34:49 -0500173 private static final int MSG_LOAD_SUGGESTED = 4;
174
Matt Pietal10eae312019-12-10 08:46:45 -0500175 RequestHandler(Looper looper) {
176 super(looper);
177 }
178
179 public void handleMessage(Message msg) {
180 switch(msg.what) {
Matt Pietald0553b02020-02-13 07:08:58 -0500181 case MSG_LOAD: {
182 final IControlsSubscriber cs = (IControlsSubscriber) msg.obj;
183 final SubscriberProxy proxy = new SubscriberProxy(true, mToken, cs);
Matt Pietal587a5f72020-02-07 09:34:49 -0500184
Matt Pietale44217b2020-02-26 08:48:37 -0500185 ControlsProviderService.this.createPublisherForAllAvailable().subscribe(proxy);
Matt Pietal10eae312019-12-10 08:46:45 -0500186 break;
Matt Pietald0553b02020-02-13 07:08:58 -0500187 }
Fabian Kozynski1bb26b52020-01-08 18:20:36 -0500188
Matt Pietald0553b02020-02-13 07:08:58 -0500189 case MSG_LOAD_SUGGESTED: {
190 final IControlsSubscriber cs = (IControlsSubscriber) msg.obj;
191 final SubscriberProxy proxy = new SubscriberProxy(true, mToken, cs);
192
193 Publisher<Control> publisher =
Matt Pietale44217b2020-02-26 08:48:37 -0500194 ControlsProviderService.this.createPublisherForSuggested();
Matt Pietald0553b02020-02-13 07:08:58 -0500195 if (publisher == null) {
196 Log.i(TAG, "No publisher provided for suggested controls");
197 proxy.onComplete();
198 } else {
199 publisher.subscribe(proxy);
200 }
201 break;
202 }
203
204 case MSG_SUBSCRIBE: {
Fabian Kozynski1bb26b52020-01-08 18:20:36 -0500205 final SubscribeMessage sMsg = (SubscribeMessage) msg.obj;
Matt Pietald0553b02020-02-13 07:08:58 -0500206 final SubscriberProxy proxy = new SubscriberProxy(false, mToken,
207 sMsg.mSubscriber);
Fabian Kozynski1bb26b52020-01-08 18:20:36 -0500208
Matt Pietale44217b2020-02-26 08:48:37 -0500209 ControlsProviderService.this.createPublisherFor(sMsg.mControlIds)
210 .subscribe(proxy);
Matt Pietald0553b02020-02-13 07:08:58 -0500211 break;
212 }
213
214 case MSG_ACTION: {
Fabian Kozynski1bb26b52020-01-08 18:20:36 -0500215 final ActionMessage aMsg = (ActionMessage) msg.obj;
216 ControlsProviderService.this.performControlAction(aMsg.mControlId,
217 aMsg.mAction, consumerFor(aMsg.mControlId, aMsg.mCb));
Matt Pietal10eae312019-12-10 08:46:45 -0500218 break;
Matt Pietald0553b02020-02-13 07:08:58 -0500219 }
Matt Pietal10eae312019-12-10 08:46:45 -0500220 }
221 }
Fabian Kozynski1bb26b52020-01-08 18:20:36 -0500222
223 private Consumer<Integer> consumerFor(final String controlId,
224 final IControlsActionCallback cb) {
225 return (@NonNull Integer response) -> {
226 Preconditions.checkNotNull(response);
227 if (!ControlAction.isValidResponse(response)) {
228 Log.e(TAG, "Not valid response result: " + response);
229 response = ControlAction.RESPONSE_UNKNOWN;
230 }
231 try {
232 cb.accept(mToken, controlId, response);
233 } catch (RemoteException ex) {
234 ex.rethrowAsRuntimeException();
235 }
236 };
237 }
Matt Pietald0553b02020-02-13 07:08:58 -0500238 }
Fabian Kozynski1bb26b52020-01-08 18:20:36 -0500239
Matt Pietald0553b02020-02-13 07:08:58 -0500240 private static boolean isStatelessControl(Control control) {
241 return (control.getStatus() == Control.STATUS_UNKNOWN
Fabian Kozynski95dcd242020-03-03 14:25:55 -0500242 && control.getControlTemplate().getTemplateType()
243 == ControlTemplate.TYPE_NO_TEMPLATE
Matt Pietald0553b02020-02-13 07:08:58 -0500244 && TextUtils.isEmpty(control.getStatusText()));
245 }
246
247 private static class SubscriberProxy implements Subscriber<Control> {
248 private IBinder mToken;
249 private IControlsSubscriber mCs;
250 private boolean mEnforceStateless;
251
252 SubscriberProxy(boolean enforceStateless, IBinder token, IControlsSubscriber cs) {
253 mEnforceStateless = enforceStateless;
254 mToken = token;
255 mCs = cs;
256 }
257
258 public void onSubscribe(Subscription subscription) {
259 try {
260 mCs.onSubscribe(mToken, new SubscriptionAdapter(subscription));
261 } catch (RemoteException ex) {
262 ex.rethrowAsRuntimeException();
263 }
264 }
265 public void onNext(@NonNull Control control) {
266 Preconditions.checkNotNull(control);
267 try {
268 if (mEnforceStateless && !isStatelessControl(control)) {
269 Log.w(TAG, "onNext(): control is not stateless. Use the "
270 + "Control.StatelessBuilder() to build the control.");
271 control = new Control.StatelessBuilder(control).build();
272 }
273 mCs.onNext(mToken, control);
274 } catch (RemoteException ex) {
275 ex.rethrowAsRuntimeException();
276 }
277 }
278 public void onError(Throwable t) {
279 try {
280 mCs.onError(mToken, t.toString());
281 } catch (RemoteException ex) {
282 ex.rethrowAsRuntimeException();
283 }
284 }
285 public void onComplete() {
286 try {
287 mCs.onComplete(mToken);
288 } catch (RemoteException ex) {
289 ex.rethrowAsRuntimeException();
290 }
Fabian Kozynski1bb26b52020-01-08 18:20:36 -0500291 }
Matt Pietal10eae312019-12-10 08:46:45 -0500292 }
293
Fabian Kozynskid6f48402020-02-05 13:01:04 -0500294 /**
295 * Request SystemUI to prompt the user to add a control to favorites.
296 *
297 * @param context A context
298 * @param componentName Component name of the {@link ControlsProviderService}
299 * @param control A stateless control to show to the user
300 */
301 public static void requestAddControl(@NonNull Context context,
302 @NonNull ComponentName componentName,
303 @NonNull Control control) {
304 Preconditions.checkNotNull(context);
305 Preconditions.checkNotNull(componentName);
306 Preconditions.checkNotNull(control);
Fabian Kozynskia9be39d2020-03-27 10:08:12 -0400307 final String controlsPackage = context.getString(
308 com.android.internal.R.string.config_controlsPackage);
Fabian Kozynskid6f48402020-02-05 13:01:04 -0500309 Intent intent = new Intent(ACTION_ADD_CONTROL);
310 intent.putExtra(Intent.EXTRA_COMPONENT_NAME, componentName);
Fabian Kozynskia9be39d2020-03-27 10:08:12 -0400311 intent.setPackage(controlsPackage);
Fabian Kozynskid6f48402020-02-05 13:01:04 -0500312 if (isStatelessControl(control)) {
313 intent.putExtra(EXTRA_CONTROL, control);
314 } else {
315 intent.putExtra(EXTRA_CONTROL, new Control.StatelessBuilder(control).build());
316 }
317 context.sendBroadcast(intent, Manifest.permission.BIND_CONTROLS);
318 }
319
Fabian Kozynski1bb26b52020-01-08 18:20:36 -0500320 private static class SubscriptionAdapter extends IControlsSubscription.Stub {
321 final Subscription mSubscription;
Matt Pietal10eae312019-12-10 08:46:45 -0500322
Fabian Kozynski1bb26b52020-01-08 18:20:36 -0500323 SubscriptionAdapter(Subscription s) {
324 this.mSubscription = s;
325 }
326
327 public void request(long n) {
328 mSubscription.request(n);
329 }
330
331 public void cancel() {
332 mSubscription.cancel();
333 }
334 }
335
336 private static class ActionMessage {
337 final String mControlId;
338 final ControlAction mAction;
339 final IControlsActionCallback mCb;
340
341 ActionMessage(String controlId, ControlAction action, IControlsActionCallback cb) {
342 this.mControlId = controlId;
Matt Pietal10eae312019-12-10 08:46:45 -0500343 this.mAction = action;
Fabian Kozynski1bb26b52020-01-08 18:20:36 -0500344 this.mCb = cb;
345 }
346 }
347
348 private static class SubscribeMessage {
349 final List<String> mControlIds;
350 final IControlsSubscriber mSubscriber;
351
352 SubscribeMessage(List<String> controlIds, IControlsSubscriber subscriber) {
353 this.mControlIds = controlIds;
354 this.mSubscriber = subscriber;
Matt Pietal10eae312019-12-10 08:46:45 -0500355 }
356 }
Matt Pietal10eae312019-12-10 08:46:45 -0500357}