From 42e68e106f8b30eded39e0a5f0b8126e2ea07dac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A1n=20Kozynski?= Date: Mon, 17 Apr 2023 13:54:32 -0400 Subject: [PATCH] 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 --- .../ControlsBindingControllerImpl.kt | 6 +- .../ControlsProviderLifecycleManager.kt | 139 ++++++++----- .../controller/PackageUpdateMonitor.kt | 76 +++++++ .../controls/controller/ServiceWrapper.kt | 2 +- .../ControlsBindingControllerImplTest.kt | 10 +- .../ControlsProviderLifecycleManagerTest.kt | 193 ++++++++++++++++-- .../controller/PackageUpdateMonitorTest.kt | 114 +++++++++++ 7 files changed, 470 insertions(+), 70 deletions(-) create mode 100644 packages/SystemUI/src/com/android/systemui/controls/controller/PackageUpdateMonitor.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/controls/controller/PackageUpdateMonitorTest.kt diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingControllerImpl.kt index 3d6d3356fb553..18bd467566603 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingControllerImpl.kt @@ -41,7 +41,8 @@ open class ControlsBindingControllerImpl @Inject constructor( private val context: Context, @Background private val backgroundExecutor: DelayableExecutor, private val lazyController: Lazy, - 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 ) } diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManager.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManager.kt index 217f4d89e24c3..cb2476ce0ebf6 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManager.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManager.kt @@ -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 = 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") } } } diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/PackageUpdateMonitor.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/PackageUpdateMonitor.kt new file mode 100644 index 0000000000000..1973b620b34fd --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/controls/controller/PackageUpdateMonitor.kt @@ -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() + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ServiceWrapper.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ServiceWrapper.kt index 2c717f5af8da2..45cb13be361ed 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/controller/ServiceWrapper.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ServiceWrapper.kt @@ -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 diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsBindingControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsBindingControllerImplTest.kt index ebbe096b0da32..26cbd7703075b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsBindingControllerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsBindingControllerImplTest.kt @@ -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, userTracker: UserTracker -) : ControlsBindingControllerImpl(context, executor, lazyController, userTracker) { +) : ControlsBindingControllerImpl( + context, + executor, + lazyController, + mock(PackageUpdateMonitor.Factory::class.java), + userTracker +) { companion object { val providers = mutableListOf() diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManagerTest.kt index da548f7ccef2a..b5d34768dc9fd 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManagerTest.kt @@ -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 + 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 capture(argumentCaptor: ArgumentCaptor): 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() 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() `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 { + `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 { + `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 { + `when`(bindServiceAsUser(any(), any(), anyInt(), any())).thenReturn(true) + } + + val manager = ControlsProviderLifecycleManager( + mockContext, + executor, + actionCallbackService, + USER, + componentName, + packageUpdateMonitorFactory, + ) + + manager.bindServiceForPanel() + executor.runAllReady() + + val flagsCaptor = argumentCaptor() + 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 { + `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 + } + } } + diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/PackageUpdateMonitorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/PackageUpdateMonitorTest.kt new file mode 100644 index 0000000000000..69547105d419a --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/PackageUpdateMonitorTest.kt @@ -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() + + 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() + + 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() + + 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" + } +}