1 /*
2  * Copyright (C) 2020 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 
17 package com.android.systemui.controls.controller
18 
19 import android.annotation.WorkerThread
20 import android.content.ComponentName
21 import android.content.Context
22 import android.content.Intent
23 import android.content.ServiceConnection
24 import android.os.Binder
25 import android.os.Bundle
26 import android.os.IBinder
27 import android.os.UserHandle
28 import android.service.controls.ControlsProviderService
29 import android.service.controls.ControlsProviderService.CALLBACK_BUNDLE
30 import android.service.controls.ControlsProviderService.CALLBACK_TOKEN
31 import android.service.controls.IControlsActionCallback
32 import android.service.controls.IControlsProvider
33 import android.service.controls.IControlsSubscriber
34 import android.service.controls.IControlsSubscription
35 import android.service.controls.actions.ControlAction
36 import android.util.ArraySet
37 import android.util.Log
38 import com.android.internal.annotations.GuardedBy
39 import com.android.systemui.util.concurrency.DelayableExecutor
40 import java.util.concurrent.TimeUnit
41 import java.util.concurrent.atomic.AtomicBoolean
42 
43 /**
44  * Manager for the lifecycle of the connection to a given [ControlsProviderService].
45  *
46  * This class handles binding and unbinding and requests to the service. The class will queue
47  * requests until the service is connected and dispatch them then.
48  *
49  * If the provider app is updated, and we are currently bound to it, it will try to rebind after
50  * update is completed.
51  *
52  * @property context A SystemUI context for binding to the services
53  * @property executor A delayable executor for posting timeouts
54  * @property actionCallbackService a callback interface to hand the remote service for sending
55  *                                 action responses
56  * @property subscriberService an "subscriber" interface for requesting and accepting updates for
57  *                             controls from the service.
58  * @property user the user for whose this service should be bound.
59  * @property componentName the name of the component for the service.
60  */
61 class ControlsProviderLifecycleManager(
62     private val context: Context,
63     private val executor: DelayableExecutor,
64     private val actionCallbackService: IControlsActionCallback.Stub,
65     val user: UserHandle,
66     val componentName: ComponentName,
67     packageUpdateMonitorFactory: PackageUpdateMonitor.Factory,
68 ) {
69 
70     val token: IBinder = Binder()
71     private var requiresBound = false
72     @GuardedBy("queuedServiceMethods")
73     private val queuedServiceMethods: MutableSet<ServiceMethod> = ArraySet()
74     private var wrapper: ServiceWrapper? = null
75     private val TAG = javaClass.simpleName
76     private var onLoadCanceller: Runnable? = null
77 
78     private var lastForPanel = false
79 
80     companion object {
81         private const val LOAD_TIMEOUT_SECONDS = 20L // seconds
82         private const val DEBUG = true
83         private val BIND_FLAGS = Context.BIND_AUTO_CREATE or Context.BIND_FOREGROUND_SERVICE or
84             Context.BIND_NOT_PERCEPTIBLE
85         // Use BIND_NOT_PERCEPTIBLE so it will be at lower priority from SystemUI.
86         // However, don't use WAIVE_PRIORITY, as by itself, it will kill the app
87         // once the Task is finished in the device controls panel.
88         private val BIND_FLAGS_PANEL = Context.BIND_AUTO_CREATE or Context.BIND_NOT_PERCEPTIBLE
89     }
90 
<lambda>null91     private val intent = Intent(ControlsProviderService.SERVICE_CONTROLS).apply {
92         component = componentName
93         putExtra(CALLBACK_BUNDLE, Bundle().apply {
94             putBinder(CALLBACK_TOKEN, token)
95         })
96     }
97 
98     private val packageUpdateMonitor = packageUpdateMonitorFactory.create(
99         user,
100         componentName.packageName,
<lambda>null101     ) {
102         if (requiresBound) {
103             // Let's unbind just in case. onBindingDied should have been called and unbound before.
104             executor.execute {
105                 unbindAndCleanup("package updated")
106                 bindService(true, lastForPanel)
107             }
108         }
109     }
110 
bindServicenull111     private fun bindService(bind: Boolean, forPanel: Boolean = false) {
112         executor.execute {
113             bindServiceBackground(bind, forPanel)
114         }
115     }
116 
117     private val serviceConnection = object : ServiceConnection {
118 
119         val connected = AtomicBoolean(false)
120 
onServiceConnectednull121         override fun onServiceConnected(name: ComponentName, service: IBinder) {
122             if (DEBUG) Log.d(TAG, "onServiceConnected $name")
123             wrapper = ServiceWrapper(IControlsProvider.Stub.asInterface(service))
124             packageUpdateMonitor.startMonitoring()
125             handlePendingServiceMethods()
126         }
127 
onServiceDisconnectednull128         override fun onServiceDisconnected(name: ComponentName?) {
129             if (DEBUG) Log.d(TAG, "onServiceDisconnected $name")
130             wrapper = null
131             // No need to call unbind. We may get a new `onServiceConnected`
132         }
133 
onNullBindingnull134         override fun onNullBinding(name: ComponentName?) {
135             if (DEBUG) Log.d(TAG, "onNullBinding $name")
136             wrapper = null
137             executor.execute {
138                 unbindAndCleanup("null binding")
139             }
140         }
141 
onBindingDiednull142         override fun onBindingDied(name: ComponentName?) {
143             super.onBindingDied(name)
144             if (DEBUG) Log.d(TAG, "onBindingDied $name")
145             executor.execute {
146                 unbindAndCleanup("binder died")
147             }
148         }
149     }
150 
handlePendingServiceMethodsnull151     private fun handlePendingServiceMethods() {
152         val queue = synchronized(queuedServiceMethods) {
153             ArraySet(queuedServiceMethods).also {
154                 queuedServiceMethods.clear()
155             }
156         }
157         queue.forEach {
158             it.run()
159         }
160     }
161 
162     @WorkerThread
bindServiceBackgroundnull163     private fun bindServiceBackground(bind: Boolean, forPanel: Boolean = true) {
164         requiresBound = bind
165         if (bind) {
166             if (wrapper == null) {
167                 if (DEBUG) {
168                     Log.d(TAG, "Binding service $intent")
169                 }
170                 try {
171                     lastForPanel = forPanel
172                     val flags = if (forPanel) BIND_FLAGS_PANEL else BIND_FLAGS
173                     var bound = false
174                     if (serviceConnection.connected.compareAndSet(false, true)) {
175                         bound = context
176                             .bindServiceAsUser(intent, serviceConnection, flags, user)
177                     }
178                     if (!bound) {
179                         Log.d(TAG, "Couldn't bind to $intent")
180                         doUnbind()
181                     }
182                 } catch (e: SecurityException) {
183                     Log.e(TAG, "Failed to bind to service", e)
184                     // Couldn't even bind. Let's reset the connected value
185                     serviceConnection.connected.set(false)
186                 }
187             }
188         } else {
189             unbindAndCleanup("unbind requested")
190             packageUpdateMonitor.stopMonitoring()
191         }
192     }
193 
194     @WorkerThread
unbindAndCleanupnull195     private fun unbindAndCleanup(reason: String) {
196         if (DEBUG) {
197             Log.d(TAG, "Unbinding service $intent. Reason: $reason")
198         }
199         wrapper = null
200         try {
201             doUnbind()
202         } catch (e: IllegalArgumentException) {
203             Log.e(TAG, "Failed to unbind service", e)
204         }
205     }
206 
207     @WorkerThread
doUnbindnull208     private fun doUnbind() {
209         if (serviceConnection.connected.compareAndSet(true, false)) {
210             context.unbindService(serviceConnection)
211         }
212     }
213 
queueServiceMethodnull214     private fun queueServiceMethod(sm: ServiceMethod) {
215         synchronized(queuedServiceMethods) {
216             queuedServiceMethods.add(sm)
217         }
218     }
219 
invokeOrQueuenull220     private fun invokeOrQueue(sm: ServiceMethod) {
221         wrapper?.run {
222             sm.run()
223         } ?: run {
224             queueServiceMethod(sm)
225             bindService(true)
226         }
227     }
228 
229     /**
230      * Request a call to [IControlsProvider.load].
231      *
232      * If the service is not bound, the call will be queued and the service will be bound first.
233      * The service will be unbound after the controls are returned or the call times out.
234      *
235      * @param subscriber the subscriber that manages coordination for loading controls
236      */
maybeBindAndLoadnull237     fun maybeBindAndLoad(subscriber: IControlsSubscriber.Stub) {
238         onLoadCanceller = executor.executeDelayed({
239             // Didn't receive a response in time, log and send back error
240             Log.d(TAG, "Timeout waiting onLoad for $componentName")
241             subscriber.onError(token, "Timeout waiting onLoad")
242             unbindService()
243         }, LOAD_TIMEOUT_SECONDS, TimeUnit.SECONDS)
244 
245         invokeOrQueue(Load(subscriber))
246     }
247 
248     /**
249      * Request a call to [IControlsProvider.loadSuggested].
250      *
251      * If the service is not bound, the call will be queued and the service will be bound first.
252      * The service will be unbound if the call times out.
253      *
254      * @param subscriber the subscriber that manages coordination for loading controls
255      */
maybeBindAndLoadSuggestednull256     fun maybeBindAndLoadSuggested(subscriber: IControlsSubscriber.Stub) {
257         onLoadCanceller = executor.executeDelayed({
258             // Didn't receive a response in time, log and send back error
259             Log.d(TAG, "Timeout waiting onLoadSuggested for $componentName")
260             subscriber.onError(token, "Timeout waiting onLoadSuggested")
261             unbindService()
262         }, LOAD_TIMEOUT_SECONDS, TimeUnit.SECONDS)
263 
264         invokeOrQueue(Suggest(subscriber))
265     }
266 
cancelLoadTimeoutnull267     fun cancelLoadTimeout() {
268         onLoadCanceller?.run()
269         onLoadCanceller = null
270     }
271 
272     /**
273      * Request a subscription to the [Publisher] returned by [ControlsProviderService.publisherFor]
274      *
275      * If the service is not bound, the call will be queued and the service will be bound first.
276      *
277      * @param controlIds a list of the ids of controls to send status back.
278      */
maybeBindAndSubscribenull279     fun maybeBindAndSubscribe(controlIds: List<String>, subscriber: IControlsSubscriber) =
280         invokeOrQueue(Subscribe(controlIds, subscriber))
281 
282     /**
283      * Request a call to [ControlsProviderService.performControlAction].
284      *
285      * If the service is not bound, the call will be queued and the service will be bound first.
286      *
287      * @param controlId the id of the [Control] the action is performed on
288      * @param action the action performed
289      */
290     fun maybeBindAndSendAction(controlId: String, action: ControlAction) =
291         invokeOrQueue(Action(controlId, action))
292 
293     /**
294      * Starts the subscription to the [ControlsProviderService] and requests status of controls.
295      *
296      * @param subscription the subscription to use to request controls
297      * @see maybeBindAndLoad
298      */
299     fun startSubscription(subscription: IControlsSubscription, requestLimit: Long) {
300         if (DEBUG) {
301             Log.d(TAG, "startSubscription: $subscription")
302         }
303 
304         wrapper?.request(subscription, requestLimit)
305     }
306 
307     /**
308      * Cancels the subscription to the [ControlsProviderService].
309      *
310      * @param subscription the subscription to cancel
311      * @see maybeBindAndLoad
312      */
cancelSubscriptionnull313     fun cancelSubscription(subscription: IControlsSubscription) {
314         if (DEBUG) {
315             Log.d(TAG, "cancelSubscription: $subscription")
316         }
317 
318         wrapper?.cancel(subscription)
319     }
320 
321     /**
322      * Request bind to the service.
323      */
bindServicenull324     fun bindService() {
325         bindService(true)
326     }
327 
bindServiceForPanelnull328     fun bindServiceForPanel() {
329         bindService(bind = true, forPanel = true)
330     }
331 
332     /**
333      * Request unbind from the service.
334      */
unbindServicenull335     fun unbindService() {
336         onLoadCanceller?.run()
337         onLoadCanceller = null
338 
339         bindService(false)
340     }
341 
toStringnull342     override fun toString(): String {
343         return StringBuilder("ControlsProviderLifecycleManager(").apply {
344             append("component=$componentName")
345             append(", user=$user")
346             append(")")
347         }.toString()
348     }
349 
350     /**
351      * Service methods that can be queued or invoked, and are retryable for failure scenarios
352      */
353     abstract inner class ServiceMethod {
runnull354         fun run() {
355             if (!callWrapper()) {
356                 queueServiceMethod(this)
357                 executor.execute { unbindAndCleanup("couldn't call through binder") }
358             }
359         }
360 
callWrappernull361         internal abstract fun callWrapper(): Boolean
362     }
363 
364     inner class Load(val subscriber: IControlsSubscriber.Stub) : ServiceMethod() {
365         override fun callWrapper(): Boolean {
366             if (DEBUG) {
367                 Log.d(TAG, "load $componentName")
368             }
369             return wrapper?.load(subscriber) ?: false
370         }
371     }
372 
373     inner class Suggest(val subscriber: IControlsSubscriber.Stub) : ServiceMethod() {
callWrappernull374         override fun callWrapper(): Boolean {
375             if (DEBUG) {
376                 Log.d(TAG, "suggest $componentName")
377             }
378             return wrapper?.loadSuggested(subscriber) ?: false
379         }
380     }
381     inner class Subscribe(
382         val list: List<String>,
383         val subscriber: IControlsSubscriber
384     ) : ServiceMethod() {
callWrappernull385         override fun callWrapper(): Boolean {
386             if (DEBUG) {
387                 Log.d(TAG, "subscribe $componentName - $list")
388             }
389 
390             return wrapper?.subscribe(list, subscriber) ?: false
391         }
392     }
393 
394     inner class Action(val id: String, val action: ControlAction) : ServiceMethod() {
callWrappernull395         override fun callWrapper(): Boolean {
396             if (DEBUG) {
397                 Log.d(TAG, "onAction $componentName - $id")
398             }
399             return wrapper?.action(id, action, actionCallbackService) ?: false
400         }
401     }
402 }
403