blob: 6bd376a19fc5ea6a132ef4d20d4cf4e59eb1aacf [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
Matt Pietal1f7c8172020-05-26 14:29:06 -0400101 * 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.
Matt Pietald0553b02020-02-13 07:08:58 -0500107 */
108 @Nullable
Matt Pietale44217b2020-02-26 08:48:37 -0500109 public Publisher<Control> createPublisherForSuggested() {
Matt Pietald0553b02020-02-13 07:08:58 -0500110 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
Matt Pietale44217b2020-02-26 08:48:37 -0500117 * {@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}.
Matt Pietal10eae312019-12-10 08:46:45 -0500119 */
Fabian Kozynski1bb26b52020-01-08 18:20:36 -0500120 @NonNull
Matt Pietale44217b2020-02-26 08:48:37 -0500121 public abstract Publisher<Control> createPublisherFor(@NonNull List<String> controlIds);
Matt Pietal10eae312019-12-10 08:46:45 -0500122
123 /**
124 * The user has interacted with a Control. The action is dictated by the type of
Fabian Kozynski1bb26b52020-01-08 18:20:36 -0500125 * {@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.
Matt Pietal10eae312019-12-10 08:46:45 -0500130 */
Fabian Kozynski1bb26b52020-01-08 18:20:36 -0500131 public abstract void performControlAction(@NonNull String controlId,
132 @NonNull ControlAction action, @NonNull Consumer<Integer> consumer);
Matt Pietal10eae312019-12-10 08:46:45 -0500133
134 @Override
Fabian Kozynski1bb26b52020-01-08 18:20:36 -0500135 @NonNull
136 public final IBinder onBind(@NonNull Intent intent) {
Matt Pietal10eae312019-12-10 08:46:45 -0500137 mHandler = new RequestHandler(Looper.getMainLooper());
138
Fabian Kozynskiee57f492019-12-30 12:30:24 -0500139 Bundle bundle = intent.getBundleExtra(CALLBACK_BUNDLE);
Fabian Kozynskiee57f492019-12-30 12:30:24 -0500140 mToken = bundle.getBinder(CALLBACK_TOKEN);
Matt Pietal10eae312019-12-10 08:46:45 -0500141
142 return new IControlsProvider.Stub() {
Matt Pietald0553b02020-02-13 07:08:58 -0500143 public void load(IControlsSubscriber subscriber) {
144 mHandler.obtainMessage(RequestHandler.MSG_LOAD, subscriber).sendToTarget();
Matt Pietal10eae312019-12-10 08:46:45 -0500145 }
146
Matt Pietald0553b02020-02-13 07:08:58 -0500147 public void loadSuggested(IControlsSubscriber subscriber) {
148 mHandler.obtainMessage(RequestHandler.MSG_LOAD_SUGGESTED, subscriber)
149 .sendToTarget();
Matt Pietal587a5f72020-02-07 09:34:49 -0500150 }
151
Fabian Kozynski1bb26b52020-01-08 18:20:36 -0500152 public void subscribe(List<String> controlIds,
153 IControlsSubscriber subscriber) {
154 SubscribeMessage msg = new SubscribeMessage(controlIds, subscriber);
155 mHandler.obtainMessage(RequestHandler.MSG_SUBSCRIBE, msg).sendToTarget();
Matt Pietal10eae312019-12-10 08:46:45 -0500156 }
157
Fabian Kozynski1bb26b52020-01-08 18:20:36 -0500158 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();
Matt Pietal10eae312019-12-10 08:46:45 -0500162 }
163 };
164 }
165
166 @Override
Matt Pietale44217b2020-02-26 08:48:37 -0500167 public final boolean onUnbind(@NonNull Intent intent) {
Matt Pietal10eae312019-12-10 08:46:45 -0500168 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;
Fabian Kozynski1bb26b52020-01-08 18:20:36 -0500175 private static final int MSG_ACTION = 3;
Matt Pietal587a5f72020-02-07 09:34:49 -0500176 private static final int MSG_LOAD_SUGGESTED = 4;
177
Matt Pietal10eae312019-12-10 08:46:45 -0500178 RequestHandler(Looper looper) {
179 super(looper);
180 }
181
182 public void handleMessage(Message msg) {
183 switch(msg.what) {
Matt Pietald0553b02020-02-13 07:08:58 -0500184 case MSG_LOAD: {
185 final IControlsSubscriber cs = (IControlsSubscriber) msg.obj;
186 final SubscriberProxy proxy = new SubscriberProxy(true, mToken, cs);
Matt Pietal587a5f72020-02-07 09:34:49 -0500187
Matt Pietale44217b2020-02-26 08:48:37 -0500188 ControlsProviderService.this.createPublisherForAllAvailable().subscribe(proxy);
Matt Pietal10eae312019-12-10 08:46:45 -0500189 break;
Matt Pietald0553b02020-02-13 07:08:58 -0500190 }
Fabian Kozynski1bb26b52020-01-08 18:20:36 -0500191
Matt Pietald0553b02020-02-13 07:08:58 -0500192 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 =
Matt Pietale44217b2020-02-26 08:48:37 -0500197 ControlsProviderService.this.createPublisherForSuggested();
Matt Pietald0553b02020-02-13 07:08:58 -0500198 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: {
Fabian Kozynski1bb26b52020-01-08 18:20:36 -0500208 final SubscribeMessage sMsg = (SubscribeMessage) msg.obj;
Matt Pietald0553b02020-02-13 07:08:58 -0500209 final SubscriberProxy proxy = new SubscriberProxy(false, mToken,
210 sMsg.mSubscriber);
Fabian Kozynski1bb26b52020-01-08 18:20:36 -0500211
Matt Pietale44217b2020-02-26 08:48:37 -0500212 ControlsProviderService.this.createPublisherFor(sMsg.mControlIds)
213 .subscribe(proxy);
Matt Pietald0553b02020-02-13 07:08:58 -0500214 break;
215 }
216
217 case MSG_ACTION: {
Fabian Kozynski1bb26b52020-01-08 18:20:36 -0500218 final ActionMessage aMsg = (ActionMessage) msg.obj;
219 ControlsProviderService.this.performControlAction(aMsg.mControlId,
220 aMsg.mAction, consumerFor(aMsg.mControlId, aMsg.mCb));
Matt Pietal10eae312019-12-10 08:46:45 -0500221 break;
Matt Pietald0553b02020-02-13 07:08:58 -0500222 }
Matt Pietal10eae312019-12-10 08:46:45 -0500223 }
224 }
Fabian Kozynski1bb26b52020-01-08 18:20:36 -0500225
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 }
Matt Pietald0553b02020-02-13 07:08:58 -0500241 }
Fabian Kozynski1bb26b52020-01-08 18:20:36 -0500242
Matt Pietald0553b02020-02-13 07:08:58 -0500243 private static boolean isStatelessControl(Control control) {
244 return (control.getStatus() == Control.STATUS_UNKNOWN
Fabian Kozynski95dcd242020-03-03 14:25:55 -0500245 && control.getControlTemplate().getTemplateType()
246 == ControlTemplate.TYPE_NO_TEMPLATE
Matt Pietald0553b02020-02-13 07:08:58 -0500247 && 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 }
Fabian Kozynski1bb26b52020-01-08 18:20:36 -0500294 }
Matt Pietal10eae312019-12-10 08:46:45 -0500295 }
296
Fabian Kozynskid6f48402020-02-05 13:01:04 -0500297 /**
298 * Request SystemUI to prompt the user to add a control to favorites.
Fabian Kozynskie134edc2020-06-23 15:46:35 -0400299 * <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.
Fabian Kozynskid6f48402020-02-05 13:01:04 -0500303 *
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);
Fabian Kozynskia9be39d2020-03-27 10:08:12 -0400314 final String controlsPackage = context.getString(
315 com.android.internal.R.string.config_controlsPackage);
Fabian Kozynskid6f48402020-02-05 13:01:04 -0500316 Intent intent = new Intent(ACTION_ADD_CONTROL);
317 intent.putExtra(Intent.EXTRA_COMPONENT_NAME, componentName);
Fabian Kozynskia9be39d2020-03-27 10:08:12 -0400318 intent.setPackage(controlsPackage);
Fabian Kozynskid6f48402020-02-05 13:01:04 -0500319 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
Fabian Kozynski1bb26b52020-01-08 18:20:36 -0500327 private static class SubscriptionAdapter extends IControlsSubscription.Stub {
328 final Subscription mSubscription;
Matt Pietal10eae312019-12-10 08:46:45 -0500329
Fabian Kozynski1bb26b52020-01-08 18:20:36 -0500330 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;
Matt Pietal10eae312019-12-10 08:46:45 -0500350 this.mAction = action;
Fabian Kozynski1bb26b52020-01-08 18:20:36 -0500351 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;
Matt Pietal10eae312019-12-10 08:46:45 -0500362 }
363 }
Matt Pietal10eae312019-12-10 08:46:45 -0500364}