From 4167f0c3d6e0543f04029a44673080a48c925937 Mon Sep 17 00:00:00 2001 From: Kweku Adams Date: Wed, 26 Oct 2022 16:56:30 +0000 Subject: [PATCH] Add logic to show apps running certain jobs in Task Manager. For now, we will show apps with an active FGS or jobs that the user should be aware of (ie. "user-visible jobs") in the exact same way. Users will be able to stop the ongoing FGS and user-visible jobs using the same button. This won't be used to show apps on TM, so the JobScheduler implementation is empty. We'll add the JobScheduler implementation in U. Bug: 255767350 Change-Id: Iaf482926c1f568aee26a2798ce827c6138f3f779 Test: atest SystemUITests Test: Manually start jobs and interact with the Task Manager --- .../java/android/app/JobSchedulerImpl.java | 27 +++ .../app/job/IUserVisibleJobObserver.aidl | 33 +++ .../java/android/app/job/JobScheduler.java | 30 ++- .../app/job/UserVisibleJobSummary.aidl | 19 ++ .../app/job/UserVisibleJobSummary.java | 122 ++++++++++ .../sysui/SystemUiDeviceConfigFlags.java | 6 + .../dagger/FrameworkServicesModule.java | 7 + .../systemui/qs/FgsManagerController.kt | 224 ++++++++++++++---- .../systemui/qs/FgsManagerControllerTest.java | 179 ++++++++++++++ .../systemui/qs/FakeFgsManagerController.kt | 3 + 10 files changed, 600 insertions(+), 50 deletions(-) create mode 100644 apex/jobscheduler/framework/java/android/app/job/IUserVisibleJobObserver.aidl create mode 100644 apex/jobscheduler/framework/java/android/app/job/UserVisibleJobSummary.aidl create mode 100644 apex/jobscheduler/framework/java/android/app/job/UserVisibleJobSummary.java 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()