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