• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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