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      * @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 
70     /**
71      * @hide
72      */
73     public static final String CALLBACK_BUNDLE = "CALLBACK_BUNDLE";
74 
75     /**
76      * @hide
77      */
78     public static final String CALLBACK_TOKEN = "CALLBACK_TOKEN";
79 
80     public static final @NonNull String TAG = "ControlsProviderService";
81 
82     private IBinder mToken;
83     private RequestHandler mHandler;
84 
85     /**
86      * 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      */
93     @NonNull
createPublisherForAllAvailable()94     public abstract Publisher<Control> createPublisherForAllAvailable();
95 
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 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.
107      */
108     @Nullable
createPublisherForSuggested()109     public Publisher<Control> createPublisherForSuggested() {
110         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
117      * {@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}.
119      */
120     @NonNull
createPublisherFor(@onNull List<String> controlIds)121     public abstract Publisher<Control> createPublisherFor(@NonNull List<String> controlIds);
122 
123     /**
124      * The user has interacted with a Control. The action is dictated by the type of
125      * {@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.
130      */
performControlAction(@onNull String controlId, @NonNull ControlAction action, @NonNull Consumer<Integer> consumer)131     public abstract void performControlAction(@NonNull String controlId,
132             @NonNull ControlAction action, @NonNull Consumer<Integer> consumer);
133 
134     @Override
135     @NonNull
onBind(@onNull Intent intent)136     public final IBinder onBind(@NonNull Intent intent) {
137         mHandler = new RequestHandler(Looper.getMainLooper());
138 
139         Bundle bundle = intent.getBundleExtra(CALLBACK_BUNDLE);
140         mToken = bundle.getBinder(CALLBACK_TOKEN);
141 
142         return new IControlsProvider.Stub() {
143             public void load(IControlsSubscriber subscriber) {
144                 mHandler.obtainMessage(RequestHandler.MSG_LOAD, subscriber).sendToTarget();
145             }
146 
147             public void loadSuggested(IControlsSubscriber subscriber) {
148                 mHandler.obtainMessage(RequestHandler.MSG_LOAD_SUGGESTED, subscriber)
149                         .sendToTarget();
150             }
151 
152             public void subscribe(List<String> controlIds,
153                     IControlsSubscriber subscriber) {
154                 SubscribeMessage msg = new SubscribeMessage(controlIds, subscriber);
155                 mHandler.obtainMessage(RequestHandler.MSG_SUBSCRIBE, msg).sendToTarget();
156             }
157 
158             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();
162             }
163         };
164     }
165 
166     @Override
167     public final boolean onUnbind(@NonNull Intent intent) {
168         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;
175         private static final int MSG_ACTION = 3;
176         private static final int MSG_LOAD_SUGGESTED = 4;
177 
178         RequestHandler(Looper looper) {
179             super(looper);
180         }
181 
182         public void handleMessage(Message msg) {
183             switch(msg.what) {
184                 case MSG_LOAD: {
185                     final IControlsSubscriber cs = (IControlsSubscriber) msg.obj;
186                     final SubscriberProxy proxy = new SubscriberProxy(true, mToken, cs);
187 
188                     ControlsProviderService.this.createPublisherForAllAvailable().subscribe(proxy);
189                     break;
190                 }
191 
192                 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 =
197                             ControlsProviderService.this.createPublisherForSuggested();
198                     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: {
208                     final SubscribeMessage sMsg = (SubscribeMessage) msg.obj;
209                     final SubscriberProxy proxy = new SubscriberProxy(false, mToken,
210                             sMsg.mSubscriber);
211 
212                     ControlsProviderService.this.createPublisherFor(sMsg.mControlIds)
213                             .subscribe(proxy);
214                     break;
215                 }
216 
217                 case MSG_ACTION: {
218                     final ActionMessage aMsg = (ActionMessage) msg.obj;
219                     ControlsProviderService.this.performControlAction(aMsg.mControlId,
220                             aMsg.mAction, consumerFor(aMsg.mControlId, aMsg.mCb));
221                     break;
222                 }
223             }
224         }
225 
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         }
241     }
242 
243     private static boolean isStatelessControl(Control control) {
244         return (control.getStatus() == Control.STATUS_UNKNOWN
245                 && control.getControlTemplate().getTemplateType()
246                 == ControlTemplate.TYPE_NO_TEMPLATE
247                 && 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             }
294         }
295     }
296 
297     /**
298      * Request SystemUI to prompt the user to add a control to favorites.
299      * <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.
303      *
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);
314         final String controlsPackage = context.getString(
315                 com.android.internal.R.string.config_controlsPackage);
316         Intent intent = new Intent(ACTION_ADD_CONTROL);
317         intent.putExtra(Intent.EXTRA_COMPONENT_NAME, componentName);
318         intent.setPackage(controlsPackage);
319         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 
327     private static class SubscriptionAdapter extends IControlsSubscription.Stub {
328         final Subscription mSubscription;
329 
330         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;
350             this.mAction = action;
351             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;
362         }
363     }
364 }
365