diff options
| -rw-r--r-- | packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt | 88 | ||||
| -rw-r--r-- | packages/SystemUI/tests/src/com/android/systemui/qs/FgsManagerControllerTest.java | 285 |
2 files changed, 349 insertions, 24 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt b/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt index 3e8cdf3a3592..e5d7b4003297 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt @@ -23,10 +23,12 @@ import android.content.Context import android.content.Intent import android.content.IntentFilter import android.content.pm.PackageManager +import android.content.pm.UserInfo import android.graphics.drawable.Drawable import android.os.IBinder import android.os.PowerExemptionManager import android.os.RemoteException +import android.os.UserHandle import android.provider.DeviceConfig.NAMESPACE_SYSTEMUI import android.text.format.DateUtils import android.util.ArrayMap @@ -51,6 +53,7 @@ import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dump.DumpManager import com.android.systemui.shared.system.SysUiStatsLog +import com.android.systemui.settings.UserTracker import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.util.DeviceConfigProxy import com.android.systemui.util.indentIfPossible @@ -69,6 +72,7 @@ class FgsManagerController @Inject constructor( private val systemClock: SystemClock, private val activityManager: IActivityManager, private val packageManager: PackageManager, + private val userTracker: UserTracker, private val deviceConfigProxy: DeviceConfigProxy, private val dialogLaunchAnimator: DialogLaunchAnimator, private val broadcastDispatcher: BroadcastDispatcher, @@ -82,7 +86,8 @@ class FgsManagerController @Inject constructor( var changesSinceDialog = false private set - private var isAvailable = false + var isAvailable = false + private set private val lock = Any() @@ -90,6 +95,12 @@ class FgsManagerController @Inject constructor( var initialized = false @GuardedBy("lock") + private var lastNumberOfVisiblePackages = 0 + + @GuardedBy("lock") + private var currentProfileIds = mutableSetOf<Int>() + + @GuardedBy("lock") private val runningServiceTokens = mutableMapOf<UserPackage, StartTimeAndTokens>() @GuardedBy("lock") @@ -101,6 +112,19 @@ class FgsManagerController @Inject constructor( @GuardedBy("lock") private var runningApps: ArrayMap<UserPackage, RunningApp> = ArrayMap() + private val userTrackerCallback = object : UserTracker.Callback { + override fun onUserChanged(newUser: Int, userContext: Context) {} + + override fun onProfilesChanged(profiles: List<UserInfo>) { + synchronized(lock) { + currentProfileIds.clear() + currentProfileIds.addAll(profiles.map { it.id }) + lastNumberOfVisiblePackages = 0 + updateNumberOfVisibleRunningPackagesLocked() + } + } + } + interface OnNumberOfPackagesChangedListener { fun onNumberOfPackagesChanged(numPackages: Int) } @@ -120,6 +144,10 @@ class FgsManagerController @Inject constructor( e.rethrowFromSystemServer() } + userTracker.addCallback(userTrackerCallback, backgroundExecutor) + + currentProfileIds.addAll(userTracker.userProfiles.map { it.id }) + deviceConfigProxy.addOnPropertiesChangedListener(NAMESPACE_SYSTEMUI, backgroundExecutor) { isAvailable = it.getBoolean(TASK_MANAGER_ENABLED, isAvailable) @@ -153,10 +181,9 @@ class FgsManagerController @Inject constructor( isForeground: Boolean ) { synchronized(lock) { - val numPackagesBefore = getNumRunningPackagesLocked() val userPackageKey = UserPackage(userId, packageName) if (isForeground) { - runningServiceTokens.getOrPut(userPackageKey, { StartTimeAndTokens(systemClock) }) + runningServiceTokens.getOrPut(userPackageKey) { StartTimeAndTokens(systemClock) } .addToken(token) } else { if (runningServiceTokens[userPackageKey]?.also { @@ -165,14 +192,7 @@ class FgsManagerController @Inject constructor( } } - val numPackagesAfter = getNumRunningPackagesLocked() - - if (numPackagesAfter != numPackagesBefore) { - changesSinceDialog = true - onNumberOfPackagesChangedListeners.forEach { - backgroundExecutor.execute { it.onNumberOfPackagesChanged(numPackagesAfter) } - } - } + updateNumberOfVisibleRunningPackagesLocked() updateAppItemsLocked() } @@ -209,18 +229,30 @@ class FgsManagerController @Inject constructor( } } - fun isAvailable(): Boolean { - return isAvailable - } - fun getNumRunningPackages(): Int { synchronized(lock) { - return getNumRunningPackagesLocked() + return getNumVisiblePackagesLocked() } } - private fun getNumRunningPackagesLocked() = - runningServiceTokens.keys.count { it.uiControl != UIControl.HIDE_ENTRY } + private fun getNumVisiblePackagesLocked(): Int { + return runningServiceTokens.keys.count { + it.uiControl != UIControl.HIDE_ENTRY && currentProfileIds.contains(it.userId) + } + } + + private fun updateNumberOfVisibleRunningPackagesLocked() { + val num = getNumVisiblePackagesLocked() + if (num != lastNumberOfVisiblePackages) { + lastNumberOfVisiblePackages = num + changesSinceDialog = true + onNumberOfPackagesChangedListeners.forEach { + backgroundExecutor.execute { + it.onNumberOfPackagesChanged(num) + } + } + } + } fun shouldUpdateFooterVisibility() = dialog == null @@ -289,7 +321,9 @@ class FgsManagerController @Inject constructor( val ai = packageManager.getApplicationInfoAsUser(it.packageName, 0, it.userId) runningApps[it] = RunningApp(it.userId, it.packageName, runningServiceTokens[it]!!.startTime, it.uiControl, - ai.loadLabel(packageManager), ai.loadIcon(packageManager)) + packageManager.getApplicationLabel(ai), + packageManager.getUserBadgedIcon( + packageManager.getApplicationIcon(ai), UserHandle.of(it.userId))) logEvent(stopped = false, it.packageName, it.userId, runningApps[it]!!.timeStarted) } @@ -404,6 +438,7 @@ class FgsManagerController @Inject constructor( val packageName: String ) { val uid by lazy { packageManager.getPackageUidAsUser(packageName, userId) } + var backgroundRestrictionExemptionReason = PowerExemptionManager.REASON_DENIED private var uiControlInitialized = false var uiControl: UIControl = UIControl.NORMAL @@ -416,7 +451,9 @@ class FgsManagerController @Inject constructor( private set fun updateUiControl() { - uiControl = when (activityManager.getBackgroundRestrictionExemptionReason(uid)) { + backgroundRestrictionExemptionReason = + activityManager.getBackgroundRestrictionExemptionReason(uid) + uiControl = when (backgroundRestrictionExemptionReason) { PowerExemptionManager.REASON_SYSTEM_UID, PowerExemptionManager.REASON_DEVICE_DEMO_MODE -> UIControl.HIDE_ENTRY @@ -448,7 +485,7 @@ class FgsManagerController @Inject constructor( pw.indentIfPossible { pw.println("userId=$userId") pw.println("packageName=$packageName") - pw.println("uiControl=$uiControl") + pw.println("uiControl=$uiControl (reason=$backgroundRestrictionExemptionReason)") } pw.println("]") } @@ -525,7 +562,7 @@ class FgsManagerController @Inject constructor( pw.println("userId=$userId") pw.println("packageName=$packageName") pw.println("timeStarted=$timeStarted (time since start =" + - " ${systemClock.elapsedRealtime() - timeStarted}ms)\"") + " ${systemClock.elapsedRealtime() - timeStarted}ms)") pw.println("uiControl=$uiControl") pw.println("appLabel=$appLabel") pw.println("icon=$icon") @@ -542,6 +579,7 @@ class FgsManagerController @Inject constructor( override fun dump(printwriter: PrintWriter, args: Array<out String>) { val pw = IndentingPrintWriter(printwriter) synchronized(lock) { + pw.println("current user profiles = $currentProfileIds") pw.println("changesSinceDialog=$changesSinceDialog") pw.println("Running service tokens: [") pw.indentIfPossible { @@ -560,8 +598,10 @@ class FgsManagerController @Inject constructor( pw.indentIfPossible { runningApps.forEach { (userPackage, runningApp) -> pw.println("{") - userPackage.dump(pw) - runningApp.dump(pw, systemClock) + pw.indentIfPossible { + userPackage.dump(pw) + runningApp.dump(pw, systemClock) + } pw.println("}") } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/FgsManagerControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/FgsManagerControllerTest.java new file mode 100644 index 000000000000..2927669020c8 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/FgsManagerControllerTest.java @@ -0,0 +1,285 @@ +/* + * Copyright (C) 2022 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.qs; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.app.IActivityManager; +import android.app.IForegroundServiceObserver; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.UserInfo; +import android.os.Binder; +import android.os.RemoteException; +import android.provider.DeviceConfig; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; + +import androidx.test.filters.SmallTest; + +import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; +import com.android.systemui.SysuiTestCase; +import com.android.systemui.animation.DialogLaunchAnimator; +import com.android.systemui.broadcast.BroadcastDispatcher; +import com.android.systemui.dump.DumpManager; +import com.android.systemui.settings.UserTracker; +import com.android.systemui.util.DeviceConfigProxyFake; +import com.android.systemui.util.concurrency.FakeExecutor; +import com.android.systemui.util.time.FakeSystemClock; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatchers; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +import java.util.ArrayList; +import java.util.List; + +@RunWith(AndroidTestingRunner.class) +@TestableLooper.RunWithLooper +@SmallTest +public class FgsManagerControllerTest extends SysuiTestCase { + + FakeSystemClock mSystemClock; + FakeExecutor mMainExecutor; + FakeExecutor mBackgroundExecutor; + DeviceConfigProxyFake mDeviceConfigProxyFake; + + @Mock + IActivityManager mIActivityManager; + @Mock + PackageManager mPackageManager; + @Mock + UserTracker mUserTracker; + @Mock + DialogLaunchAnimator mDialogLaunchAnimator; + @Mock + BroadcastDispatcher mBroadcastDispatcher; + @Mock + DumpManager mDumpManager; + + private FgsManagerController mFmc; + + private IForegroundServiceObserver mIForegroundServiceObserver; + private UserTracker.Callback mUserTrackerCallback; + private BroadcastReceiver mShowFgsManagerReceiver; + + private List<UserInfo> mUserProfiles; + + @Before + public void setUp() throws RemoteException { + MockitoAnnotations.initMocks(this); + + mDeviceConfigProxyFake = new DeviceConfigProxyFake(); + mDeviceConfigProxyFake.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, + SystemUiDeviceConfigFlags.TASK_MANAGER_ENABLED, "true", false); + mSystemClock = new FakeSystemClock(); + mMainExecutor = new FakeExecutor(mSystemClock); + mBackgroundExecutor = new FakeExecutor(mSystemClock); + + mUserProfiles = new ArrayList<>(); + Mockito.doReturn(mUserProfiles).when(mUserTracker).getUserProfiles(); + + mFmc = createFgsManagerController(); + } + + @Test + public void testNumPackages() throws RemoteException { + setUserProfiles(0); + + Binder b1 = new Binder(); + Binder b2 = new Binder(); + Assert.assertEquals(0, mFmc.getNumRunningPackages()); + mIForegroundServiceObserver.onForegroundStateChanged(b1, "pkg1", 0, true); + Assert.assertEquals(1, mFmc.getNumRunningPackages()); + mIForegroundServiceObserver.onForegroundStateChanged(b2, "pkg2", 0, true); + Assert.assertEquals(2, mFmc.getNumRunningPackages()); + mIForegroundServiceObserver.onForegroundStateChanged(b1, "pkg1", 0, false); + Assert.assertEquals(1, mFmc.getNumRunningPackages()); + mIForegroundServiceObserver.onForegroundStateChanged(b2, "pkg2", 0, false); + Assert.assertEquals(0, mFmc.getNumRunningPackages()); + } + + @Test + public void testNumPackagesDoesNotChangeWhenSecondFgsIsStarted() throws RemoteException { + setUserProfiles(0); + + // Different tokens == different services + Binder b1 = new Binder(); + Binder b2 = new Binder(); + Assert.assertEquals(0, mFmc.getNumRunningPackages()); + mIForegroundServiceObserver.onForegroundStateChanged(b1, "pkg1", 0, true); + Assert.assertEquals(1, mFmc.getNumRunningPackages()); + mIForegroundServiceObserver.onForegroundStateChanged(b2, "pkg1", 0, true); + Assert.assertEquals(1, mFmc.getNumRunningPackages()); + mIForegroundServiceObserver.onForegroundStateChanged(b1, "pkg1", 0, false); + Assert.assertEquals(1, mFmc.getNumRunningPackages()); + mIForegroundServiceObserver.onForegroundStateChanged(b2, "pkg1", 0, false); + Assert.assertEquals(0, mFmc.getNumRunningPackages()); + } + + @Test + public void testNumPackagesListener() throws RemoteException { + setUserProfiles(0); + + FgsManagerController.OnNumberOfPackagesChangedListener onNumberOfPackagesChangedListener = + Mockito.mock(FgsManagerController.OnNumberOfPackagesChangedListener.class); + + mFmc.addOnNumberOfPackagesChangedListener(onNumberOfPackagesChangedListener); + + Binder b1 = new Binder(); + Binder b2 = new Binder(); + + verify(onNumberOfPackagesChangedListener, never()).onNumberOfPackagesChanged(anyInt()); + + mIForegroundServiceObserver.onForegroundStateChanged(b1, "pkg1", 0, true); + mBackgroundExecutor.advanceClockToLast(); + mBackgroundExecutor.runAllReady(); + verify(onNumberOfPackagesChangedListener).onNumberOfPackagesChanged(1); + + mIForegroundServiceObserver.onForegroundStateChanged(b2, "pkg2", 0, true); + mBackgroundExecutor.advanceClockToLast(); + mBackgroundExecutor.runAllReady(); + verify(onNumberOfPackagesChangedListener).onNumberOfPackagesChanged(2); + + mIForegroundServiceObserver.onForegroundStateChanged(b1, "pkg1", 0, false); + mBackgroundExecutor.advanceClockToLast(); + mBackgroundExecutor.runAllReady(); + verify(onNumberOfPackagesChangedListener, times(2)).onNumberOfPackagesChanged(1); + + mIForegroundServiceObserver.onForegroundStateChanged(b2, "pkg2", 0, false); + mBackgroundExecutor.advanceClockToLast(); + mBackgroundExecutor.runAllReady(); + verify(onNumberOfPackagesChangedListener).onNumberOfPackagesChanged(0); + } + + @Test + public void testChangesSinceLastDialog() throws RemoteException { + setUserProfiles(0); + + Assert.assertFalse(mFmc.getChangesSinceDialog()); + mIForegroundServiceObserver.onForegroundStateChanged(new Binder(), "pkg", 0, true); + Assert.assertTrue(mFmc.getChangesSinceDialog()); + } + + @Test + public void testProfilePackagesCounted() throws RemoteException { + setUserProfiles(0, 10); + + mIForegroundServiceObserver.onForegroundStateChanged(new Binder(), "pkg1", 0, true); + mIForegroundServiceObserver.onForegroundStateChanged(new Binder(), "pkg2", 10, true); + Assert.assertEquals(2, mFmc.getNumRunningPackages()); + } + + @Test + public void testSecondaryUserPackagesAreNotCounted() throws RemoteException { + setUserProfiles(0); + + mIForegroundServiceObserver.onForegroundStateChanged(new Binder(), "pkg1", 0, true); + mIForegroundServiceObserver.onForegroundStateChanged(new Binder(), "pkg2", 10, true); + Assert.assertEquals(1, mFmc.getNumRunningPackages()); + } + + @Test + public void testSecondaryUserPackagesAreCountedWhenUserSwitch() throws RemoteException { + setUserProfiles(0); + + mIForegroundServiceObserver.onForegroundStateChanged(new Binder(), "pkg1", 0, true); + mIForegroundServiceObserver.onForegroundStateChanged(new Binder(), "pkg2", 10, true); + mIForegroundServiceObserver.onForegroundStateChanged(new Binder(), "pkg3", 10, true); + + Assert.assertEquals(1, mFmc.getNumRunningPackages()); + + setUserProfiles(10); + Assert.assertEquals(2, mFmc.getNumRunningPackages()); + } + + + + FgsManagerController createFgsManagerController() throws RemoteException { + ArgumentCaptor<IForegroundServiceObserver> iForegroundServiceObserverArgumentCaptor = + ArgumentCaptor.forClass(IForegroundServiceObserver.class); + ArgumentCaptor<UserTracker.Callback> userTrackerCallbackArgumentCaptor = + ArgumentCaptor.forClass(UserTracker.Callback.class); + ArgumentCaptor<BroadcastReceiver> showFgsManagerReceiverArgumentCaptor = + ArgumentCaptor.forClass(BroadcastReceiver.class); + + FgsManagerController result = new FgsManagerController( + mContext, + mMainExecutor, + mBackgroundExecutor, + mSystemClock, + mIActivityManager, + mPackageManager, + mUserTracker, + mDeviceConfigProxyFake, + mDialogLaunchAnimator, + mBroadcastDispatcher, + mDumpManager + ); + result.init(); + + verify(mIActivityManager).registerForegroundServiceObserver( + iForegroundServiceObserverArgumentCaptor.capture() + ); + verify(mUserTracker).addCallback( + userTrackerCallbackArgumentCaptor.capture(), + ArgumentMatchers.eq(mBackgroundExecutor) + ); + verify(mBroadcastDispatcher).registerReceiver( + showFgsManagerReceiverArgumentCaptor.capture(), + argThat(fltr -> fltr.matchAction(Intent.ACTION_SHOW_FOREGROUND_SERVICE_MANAGER)), + eq(mMainExecutor), + isNull(), + eq(Context.RECEIVER_NOT_EXPORTED), + isNull() + ); + + mIForegroundServiceObserver = iForegroundServiceObserverArgumentCaptor.getValue(); + mUserTrackerCallback = userTrackerCallbackArgumentCaptor.getValue(); + mShowFgsManagerReceiver = showFgsManagerReceiverArgumentCaptor.getValue(); + + return result; + } + + private void setUserProfiles(int current, int... profileUserIds) { + mUserProfiles.clear(); + mUserProfiles.add(new UserInfo(current, "current:" + current, 0)); + for (int id : profileUserIds) { + mUserProfiles.add(new UserInfo(id, "profile:" + id, 0)); + } + + if (mUserTrackerCallback != null) { + mUserTrackerCallback.onUserChanged(current, mock(Context.class)); + mUserTrackerCallback.onProfilesChanged(mUserProfiles); + } + } +} |