diff --git a/apex/jobscheduler/framework/java/android/app/JobSchedulerImpl.java b/apex/jobscheduler/framework/java/android/app/JobSchedulerImpl.java index f59e7a4ae6ecd..25d258c817b6a 100644 --- a/apex/jobscheduler/framework/java/android/app/JobSchedulerImpl.java +++ b/apex/jobscheduler/framework/java/android/app/JobSchedulerImpl.java @@ -16,7 +16,10 @@ package android.app; +import android.annotation.NonNull; +import android.annotation.RequiresPermission; import android.app.job.IJobScheduler; +import android.app.job.IUserVisibleJobObserver; import android.app.job.JobInfo; import android.app.job.JobScheduler; import android.app.job.JobSnapshot; @@ -119,4 +122,28 @@ public class JobSchedulerImpl extends JobScheduler { return null; } } + + @RequiresPermission(allOf = { + android.Manifest.permission.MANAGE_ACTIVITY_TASKS, + android.Manifest.permission.INTERACT_ACROSS_USERS_FULL}) + @Override + public void registerUserVisibleJobObserver(@NonNull IUserVisibleJobObserver observer) { + // TODO(255767350): implement + } + + @RequiresPermission(allOf = { + android.Manifest.permission.MANAGE_ACTIVITY_TASKS, + android.Manifest.permission.INTERACT_ACROSS_USERS_FULL}) + @Override + public void unregisterUserVisibleJobObserver(@NonNull IUserVisibleJobObserver observer) { + // TODO(255767350): implement + } + + @RequiresPermission(allOf = { + android.Manifest.permission.MANAGE_ACTIVITY_TASKS, + android.Manifest.permission.INTERACT_ACROSS_USERS_FULL}) + @Override + public void stopUserVisibleJobsForUser(@NonNull String packageName, int userId) { + // TODO(255767350): implement + } } diff --git a/apex/jobscheduler/framework/java/android/app/job/IUserVisibleJobObserver.aidl b/apex/jobscheduler/framework/java/android/app/job/IUserVisibleJobObserver.aidl new file mode 100644 index 0000000000000..f65a47d643bc6 --- /dev/null +++ b/apex/jobscheduler/framework/java/android/app/job/IUserVisibleJobObserver.aidl @@ -0,0 +1,33 @@ +/* + * 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 android.app.job; + +import android.app.job.UserVisibleJobSummary; + +/** + * IPC protocol to know about user-visible job activity. + * + * @hide + */ +oneway interface IUserVisibleJobObserver { + /** + * Notify the client of all changes to a user-visible jobs' state. + * @param summary A token/summary that uniquely identifies and details a single running job + * @param isRunning whether the job is currently running or not + */ + void onUserVisibleJobStateChanged(in UserVisibleJobSummary summary, boolean isRunning); +} diff --git a/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java b/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java index 632ecb2c03813..acbf2c49628c5 100644 --- a/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java +++ b/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java @@ -237,4 +237,32 @@ public abstract class JobScheduler { */ @SuppressWarnings("HiddenAbstractMethod") public abstract List getAllJobSnapshots(); -} \ No newline at end of file + + /** + * @hide + */ + @RequiresPermission(allOf = { + android.Manifest.permission.MANAGE_ACTIVITY_TASKS, + android.Manifest.permission.INTERACT_ACROSS_USERS_FULL}) + @SuppressWarnings("HiddenAbstractMethod") + public abstract void registerUserVisibleJobObserver(@NonNull IUserVisibleJobObserver observer); + + /** + * @hide + */ + @RequiresPermission(allOf = { + android.Manifest.permission.MANAGE_ACTIVITY_TASKS, + android.Manifest.permission.INTERACT_ACROSS_USERS_FULL}) + @SuppressWarnings("HiddenAbstractMethod") + public abstract void unregisterUserVisibleJobObserver( + @NonNull IUserVisibleJobObserver observer); + + /** + * @hide + */ + @RequiresPermission(allOf = { + android.Manifest.permission.MANAGE_ACTIVITY_TASKS, + android.Manifest.permission.INTERACT_ACROSS_USERS_FULL}) + @SuppressWarnings("HiddenAbstractMethod") + public abstract void stopUserVisibleJobsForUser(@NonNull String packageName, int userId); +} diff --git a/apex/jobscheduler/framework/java/android/app/job/UserVisibleJobSummary.aidl b/apex/jobscheduler/framework/java/android/app/job/UserVisibleJobSummary.aidl new file mode 100644 index 0000000000000..5160b42e40dc6 --- /dev/null +++ b/apex/jobscheduler/framework/java/android/app/job/UserVisibleJobSummary.aidl @@ -0,0 +1,19 @@ +/** + * 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 android.app.job; + +parcelable UserVisibleJobSummary; diff --git a/apex/jobscheduler/framework/java/android/app/job/UserVisibleJobSummary.java b/apex/jobscheduler/framework/java/android/app/job/UserVisibleJobSummary.java new file mode 100644 index 0000000000000..afcbe7d8eb3db --- /dev/null +++ b/apex/jobscheduler/framework/java/android/app/job/UserVisibleJobSummary.java @@ -0,0 +1,122 @@ +/* + * 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 android.app.job; + +import android.annotation.NonNull; +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Summary of a scheduled job that the user is meant to be aware of. + * + * @hide + */ +public class UserVisibleJobSummary implements Parcelable { + private final int mCallingUid; + private final int mSourceUserId; + @NonNull + private final String mSourcePackageName; + private final int mJobId; + + public UserVisibleJobSummary(int callingUid, int sourceUserId, + @NonNull String sourcePackageName, int jobId) { + mCallingUid = callingUid; + mSourceUserId = sourceUserId; + mSourcePackageName = sourcePackageName; + mJobId = jobId; + } + + protected UserVisibleJobSummary(Parcel in) { + mCallingUid = in.readInt(); + mSourceUserId = in.readInt(); + mSourcePackageName = in.readString(); + mJobId = in.readInt(); + } + + public int getCallingUid() { + return mCallingUid; + } + + public int getJobId() { + return mJobId; + } + + public int getSourceUserId() { + return mSourceUserId; + } + + public String getSourcePackageName() { + return mSourcePackageName; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof UserVisibleJobSummary)) return false; + UserVisibleJobSummary that = (UserVisibleJobSummary) o; + return mCallingUid == that.mCallingUid + && mSourceUserId == that.mSourceUserId + && mSourcePackageName.equals(that.mSourcePackageName) + && mJobId == that.mJobId; + } + + @Override + public int hashCode() { + int result = 0; + result = 31 * result + mCallingUid; + result = 31 * result + mSourceUserId; + result = 31 * result + mSourcePackageName.hashCode(); + result = 31 * result + mJobId; + return result; + } + + @Override + public String toString() { + return "UserVisibleJobSummary{" + + "callingUid=" + mCallingUid + + ", sourceUserId=" + mSourceUserId + + ", sourcePackageName='" + mSourcePackageName + "'" + + ", jobId=" + mJobId + + "}"; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mCallingUid); + dest.writeInt(mSourceUserId); + dest.writeString(mSourcePackageName); + dest.writeInt(mJobId); + } + + public static final Creator CREATOR = + new Creator() { + @Override + public UserVisibleJobSummary createFromParcel(Parcel in) { + return new UserVisibleJobSummary(in); + } + + @Override + public UserVisibleJobSummary[] newArray(int size) { + return new UserVisibleJobSummary[size]; + } + }; +} diff --git a/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java b/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java index 0f64f6da63f75..24764622ac5fe 100644 --- a/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java +++ b/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java @@ -555,6 +555,12 @@ public final class SystemUiDeviceConfigFlags { public static final String TASK_MANAGER_SHOW_STOP_BUTTON_FOR_USER_ALLOWLISTED_APPS = "show_stop_button_for_user_allowlisted_apps"; + /** + * (boolean) Whether the task manager should show apps running user-visible jobs. + */ + public static final String TASK_MANAGER_SHOW_USER_VISIBLE_JOBS = + "task_manager_show_user_visible_jobs"; + /** * (boolean) Whether to show notification volume control slider separate from ring. */ diff --git a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java index 139a8b769583a..4bff28c9e3e65 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java @@ -30,6 +30,7 @@ import android.app.StatsManager; import android.app.UiModeManager; import android.app.WallpaperManager; import android.app.admin.DevicePolicyManager; +import android.app.job.JobScheduler; import android.app.role.RoleManager; import android.app.smartspace.SmartspaceManager; import android.app.trust.TrustManager; @@ -273,6 +274,12 @@ public class FrameworkServicesModule { context.getSystemService(BiometricManager.class); } + @Provides + @Singleton + static JobScheduler provideJobScheduler(Context context) { + return context.getSystemService(JobScheduler.class); + } + @Provides @Singleton static InteractionJankMonitor provideInteractionJankMonitor() { diff --git a/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt b/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt index bb2b4419a80ae..3c10778675825 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt @@ -18,6 +18,9 @@ package com.android.systemui.qs import android.app.IActivityManager import android.app.IForegroundServiceObserver +import android.app.job.IUserVisibleJobObserver +import android.app.job.JobScheduler +import android.app.job.UserVisibleJobSummary import android.content.BroadcastReceiver import android.content.Context import android.content.Intent @@ -47,6 +50,7 @@ import androidx.recyclerview.widget.RecyclerView import com.android.internal.config.sysui.SystemUiDeviceConfigFlags.TASK_MANAGER_ENABLED import com.android.internal.config.sysui.SystemUiDeviceConfigFlags.TASK_MANAGER_SHOW_FOOTER_DOT import com.android.internal.config.sysui.SystemUiDeviceConfigFlags.TASK_MANAGER_SHOW_STOP_BUTTON_FOR_USER_ALLOWLISTED_APPS +import com.android.internal.config.sysui.SystemUiDeviceConfigFlags.TASK_MANAGER_SHOW_USER_VISIBLE_JOBS import com.android.internal.jank.InteractionJankMonitor import com.android.systemui.Dumpable import com.android.systemui.R @@ -92,6 +96,8 @@ interface FgsManagerController { */ val showFooterDot: StateFlow + val includesUserVisibleJobs: Boolean + /** * Initialize this controller. This should be called once, before this controller is used for * the first time. @@ -141,19 +147,21 @@ class FgsManagerControllerImpl @Inject constructor( @Background private val backgroundExecutor: Executor, private val systemClock: SystemClock, private val activityManager: IActivityManager, + private val jobScheduler: JobScheduler, private val packageManager: PackageManager, private val userTracker: UserTracker, private val deviceConfigProxy: DeviceConfigProxy, private val dialogLaunchAnimator: DialogLaunchAnimator, private val broadcastDispatcher: BroadcastDispatcher, private val dumpManager: DumpManager -) : IForegroundServiceObserver.Stub(), Dumpable, FgsManagerController { +) : Dumpable, FgsManagerController { companion object { private const val INTERACTION_JANK_TAG = "active_background_apps" private const val DEFAULT_TASK_MANAGER_ENABLED = true private const val DEFAULT_TASK_MANAGER_SHOW_FOOTER_DOT = false private const val DEFAULT_TASK_MANAGER_SHOW_STOP_BUTTON_FOR_USER_ALLOWLISTED_APPS = true + private const val DEFAULT_TASK_MANAGER_SHOW_USER_VISIBLE_JOBS = false } override var newChangesSinceDialogWasDismissed = false @@ -167,6 +175,11 @@ class FgsManagerControllerImpl @Inject constructor( private var showStopBtnForUserAllowlistedApps = false + private var showUserVisibleJobs = DEFAULT_TASK_MANAGER_SHOW_USER_VISIBLE_JOBS + + override val includesUserVisibleJobs: Boolean + get() = showUserVisibleJobs + override val numRunningPackages: Int get() { synchronized(lock) { @@ -186,7 +199,7 @@ class FgsManagerControllerImpl @Inject constructor( private var currentProfileIds = mutableSetOf() @GuardedBy("lock") - private val runningServiceTokens = mutableMapOf() + private val runningTaskIdentifiers = mutableMapOf() @GuardedBy("lock") private var dialog: SystemUIDialog? = null @@ -210,13 +223,29 @@ class FgsManagerControllerImpl @Inject constructor( } } + private val foregroundServiceObserver = ForegroundServiceObserver() + + private val userVisibleJobObserver = UserVisibleJobObserver() + override fun init() { synchronized(lock) { if (initialized) { return } + + showUserVisibleJobs = deviceConfigProxy.getBoolean( + NAMESPACE_SYSTEMUI, + TASK_MANAGER_SHOW_USER_VISIBLE_JOBS, DEFAULT_TASK_MANAGER_SHOW_USER_VISIBLE_JOBS) + try { - activityManager.registerForegroundServiceObserver(this) + activityManager.registerForegroundServiceObserver(foregroundServiceObserver) + // Clumping FGS and user-visible jobs here and showing a single entry and button + // for them is the easiest way to get user-visible jobs showing in Task Manager. + // Ideally, we would have dedicated UI in task manager for the user-visible jobs. + // TODO(255768978): distinguish jobs from FGS and give users more control + if (showUserVisibleJobs) { + jobScheduler.registerUserVisibleJobObserver(userVisibleJobObserver) + } } catch (e: RemoteException) { e.rethrowFromSystemServer() } @@ -235,6 +264,12 @@ class FgsManagerControllerImpl @Inject constructor( showStopBtnForUserAllowlistedApps = it.getBoolean( TASK_MANAGER_SHOW_STOP_BUTTON_FOR_USER_ALLOWLISTED_APPS, showStopBtnForUserAllowlistedApps) + var wasShowingUserVisibleJobs = showUserVisibleJobs + showUserVisibleJobs = it.getBoolean( + TASK_MANAGER_SHOW_USER_VISIBLE_JOBS, showUserVisibleJobs) + if (showUserVisibleJobs != wasShowingUserVisibleJobs) { + onShowUserVisibleJobsFlagChanged() + } } _isAvailable.value = deviceConfigProxy.getBoolean( @@ -269,32 +304,6 @@ class FgsManagerControllerImpl @Inject constructor( } } - override fun onForegroundStateChanged( - token: IBinder, - packageName: String, - userId: Int, - isForeground: Boolean - ) { - synchronized(lock) { - val userPackageKey = UserPackage(userId, packageName) - if (isForeground) { - runningServiceTokens.getOrPut(userPackageKey) { StartTimeAndTokens(systemClock) } - .addToken(token) - } else { - if (runningServiceTokens[userPackageKey]?.also { - it.removeToken(token) - }?.isEmpty() == true - ) { - runningServiceTokens.remove(userPackageKey) - } - } - - updateNumberOfVisibleRunningPackagesLocked() - - updateAppItemsLocked() - } - } - @GuardedBy("lock") private val onNumberOfPackagesChangedListeners = mutableSetOf() @@ -336,7 +345,7 @@ class FgsManagerControllerImpl @Inject constructor( } private fun getNumVisiblePackagesLocked(): Int { - return runningServiceTokens.keys.count { + return runningTaskIdentifiers.keys.count { it.uiControl != UIControl.HIDE_ENTRY && currentProfileIds.contains(it.userId) } } @@ -361,7 +370,7 @@ class FgsManagerControllerImpl @Inject constructor( } private fun getNumVisibleButtonsLocked(): Int { - return runningServiceTokens.keys.count { + return runningTaskIdentifiers.keys.count { it.uiControl != UIControl.HIDE_BUTTON && currentProfileIds.contains(it.userId) } } @@ -372,7 +381,7 @@ class FgsManagerControllerImpl @Inject constructor( synchronized(lock) { if (dialog == null) { - runningServiceTokens.keys.forEach { + runningTaskIdentifiers.keys.forEach { it.updateUiControl() } @@ -434,17 +443,17 @@ class FgsManagerControllerImpl @Inject constructor( return } - val addedPackages = runningServiceTokens.keys.filter { + val addedPackages = runningTaskIdentifiers.keys.filter { currentProfileIds.contains(it.userId) && it.uiControl != UIControl.HIDE_ENTRY && runningApps[it]?.stopped != true } - val removedPackages = runningApps.keys.filter { !runningServiceTokens.containsKey(it) } + val removedPackages = runningApps.keys.filter { !runningTaskIdentifiers.containsKey(it) } addedPackages.forEach { val ai = packageManager.getApplicationInfoAsUser(it.packageName, 0, it.userId) runningApps[it] = RunningApp( it.userId, it.packageName, - runningServiceTokens[it]!!.startTime, it.uiControl, + runningTaskIdentifiers[it]!!.startTime, it.uiControl, packageManager.getApplicationLabel(ai), packageManager.getUserBadgedIcon( packageManager.getApplicationIcon(ai), UserHandle.of(it.userId) @@ -471,7 +480,41 @@ class FgsManagerControllerImpl @Inject constructor( private fun stopPackage(userId: Int, packageName: String, timeStarted: Long) { logEvent(stopped = true, packageName, userId, timeStarted) - activityManager.stopAppForUser(packageName, userId) + val userPackageKey = UserPackage(userId, packageName) + if (showUserVisibleJobs && + runningTaskIdentifiers[userPackageKey]?.hasRunningJobs() == true) { + // TODO(255768978): allow fine-grained job control + jobScheduler.stopUserVisibleJobsForUser(packageName, userId) + } + if (runningTaskIdentifiers[userPackageKey]?.hasFgs() == true) { + activityManager.stopAppForUser(packageName, userId) + } + } + + private fun onShowUserVisibleJobsFlagChanged() { + if (showUserVisibleJobs) { + jobScheduler.registerUserVisibleJobObserver(userVisibleJobObserver) + } else { + jobScheduler.unregisterUserVisibleJobObserver(userVisibleJobObserver) + + synchronized(lock) { + for ((userPackage, startTimeAndIdentifiers) in runningTaskIdentifiers) { + if (startTimeAndIdentifiers.hasFgs()) { + // The app still has FGS running, so all we need to do is remove + // the job summaries + startTimeAndIdentifiers.clearJobSummaries() + } else { + // The app only has user-visible jobs running, so remove it from + // the map altogether + runningTaskIdentifiers.remove(userPackage) + } + } + + updateNumberOfVisibleRunningPackagesLocked() + + updateAppItemsLocked() + } + } } private fun logEvent(stopped: Boolean, packageName: String, userId: Int, timeStarted: Long) { @@ -564,6 +607,62 @@ class FgsManagerControllerImpl @Inject constructor( } } + private inner class ForegroundServiceObserver : IForegroundServiceObserver.Stub() { + override fun onForegroundStateChanged( + token: IBinder, + packageName: String, + userId: Int, + isForeground: Boolean + ) { + synchronized(lock) { + val userPackageKey = UserPackage(userId, packageName) + if (isForeground) { + runningTaskIdentifiers + .getOrPut(userPackageKey) { StartTimeAndIdentifiers(systemClock) } + .addFgsToken(token) + } else { + if (runningTaskIdentifiers[userPackageKey]?.also { + it.removeFgsToken(token) + }?.isEmpty() == true + ) { + runningTaskIdentifiers.remove(userPackageKey) + } + } + + updateNumberOfVisibleRunningPackagesLocked() + + updateAppItemsLocked() + } + } + } + + private inner class UserVisibleJobObserver : IUserVisibleJobObserver.Stub() { + override fun onUserVisibleJobStateChanged( + summary: UserVisibleJobSummary, + isRunning: Boolean + ) { + synchronized(lock) { + val userPackageKey = UserPackage(summary.sourceUserId, summary.sourcePackageName) + if (isRunning) { + runningTaskIdentifiers + .getOrPut(userPackageKey) { StartTimeAndIdentifiers(systemClock) } + .addJobSummary(summary) + } else { + if (runningTaskIdentifiers[userPackageKey]?.also { + it.removeJobSummary(summary) + }?.isEmpty() == true + ) { + runningTaskIdentifiers.remove(userPackageKey) + } + } + + updateNumberOfVisibleRunningPackagesLocked() + + updateAppItemsLocked() + } + } + } + private inner class UserPackage( val userId: Int, val packageName: String @@ -630,37 +729,64 @@ class FgsManagerControllerImpl @Inject constructor( } } - private data class StartTimeAndTokens( + private data class StartTimeAndIdentifiers( val systemClock: SystemClock ) { val startTime = systemClock.elapsedRealtime() - val tokens = mutableSetOf() + val fgsTokens = mutableSetOf() + val jobSummaries = mutableSetOf() - fun addToken(token: IBinder) { - tokens.add(token) + fun addJobSummary(summary: UserVisibleJobSummary) { + jobSummaries.add(summary) } - fun removeToken(token: IBinder) { - tokens.remove(token) + fun clearJobSummaries() { + jobSummaries.clear() + } + + fun removeJobSummary(summary: UserVisibleJobSummary) { + jobSummaries.remove(summary) + } + + fun addFgsToken(token: IBinder) { + fgsTokens.add(token) + } + + fun removeFgsToken(token: IBinder) { + fgsTokens.remove(token) + } + + fun hasFgs(): Boolean { + return !fgsTokens.isEmpty() + } + + fun hasRunningJobs(): Boolean { + return !jobSummaries.isEmpty() } fun isEmpty(): Boolean { - return tokens.isEmpty() + return fgsTokens.isEmpty() && jobSummaries.isEmpty() } fun dump(pw: PrintWriter) { - pw.println("StartTimeAndTokens: [") + pw.println("StartTimeAndIdentifiers: [") pw.indentIfPossible { pw.println( "startTime=$startTime (time running =" + " ${systemClock.elapsedRealtime() - startTime}ms)" ) - pw.println("tokens: [") + pw.println("fgs tokens: [") pw.indentIfPossible { - for (token in tokens) { + for (token in fgsTokens) { pw.println("$token") } } + pw.println("job summaries: [") + pw.indentIfPossible { + for (summary in jobSummaries) { + pw.println("$summary") + } + } pw.println("]") } pw.println("]") @@ -724,13 +850,13 @@ class FgsManagerControllerImpl @Inject constructor( synchronized(lock) { pw.println("current user profiles = $currentProfileIds") pw.println("newChangesSinceDialogWasShown=$newChangesSinceDialogWasDismissed") - pw.println("Running service tokens: [") + pw.println("Running task identifiers: [") pw.indentIfPossible { - runningServiceTokens.forEach { (userPackage, startTimeAndTokens) -> + runningTaskIdentifiers.forEach { (userPackage, startTimeAndIdentifiers) -> pw.println("{") pw.indentIfPossible { userPackage.dump(pw) - startTimeAndTokens.dump(pw) + startTimeAndIdentifiers.dump(pw) } 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 index 7bae115d2edd9..17eb6e20172e3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/FgsManagerControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/FgsManagerControllerTest.java @@ -18,6 +18,7 @@ package com.android.systemui.qs; import static android.os.PowerExemptionManager.REASON_ALLOWLISTED_PACKAGE; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; @@ -29,6 +30,9 @@ import static org.mockito.Mockito.verify; import android.app.IActivityManager; import android.app.IForegroundServiceObserver; +import android.app.job.IUserVisibleJobObserver; +import android.app.job.JobScheduler; +import android.app.job.UserVisibleJobSummary; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; @@ -59,6 +63,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.ArgumentMatchers; +import org.mockito.InOrder; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; @@ -79,6 +84,8 @@ public class FgsManagerControllerTest extends SysuiTestCase { @Mock IActivityManager mIActivityManager; @Mock + JobScheduler mJobScheduler; + @Mock PackageManager mPackageManager; @Mock UserTracker mUserTracker; @@ -92,8 +99,10 @@ public class FgsManagerControllerTest extends SysuiTestCase { private FgsManagerController mFmc; private IForegroundServiceObserver mIForegroundServiceObserver; + private IUserVisibleJobObserver mIUserVisibleJobObserver; private UserTracker.Callback mUserTrackerCallback; private BroadcastReceiver mShowFgsManagerReceiver; + private InOrder mJobSchedulerInOrder; private List mUserProfiles; @@ -111,6 +120,8 @@ public class FgsManagerControllerTest extends SysuiTestCase { mUserProfiles = new ArrayList<>(); Mockito.doReturn(mUserProfiles).when(mUserTracker).getUserProfiles(); + mJobSchedulerInOrder = Mockito.inOrder(mJobScheduler); + mFmc = createFgsManagerController(); } @@ -131,6 +142,52 @@ public class FgsManagerControllerTest extends SysuiTestCase { Assert.assertEquals(0, mFmc.getNumRunningPackages()); } + @Test + public void testNumPackages_jobs() throws RemoteException { + setUserProfiles(0); + setShowUserVisibleJobs(true); + + UserVisibleJobSummary j1 = new UserVisibleJobSummary(0, 0, "pkg1", 0); + UserVisibleJobSummary j2 = new UserVisibleJobSummary(1, 0, "pkg2", 1); + Assert.assertEquals(0, mFmc.getNumRunningPackages()); + mIUserVisibleJobObserver.onUserVisibleJobStateChanged(j1, true); + Assert.assertEquals(1, mFmc.getNumRunningPackages()); + mIUserVisibleJobObserver.onUserVisibleJobStateChanged(j2, true); + Assert.assertEquals(2, mFmc.getNumRunningPackages()); + mIUserVisibleJobObserver.onUserVisibleJobStateChanged(j1, false); + Assert.assertEquals(1, mFmc.getNumRunningPackages()); + mIUserVisibleJobObserver.onUserVisibleJobStateChanged(j2, false); + Assert.assertEquals(0, mFmc.getNumRunningPackages()); + } + + @Test + public void testNumPackages_FgsAndJobs() throws RemoteException { + setUserProfiles(0); + setShowUserVisibleJobs(true); + + Binder b1 = new Binder(); + Binder b2 = new Binder(); + UserVisibleJobSummary j1 = new UserVisibleJobSummary(0, 0, "pkg1", 0); + UserVisibleJobSummary j3 = new UserVisibleJobSummary(1, 0, "pkg3", 1); + 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()); + mIUserVisibleJobObserver.onUserVisibleJobStateChanged(j1, true); + Assert.assertEquals(2, mFmc.getNumRunningPackages()); + mIUserVisibleJobObserver.onUserVisibleJobStateChanged(j3, true); + Assert.assertEquals(3, mFmc.getNumRunningPackages()); + mIForegroundServiceObserver.onForegroundStateChanged(b2, "pkg2", 0, false); + Assert.assertEquals(2, mFmc.getNumRunningPackages()); + mIUserVisibleJobObserver.onUserVisibleJobStateChanged(j3, false); + Assert.assertEquals(1, mFmc.getNumRunningPackages()); + mIForegroundServiceObserver.onForegroundStateChanged(b1, "pkg1", 0, false); + Assert.assertEquals(1, mFmc.getNumRunningPackages()); + mIUserVisibleJobObserver.onUserVisibleJobStateChanged(j1, false); + Assert.assertEquals(0, mFmc.getNumRunningPackages()); + } + @Test public void testNumPackagesDoesNotChangeWhenSecondFgsIsStarted() throws RemoteException { setUserProfiles(0); @@ -243,6 +300,91 @@ public class FgsManagerControllerTest extends SysuiTestCase { Assert.assertEquals(0, mFmc.visibleButtonsCount()); } + @Test + public void testShowUserVisibleJobsOnCreation() { + // Test when the default is on. + mDeviceConfigProxyFake.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, + SystemUiDeviceConfigFlags.TASK_MANAGER_SHOW_USER_VISIBLE_JOBS, + "true", false); + FgsManagerController fmc = new FgsManagerControllerImpl( + mContext, + mMainExecutor, + mBackgroundExecutor, + mSystemClock, + mIActivityManager, + mJobScheduler, + mPackageManager, + mUserTracker, + mDeviceConfigProxyFake, + mDialogLaunchAnimator, + mBroadcastDispatcher, + mDumpManager + ); + fmc.init(); + Assert.assertTrue(fmc.getIncludesUserVisibleJobs()); + ArgumentCaptor iUserVisibleJobObserverArgumentCaptor = + ArgumentCaptor.forClass(IUserVisibleJobObserver.class); + mJobSchedulerInOrder.verify(mJobScheduler) + .registerUserVisibleJobObserver(iUserVisibleJobObserverArgumentCaptor.capture()); + Assert.assertNotNull(iUserVisibleJobObserverArgumentCaptor.getValue()); + + // Test when the default is off. + mDeviceConfigProxyFake.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, + SystemUiDeviceConfigFlags.TASK_MANAGER_SHOW_USER_VISIBLE_JOBS, + "false", false); + fmc = new FgsManagerControllerImpl( + mContext, + mMainExecutor, + mBackgroundExecutor, + mSystemClock, + mIActivityManager, + mJobScheduler, + mPackageManager, + mUserTracker, + mDeviceConfigProxyFake, + mDialogLaunchAnimator, + mBroadcastDispatcher, + mDumpManager + ); + fmc.init(); + Assert.assertFalse(fmc.getIncludesUserVisibleJobs()); + mJobSchedulerInOrder.verify(mJobScheduler, never()).registerUserVisibleJobObserver(any()); + } + + @Test + public void testShowUserVisibleJobsToggling() throws Exception { + setUserProfiles(0); + setShowUserVisibleJobs(true); + + // pkg1 has only job + // pkg2 has both job and fgs + // pkg3 has only fgs + UserVisibleJobSummary j1 = new UserVisibleJobSummary(0, 0, "pkg1", 0); + UserVisibleJobSummary j2 = new UserVisibleJobSummary(1, 0, "pkg2", 1); + Binder b2 = new Binder(); + Binder b3 = new Binder(); + + Assert.assertEquals(0, mFmc.getNumRunningPackages()); + mIForegroundServiceObserver.onForegroundStateChanged(b2, "pkg2", 0, true); + mIForegroundServiceObserver.onForegroundStateChanged(b3, "pkg3", 0, true); + Assert.assertEquals(2, mFmc.getNumRunningPackages()); + mIUserVisibleJobObserver.onUserVisibleJobStateChanged(j1, true); + mIUserVisibleJobObserver.onUserVisibleJobStateChanged(j2, true); + Assert.assertEquals(3, mFmc.getNumRunningPackages()); + + // Turn off the flag, confirm the number of packages is updated properly. + setShowUserVisibleJobs(false); + // Only pkg1 should be removed since the other two have fgs + Assert.assertEquals(2, mFmc.getNumRunningPackages()); + + setShowUserVisibleJobs(true); + + Assert.assertEquals(2, mFmc.getNumRunningPackages()); + mIUserVisibleJobObserver.onUserVisibleJobStateChanged(j1, true); + mIUserVisibleJobObserver.onUserVisibleJobStateChanged(j2, true); + Assert.assertEquals(3, mFmc.getNumRunningPackages()); + } + private void setShowStopButtonForUserAllowlistedApps(boolean enable) { mDeviceConfigProxyFake.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, SystemUiDeviceConfigFlags.TASK_MANAGER_SHOW_STOP_BUTTON_FOR_USER_ALLOWLISTED_APPS, @@ -251,6 +393,33 @@ public class FgsManagerControllerTest extends SysuiTestCase { mBackgroundExecutor.runAllReady(); } + private void setShowUserVisibleJobs(boolean enable) { + if (mFmc.getIncludesUserVisibleJobs() == enable) { + // No change. + return; + } + + mDeviceConfigProxyFake.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, + SystemUiDeviceConfigFlags.TASK_MANAGER_SHOW_USER_VISIBLE_JOBS, + enable ? "true" : "false", false); + mBackgroundExecutor.advanceClockToLast(); + mBackgroundExecutor.runAllReady(); + + ArgumentCaptor iUserVisibleJobObserverArgumentCaptor = + ArgumentCaptor.forClass(IUserVisibleJobObserver.class); + if (enable) { + mJobSchedulerInOrder.verify(mJobScheduler).registerUserVisibleJobObserver( + iUserVisibleJobObserverArgumentCaptor.capture() + ); + mIUserVisibleJobObserver = iUserVisibleJobObserverArgumentCaptor.getValue(); + } else { + mJobSchedulerInOrder.verify(mJobScheduler).unregisterUserVisibleJobObserver( + eq(mIUserVisibleJobObserver) + ); + mIUserVisibleJobObserver = null; + } + } + private void setBackgroundRestrictionExemptionReason(String pkgName, int uid, int reason) throws Exception { Mockito.doReturn(uid) @@ -275,6 +444,7 @@ public class FgsManagerControllerTest extends SysuiTestCase { mBackgroundExecutor, mSystemClock, mIActivityManager, + mJobScheduler, mPackageManager, mUserTracker, mDeviceConfigProxyFake, @@ -304,6 +474,15 @@ public class FgsManagerControllerTest extends SysuiTestCase { mUserTrackerCallback = userTrackerCallbackArgumentCaptor.getValue(); mShowFgsManagerReceiver = showFgsManagerReceiverArgumentCaptor.getValue(); + if (result.getIncludesUserVisibleJobs()) { + ArgumentCaptor iUserVisibleJobObserverArgumentCaptor = + ArgumentCaptor.forClass(IUserVisibleJobObserver.class); + mJobSchedulerInOrder.verify(mJobScheduler).registerUserVisibleJobObserver( + iUserVisibleJobObserverArgumentCaptor.capture() + ); + mIUserVisibleJobObserver = iUserVisibleJobObserverArgumentCaptor.getValue(); + } + return result; } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeFgsManagerController.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeFgsManagerController.kt index c33ce5d9484da..b31f119b7a7cd 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeFgsManagerController.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeFgsManagerController.kt @@ -43,6 +43,9 @@ class FakeFgsManagerController( override val showFooterDot: MutableStateFlow = MutableStateFlow(showFooterDot) + override var includesUserVisibleJobs = false + private set + private val numRunningPackagesListeners = LinkedHashSet() private val dialogDismissedListeners = LinkedHashSet()