Rebind to ControlsProviderService if package updates

If we are bound to a ControlsProviderService and the package is updated,
re-bind to it using the same flags after it has finished updating.

Test: atest PackageUpdateMonitorTest
Test: atest ControlsProviderLifecycleManagerTest
Test: manual, reinstall app using adb
Fixes: 274458990
Change-Id: I6f3bcc01695f23dcca6cbaca60255157bddbfbe4
This commit is contained in:
Fabián Kozynski
2023-04-17 13:54:32 -04:00
parent 01b56bb057
commit 42e68e106f
7 changed files with 470 additions and 70 deletions

View File

@@ -41,7 +41,8 @@ open class ControlsBindingControllerImpl @Inject constructor(
private val context: Context,
@Background private val backgroundExecutor: DelayableExecutor,
private val lazyController: Lazy<ControlsController>,
userTracker: UserTracker
private val packageUpdateMonitorFactory: PackageUpdateMonitor.Factory,
userTracker: UserTracker,
) : ControlsBindingController {
companion object {
@@ -93,7 +94,8 @@ open class ControlsBindingControllerImpl @Inject constructor(
backgroundExecutor,
actionCallbackService,
currentUser,
component
component,
packageUpdateMonitorFactory
)
}

View File

@@ -16,6 +16,7 @@
package com.android.systemui.controls.controller
import android.annotation.WorkerThread
import android.content.ComponentName
import android.content.Context
import android.content.Intent
@@ -23,7 +24,6 @@ import android.content.ServiceConnection
import android.os.Binder
import android.os.Bundle
import android.os.IBinder
import android.os.RemoteException
import android.os.UserHandle
import android.service.controls.ControlsProviderService
import android.service.controls.ControlsProviderService.CALLBACK_BUNDLE
@@ -38,6 +38,7 @@ import android.util.Log
import com.android.internal.annotations.GuardedBy
import com.android.systemui.util.concurrency.DelayableExecutor
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
/**
* Manager for the lifecycle of the connection to a given [ControlsProviderService].
@@ -45,6 +46,9 @@ import java.util.concurrent.TimeUnit
* This class handles binding and unbinding and requests to the service. The class will queue
* requests until the service is connected and dispatch them then.
*
* If the provider app is updated, and we are currently bound to it, it will try to rebind after
* update is completed.
*
* @property context A SystemUI context for binding to the services
* @property executor A delayable executor for posting timeouts
* @property actionCallbackService a callback interface to hand the remote service for sending
@@ -59,22 +63,22 @@ class ControlsProviderLifecycleManager(
private val executor: DelayableExecutor,
private val actionCallbackService: IControlsActionCallback.Stub,
val user: UserHandle,
val componentName: ComponentName
) : IBinder.DeathRecipient {
val componentName: ComponentName,
packageUpdateMonitorFactory: PackageUpdateMonitor.Factory,
) {
val token: IBinder = Binder()
private var requiresBound = false
@GuardedBy("queuedServiceMethods")
private val queuedServiceMethods: MutableSet<ServiceMethod> = ArraySet()
private var wrapper: ServiceWrapper? = null
private var bindTryCount = 0
private val TAG = javaClass.simpleName
private var onLoadCanceller: Runnable? = null
private var lastForPanel = false
companion object {
private const val BIND_RETRY_DELAY = 1000L // ms
private const val LOAD_TIMEOUT_SECONDS = 20L // seconds
private const val MAX_BIND_RETRIES = 5
private const val DEBUG = true
private val BIND_FLAGS = Context.BIND_AUTO_CREATE or Context.BIND_FOREGROUND_SERVICE or
Context.BIND_NOT_PERCEPTIBLE
@@ -91,60 +95,56 @@ class ControlsProviderLifecycleManager(
})
}
private fun bindService(bind: Boolean, forPanel: Boolean = false) {
executor.execute {
requiresBound = bind
if (bind) {
if (bindTryCount != MAX_BIND_RETRIES && wrapper == null) {
if (DEBUG) {
Log.d(TAG, "Binding service $intent")
}
bindTryCount++
try {
val flags = if (forPanel) BIND_FLAGS_PANEL else BIND_FLAGS
val bound = context
.bindServiceAsUser(intent, serviceConnection, flags, user)
if (!bound) {
context.unbindService(serviceConnection)
}
} catch (e: SecurityException) {
Log.e(TAG, "Failed to bind to service", e)
}
}
} else {
if (DEBUG) {
Log.d(TAG, "Unbinding service $intent")
}
bindTryCount = 0
wrapper?.run {
context.unbindService(serviceConnection)
}
wrapper = null
private val packageUpdateMonitor = packageUpdateMonitorFactory.create(
user,
componentName.packageName,
) {
if (requiresBound) {
// Let's unbind just in case. onBindingDied should have been called and unbound before.
executor.execute {
unbindAndCleanup("package updated")
bindService(true, lastForPanel)
}
}
}
private fun bindService(bind: Boolean, forPanel: Boolean = false) {
executor.execute {
bindServiceBackground(bind, forPanel)
}
}
private val serviceConnection = object : ServiceConnection {
val connected = AtomicBoolean(false)
override fun onServiceConnected(name: ComponentName, service: IBinder) {
if (DEBUG) Log.d(TAG, "onServiceConnected $name")
bindTryCount = 0
wrapper = ServiceWrapper(IControlsProvider.Stub.asInterface(service))
try {
service.linkToDeath(this@ControlsProviderLifecycleManager, 0)
} catch (_: RemoteException) {}
packageUpdateMonitor.startMonitoring()
handlePendingServiceMethods()
}
override fun onServiceDisconnected(name: ComponentName?) {
if (DEBUG) Log.d(TAG, "onServiceDisconnected $name")
wrapper = null
bindService(false)
// No need to call unbind. We may get a new `onServiceConnected`
}
override fun onNullBinding(name: ComponentName?) {
if (DEBUG) Log.d(TAG, "onNullBinding $name")
wrapper = null
context.unbindService(this)
executor.execute {
unbindAndCleanup("null binding")
}
}
override fun onBindingDied(name: ComponentName?) {
super.onBindingDied(name)
if (DEBUG) Log.d(TAG, "onBindingDied $name")
executor.execute {
unbindAndCleanup("binder died")
}
}
}
@@ -159,14 +159,55 @@ class ControlsProviderLifecycleManager(
}
}
override fun binderDied() {
if (wrapper == null) return
wrapper = null
if (requiresBound) {
if (DEBUG) {
Log.d(TAG, "binderDied")
@WorkerThread
private fun bindServiceBackground(bind: Boolean, forPanel: Boolean = true) {
requiresBound = bind
if (bind) {
if (wrapper == null) {
if (DEBUG) {
Log.d(TAG, "Binding service $intent")
}
try {
lastForPanel = forPanel
val flags = if (forPanel) BIND_FLAGS_PANEL else BIND_FLAGS
var bound = false
if (serviceConnection.connected.compareAndSet(false, true)) {
bound = context
.bindServiceAsUser(intent, serviceConnection, flags, user)
}
if (!bound) {
Log.d(TAG, "Couldn't bind to $intent")
doUnbind()
}
} catch (e: SecurityException) {
Log.e(TAG, "Failed to bind to service", e)
// Couldn't even bind. Let's reset the connected value
serviceConnection.connected.set(false)
}
}
// Try rebinding some time later
} else {
unbindAndCleanup("unbind requested")
packageUpdateMonitor.stopMonitoring()
}
}
@WorkerThread
private fun unbindAndCleanup(reason: String) {
if (DEBUG) {
Log.d(TAG, "Unbinding service $intent. Reason: $reason")
}
wrapper = null
try {
doUnbind()
} catch (e: IllegalArgumentException) {
Log.e(TAG, "Failed to unbind service", e)
}
}
@WorkerThread
private fun doUnbind() {
if (serviceConnection.connected.compareAndSet(true, false)) {
context.unbindService(serviceConnection)
}
}
@@ -313,7 +354,7 @@ class ControlsProviderLifecycleManager(
fun run() {
if (!callWrapper()) {
queueServiceMethod(this)
binderDied()
executor.execute { unbindAndCleanup("couldn't call through binder") }
}
}

View File

@@ -0,0 +1,76 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.controls.controller
import android.content.Context
import android.os.Handler
import android.os.UserHandle
import com.android.internal.content.PackageMonitor
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import java.util.concurrent.atomic.AtomicBoolean
/** [PackageMonitor] that tracks when [packageName] has finished updating for user [user]. */
class PackageUpdateMonitor
@AssistedInject
constructor(
@Assisted private val user: UserHandle,
@Assisted private val packageName: String,
@Assisted private val callback: Runnable,
@Background private val bgHandler: Handler,
@Application private val context: Context,
) : PackageMonitor() {
private val monitoring = AtomicBoolean(false)
@AssistedFactory
fun interface Factory {
/**
* Create a [PackageUpdateMonitor] for a given [user] and [packageName]. It will run
* [callback] every time the package finishes updating.
*/
fun create(user: UserHandle, packageName: String, callback: Runnable): PackageUpdateMonitor
}
/** Start monitoring for package updates. No-op if already monitoring. */
fun startMonitoring() {
if (monitoring.compareAndSet(/* expected */ false, /* new */ true)) {
register(context, user, false, bgHandler)
}
}
/** Stop monitoring for package updates. No-op if not monitoring. */
fun stopMonitoring() {
if (monitoring.compareAndSet(/* expected */ true, /* new */ false)) {
unregister()
}
}
/**
* If the package and the user match the ones for this [PackageUpdateMonitor], it will run
* [callback].
*/
override fun onPackageUpdateFinished(packageName: String?, uid: Int) {
super.onPackageUpdateFinished(packageName, uid)
if (packageName == this.packageName && UserHandle.getUserHandleForUid(uid) == user) {
callback.run()
}
}
}

View File

@@ -16,11 +16,11 @@
package com.android.systemui.controls.controller
import android.service.controls.actions.ControlAction
import android.service.controls.IControlsActionCallback
import android.service.controls.IControlsProvider
import android.service.controls.IControlsSubscriber
import android.service.controls.IControlsSubscription
import android.service.controls.actions.ControlAction
import android.service.controls.actions.ControlActionWrapper
import android.util.Log

View File

@@ -41,11 +41,11 @@ import org.mockito.ArgumentCaptor
import org.mockito.Captor
import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.Mockito.`when`
import org.mockito.Mockito.mock
import org.mockito.Mockito.never
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
import org.mockito.MockitoAnnotations
@SmallTest
@@ -378,7 +378,13 @@ class TestableControlsBindingControllerImpl(
executor: DelayableExecutor,
lazyController: Lazy<ControlsController>,
userTracker: UserTracker
) : ControlsBindingControllerImpl(context, executor, lazyController, userTracker) {
) : ControlsBindingControllerImpl(
context,
executor,
lazyController,
mock(PackageUpdateMonitor.Factory::class.java),
userTracker
) {
companion object {
val providers = mutableListOf<ControlsProviderLifecycleManager>()

View File

@@ -30,6 +30,10 @@ import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.argumentCaptor
import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.time.FakeSystemClock
import org.junit.After
import org.junit.Assert.assertEquals
@@ -39,17 +43,17 @@ import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
import org.mockito.ArgumentMatchers
import org.mockito.ArgumentMatchers.any
import org.mockito.ArgumentMatchers.anyString
import org.mockito.ArgumentMatchers.eq
import org.mockito.Captor
import org.mockito.Mock
import org.mockito.Mockito.`when`
import org.mockito.Mockito.anyInt
import org.mockito.Mockito.mock
import org.mockito.Mockito.clearInvocations
import org.mockito.Mockito.inOrder
import org.mockito.Mockito.never
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyNoMoreInteractions
import org.mockito.Mockito.`when`
import org.mockito.MockitoAnnotations
@SmallTest
@@ -62,16 +66,21 @@ class ControlsProviderLifecycleManagerTest : SysuiTestCase() {
private lateinit var subscriberService: IControlsSubscriber.Stub
@Mock
private lateinit var service: IControlsProvider.Stub
@Mock
private lateinit var packageUpdateMonitor: PackageUpdateMonitor
@Captor
private lateinit var wrapperCaptor: ArgumentCaptor<ControlActionWrapper>
private lateinit var packageUpdateMonitorFactory: FakePackageUpdateMonitorFactory
private val componentName = ComponentName("test.pkg", "test.cls")
private lateinit var manager: ControlsProviderLifecycleManager
private lateinit var executor: FakeExecutor
private lateinit var fakeSystemClock: FakeSystemClock
companion object {
fun <T> capture(argumentCaptor: ArgumentCaptor<T>): T = argumentCaptor.capture()
private val USER = UserHandle.of(0)
}
@Before
@@ -79,16 +88,20 @@ class ControlsProviderLifecycleManagerTest : SysuiTestCase() {
MockitoAnnotations.initMocks(this)
context.addMockService(componentName, service)
executor = FakeExecutor(FakeSystemClock())
fakeSystemClock = FakeSystemClock()
executor = FakeExecutor(fakeSystemClock)
`when`(service.asBinder()).thenCallRealMethod()
`when`(service.queryLocalInterface(ArgumentMatchers.anyString())).thenReturn(service)
`when`(service.queryLocalInterface(anyString())).thenReturn(service)
packageUpdateMonitorFactory = FakePackageUpdateMonitorFactory(packageUpdateMonitor)
manager = ControlsProviderLifecycleManager(
context,
executor,
actionCallbackService,
UserHandle.of(0),
componentName
USER,
componentName,
packageUpdateMonitorFactory,
)
}
@@ -122,7 +135,7 @@ class ControlsProviderLifecycleManagerTest : SysuiTestCase() {
@Test
fun testNullBinding() {
val mockContext = mock(Context::class.java)
val mockContext = mock<Context>()
lateinit var serviceConnection: ServiceConnection
`when`(mockContext.bindServiceAsUser(any(), any(), anyInt(), any())).thenAnswer {
val component = (it.arguments[0] as Intent).component
@@ -139,8 +152,9 @@ class ControlsProviderLifecycleManagerTest : SysuiTestCase() {
mockContext,
executor,
actionCallbackService,
UserHandle.of(0),
componentName
USER,
componentName,
packageUpdateMonitorFactory,
)
nullManager.bindService()
@@ -229,14 +243,15 @@ class ControlsProviderLifecycleManagerTest : SysuiTestCase() {
@Test
fun testFalseBindCallsUnbind() {
val falseContext = mock(Context::class.java)
val falseContext = mock<Context>()
`when`(falseContext.bindServiceAsUser(any(), any(), anyInt(), any())).thenReturn(false)
val manager = ControlsProviderLifecycleManager(
falseContext,
executor,
actionCallbackService,
UserHandle.of(0),
componentName
USER,
componentName,
packageUpdateMonitorFactory,
)
manager.bindService()
executor.runAllReady()
@@ -247,4 +262,150 @@ class ControlsProviderLifecycleManagerTest : SysuiTestCase() {
verify(falseContext).bindServiceAsUser(any(), captor.capture(), anyInt(), any())
verify(falseContext).unbindService(captor.value)
}
@Test
fun testPackageUpdateMonitor_createdWithCorrectValues() {
assertEquals(USER, packageUpdateMonitorFactory.lastUser)
assertEquals(componentName.packageName, packageUpdateMonitorFactory.lastPackage)
}
@Test
fun testBound_packageMonitorStartsMonitoring() {
manager.bindService()
executor.runAllReady()
// Service will connect and monitoring should start
verify(packageUpdateMonitor).startMonitoring()
}
@Test
fun testOnPackageUpdateWhileBound_unbound_thenBindAgain() {
val mockContext = mock<Context> {
`when`(bindServiceAsUser(any(), any(), anyInt(), any())).thenReturn(true)
}
val manager = ControlsProviderLifecycleManager(
mockContext,
executor,
actionCallbackService,
USER,
componentName,
packageUpdateMonitorFactory,
)
manager.bindService()
executor.runAllReady()
clearInvocations(mockContext)
packageUpdateMonitorFactory.lastCallback?.run()
executor.runAllReady()
val inOrder = inOrder(mockContext)
inOrder.verify(mockContext).unbindService(any())
inOrder.verify(mockContext).bindServiceAsUser(any(), any(), anyInt(), any())
}
@Test
fun testOnPackageUpdateWhenNotBound_nothingHappens() {
val mockContext = mock<Context> {
`when`(bindServiceAsUser(any(), any(), anyInt(), any())).thenReturn(true)
}
ControlsProviderLifecycleManager(
mockContext,
executor,
actionCallbackService,
USER,
componentName,
packageUpdateMonitorFactory,
)
packageUpdateMonitorFactory.lastCallback?.run()
verifyNoMoreInteractions(mockContext)
}
@Test
fun testUnbindService_stopsTracking() {
manager.bindService()
manager.unbindService()
executor.runAllReady()
verify(packageUpdateMonitor).stopMonitoring()
}
@Test
fun testRebindForPanelWithSameFlags() {
val mockContext = mock<Context> {
`when`(bindServiceAsUser(any(), any(), anyInt(), any())).thenReturn(true)
}
val manager = ControlsProviderLifecycleManager(
mockContext,
executor,
actionCallbackService,
USER,
componentName,
packageUpdateMonitorFactory,
)
manager.bindServiceForPanel()
executor.runAllReady()
val flagsCaptor = argumentCaptor<Int>()
verify(mockContext).bindServiceAsUser(any(), any(), capture(flagsCaptor), any())
clearInvocations(mockContext)
packageUpdateMonitorFactory.lastCallback?.run()
executor.runAllReady()
verify(mockContext).bindServiceAsUser(any(), any(), eq(flagsCaptor.value), any())
}
@Test
fun testBindAfterSecurityExceptionWorks() {
val mockContext = mock<Context> {
`when`(bindServiceAsUser(any(), any(), anyInt(), any()))
.thenThrow(SecurityException("exception"))
}
val manager = ControlsProviderLifecycleManager(
mockContext,
executor,
actionCallbackService,
USER,
componentName,
packageUpdateMonitorFactory,
)
manager.bindServiceForPanel()
executor.runAllReady()
`when`(mockContext.bindServiceAsUser(any(), any(), anyInt(), any())).thenReturn(true)
manager.bindServiceForPanel()
executor.runAllReady()
verify(mockContext, times(2)).bindServiceAsUser(any(), any(), anyInt(), any())
}
private class FakePackageUpdateMonitorFactory(
private val monitor: PackageUpdateMonitor
) : PackageUpdateMonitor.Factory {
var lastUser: UserHandle? = null
var lastPackage: String? = null
var lastCallback: Runnable? = null
override fun create(
user: UserHandle,
packageName: String,
callback: Runnable
): PackageUpdateMonitor {
lastUser = user
lastPackage = packageName
lastCallback = callback
return monitor
}
}
}

View File

@@ -0,0 +1,114 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.controls.controller
import android.content.Context
import android.os.Handler
import android.os.UserHandle
import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.mock
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mockito.clearInvocations
import org.mockito.Mockito.never
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyNoMoreInteractions
import org.mockito.MockitoAnnotations
@SmallTest
@RunWith(AndroidTestingRunner::class)
class PackageUpdateMonitorTest : SysuiTestCase() {
@Mock private lateinit var context: Context
@Mock private lateinit var bgHandler: Handler
private lateinit var underTest: PackageUpdateMonitor
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
}
@Test
fun startMonitoring_registerOnlyOnce() {
underTest = PackageUpdateMonitor(USER, PACKAGE, {}, bgHandler, context)
underTest.startMonitoring()
// There are two receivers registered
verify(context, times(2))
.registerReceiverAsUser(any(), eq(USER), any(), eq(null), eq(bgHandler))
underTest.startMonitoring()
verifyNoMoreInteractions(context)
}
@Test
fun stopMonitoring_unregistersOnlyOnce() {
underTest = PackageUpdateMonitor(USER, PACKAGE, {}, bgHandler, context)
underTest.startMonitoring()
clearInvocations(context)
underTest.stopMonitoring()
verify(context).unregisterReceiver(any())
underTest.stopMonitoring()
verifyNoMoreInteractions(context)
}
@Test
fun onPackageUpdated_correctPackageAndUser_callbackRuns() {
val callback = mock<Runnable>()
underTest = PackageUpdateMonitor(USER, PACKAGE, callback, bgHandler, context)
underTest.onPackageUpdateFinished(PACKAGE, UserHandle.getUid(USER.identifier, 10000))
verify(callback).run()
}
@Test
fun onPackageUpdated_correctPackage_wrongUser_callbackDoesntRun() {
val callback = mock<Runnable>()
underTest = PackageUpdateMonitor(USER, PACKAGE, callback, bgHandler, context)
underTest.onPackageUpdateFinished(PACKAGE, UserHandle.getUid(USER.identifier + 1, 10000))
verify(callback, never()).run()
}
@Test
fun onPackageUpdated_wrongPackage_correctUser_callbackDoesntRun() {
val callback = mock<Runnable>()
underTest = PackageUpdateMonitor(USER, PACKAGE, callback, bgHandler, context)
underTest.onPackageUpdateFinished("bad", UserHandle.getUid(USER.identifier + 1, 10000))
verify(callback, never()).run()
}
companion object {
private val USER = UserHandle.of(0)
private val PACKAGE = "pkg"
}
}