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