diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 75ae52c6e6f4b..abc0999318d8d 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -2367,6 +2367,8 @@ Active apps Stop + + Stopped Copy diff --git a/packages/SystemUI/src/com/android/systemui/fgsmanager/FgsManagerDialog.kt b/packages/SystemUI/src/com/android/systemui/fgsmanager/FgsManagerDialog.kt deleted file mode 100644 index 42f3512129a39..0000000000000 --- a/packages/SystemUI/src/com/android/systemui/fgsmanager/FgsManagerDialog.kt +++ /dev/null @@ -1,141 +0,0 @@ -/* - * Copyright (C) 2021 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.fgsmanager - -import android.content.Context -import android.os.Bundle -import android.text.format.DateUtils -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.Button -import android.widget.ImageView -import android.widget.TextView -import androidx.annotation.GuardedBy -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import com.android.systemui.R -import com.android.systemui.dagger.qualifiers.Background -import com.android.systemui.fgsmanager.FgsManagerDialogController.RunningApp -import com.android.systemui.statusbar.phone.SystemUIDialog -import com.android.systemui.util.time.SystemClock -import java.util.concurrent.Executor - -/** - * Dialog which shows a list of running foreground services and offers controls to them - */ -class FgsManagerDialog( - context: Context, - private val executor: Executor, - @Background private val backgroundExecutor: Executor, - private val systemClock: SystemClock, - private val fgsManagerDialogController: FgsManagerDialogController -) : SystemUIDialog(context, R.style.Theme_SystemUI_Dialog) { - - private val appListRecyclerView: RecyclerView = RecyclerView(this.context) - private val adapter: AppListAdapter = AppListAdapter() - - init { - setTitle(R.string.fgs_manager_dialog_title) - setView(appListRecyclerView) - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - appListRecyclerView.layoutManager = LinearLayoutManager(context) - fgsManagerDialogController.registerDialogForChanges( - object : FgsManagerDialogController.FgsManagerDialogCallback { - override fun onRunningAppsChanged(apps: List) { - executor.execute { - adapter.setData(apps) - } - } - } - ) - appListRecyclerView.adapter = adapter - backgroundExecutor.execute { adapter.setData(fgsManagerDialogController.runningAppList) } - } - - private inner class AppListAdapter : RecyclerView.Adapter() { - private val lock = Any() - - @GuardedBy("lock") - private val data: MutableList = ArrayList() - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AppItemViewHolder { - return AppItemViewHolder(LayoutInflater.from(context) - .inflate(R.layout.fgs_manager_app_item, parent, false)) - } - - override fun onBindViewHolder(holder: AppItemViewHolder, position: Int) { - var runningApp: RunningApp - synchronized(lock) { - runningApp = data[position] - } - with(holder) { - iconView.setImageDrawable(runningApp.mIcon) - appLabelView.text = runningApp.mAppLabel - durationView.text = DateUtils.formatDuration( - Math.max(systemClock.elapsedRealtime() - runningApp.mTimeStarted, 60000), - DateUtils.LENGTH_MEDIUM) - stopButton.setOnClickListener { - fgsManagerDialogController - .stopAllFgs(runningApp.mUserId, runningApp.mPackageName) - } - } - } - - override fun getItemCount(): Int { - synchronized(lock) { return data.size } - } - - fun setData(newData: List) { - var oldData: List - synchronized(lock) { - oldData = ArrayList(data) - data.clear() - data.addAll(newData) - } - - DiffUtil.calculateDiff(object : DiffUtil.Callback() { - override fun getOldListSize(): Int { - return oldData.size - } - - override fun getNewListSize(): Int { - return newData.size - } - - override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): - Boolean { - return oldData[oldItemPosition] == newData[newItemPosition] - } - - override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): - Boolean { - return true // TODO, look into updating the time subtext - } - }).dispatchUpdatesTo(this) - } - } - - private class AppItemViewHolder(parent: View) : RecyclerView.ViewHolder(parent) { - val appLabelView: TextView = parent.requireViewById(R.id.fgs_manager_app_item_label) - val durationView: TextView = parent.requireViewById(R.id.fgs_manager_app_item_duration) - val iconView: ImageView = parent.requireViewById(R.id.fgs_manager_app_item_icon) - val stopButton: Button = parent.requireViewById(R.id.fgs_manager_app_item_stop_button) - } -} \ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/fgsmanager/FgsManagerDialogController.kt b/packages/SystemUI/src/com/android/systemui/fgsmanager/FgsManagerDialogController.kt deleted file mode 100644 index 159ed39025a1c..0000000000000 --- a/packages/SystemUI/src/com/android/systemui/fgsmanager/FgsManagerDialogController.kt +++ /dev/null @@ -1,151 +0,0 @@ -/* - * Copyright (C) 2021 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.fgsmanager - -import android.content.pm.PackageManager -import android.content.pm.PackageManager.NameNotFoundException -import android.graphics.drawable.Drawable -import android.os.Handler -import android.os.UserHandle -import android.util.ArrayMap -import android.util.Log -import androidx.annotation.GuardedBy -import com.android.systemui.dagger.qualifiers.Background -import com.android.systemui.statusbar.policy.RunningFgsController -import com.android.systemui.statusbar.policy.RunningFgsController.UserPackageTime -import javax.inject.Inject - -/** - * Controls events relevant to FgsManagerDialog - */ -class FgsManagerDialogController @Inject constructor( - private val packageManager: PackageManager, - @Background private val backgroundHandler: Handler, - private val runningFgsController: RunningFgsController -) : RunningFgsController.Callback { - private val lock = Any() - private val clearCacheToken = Any() - - @GuardedBy("lock") - private var runningApps: Map? = null - @GuardedBy("lock") - private var listener: FgsManagerDialogCallback? = null - - interface FgsManagerDialogCallback { - fun onRunningAppsChanged(apps: List) - } - - data class RunningApp( - val mUserId: Int, - val mPackageName: String, - val mAppLabel: CharSequence, - val mIcon: Drawable, - val mTimeStarted: Long - ) - - val runningAppList: List - get() { - synchronized(lock) { - if (runningApps == null) { - onFgsPackagesChangedLocked(runningFgsController.getPackagesWithFgs()) - } - return convertToRunningAppList(runningApps!!) - } - } - - fun registerDialogForChanges(callback: FgsManagerDialogCallback) { - synchronized(lock) { - runningFgsController.addCallback(this) - listener = callback - backgroundHandler.removeCallbacksAndMessages(clearCacheToken) - } - } - - fun onFinishDialog() { - synchronized(lock) { - listener = null - // Keep data such as icons cached for some time since loading can be slow - backgroundHandler.postDelayed( - { - synchronized(lock) { - runningFgsController.removeCallback(this) - runningApps = null - } - }, clearCacheToken, RUNNING_APP_CACHE_TIMEOUT_MILLIS) - } - } - - private fun onRunningAppsChanged(apps: ArrayMap) { - listener?.let { - backgroundHandler.post { it.onRunningAppsChanged(convertToRunningAppList(apps)) } - } - } - - override fun onFgsPackagesChanged(packages: List) { - backgroundHandler.post { - synchronized(lock) { onFgsPackagesChangedLocked(packages) } - } - } - - /** - * Run on background thread - */ - private fun onFgsPackagesChangedLocked(packages: List) { - val newRunningApps = ArrayMap() - for (packageWithFgs in packages) { - val ra = runningApps?.get(packageWithFgs) - if (ra == null) { - val userId = packageWithFgs.userId - val packageName = packageWithFgs.packageName - try { - val ai = packageManager.getApplicationInfo(packageName, 0) - var icon = packageManager.getApplicationIcon(ai) - icon = packageManager.getUserBadgedIcon(icon, - UserHandle.of(userId)) - val label = packageManager.getApplicationLabel(ai) - newRunningApps[packageWithFgs] = RunningApp(userId, packageName, - label, icon, packageWithFgs.startTimeMillis) - } catch (e: NameNotFoundException) { - Log.e(LOG_TAG, - "Application info not found: $packageName", e) - } - } else { - newRunningApps[packageWithFgs] = ra - } - } - runningApps = newRunningApps - onRunningAppsChanged(newRunningApps) - } - - fun stopAllFgs(userId: Int, packageName: String) { - runningFgsController.stopFgs(userId, packageName) - } - - companion object { - private val LOG_TAG = FgsManagerDialogController::class.java.simpleName - private const val RUNNING_APP_CACHE_TIMEOUT_MILLIS: Long = 20_000 - - private fun convertToRunningAppList(apps: Map): - List { - val result = mutableListOf() - result.addAll(apps.values) - result.sortWith { a: RunningApp, b: RunningApp -> - b.mTimeStarted.compareTo(a.mTimeStarted) - } - return result - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/fgsmanager/FgsManagerDialogFactory.kt b/packages/SystemUI/src/com/android/systemui/fgsmanager/FgsManagerDialogFactory.kt deleted file mode 100644 index 28749296c4cfc..0000000000000 --- a/packages/SystemUI/src/com/android/systemui/fgsmanager/FgsManagerDialogFactory.kt +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright (C) 2021 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.fgsmanager - -import android.content.Context -import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.animation.DialogLaunchAnimator -import android.content.DialogInterface -import android.view.View -import com.android.systemui.dagger.qualifiers.Background -import com.android.systemui.dagger.qualifiers.Main -import com.android.systemui.util.time.SystemClock -import java.util.concurrent.Executor -import javax.inject.Inject - -/** - * Factory to create [FgsManagerDialog] instances - */ -@SysUISingleton -class FgsManagerDialogFactory -@Inject constructor( - private val context: Context, - @Main private val executor: Executor, - @Background private val backgroundExecutor: Executor, - private val systemClock: SystemClock, - private val dialogLaunchAnimator: DialogLaunchAnimator, - private val fgsManagerDialogController: FgsManagerDialogController -) { - - val lock = Any() - - companion object { - private var fgsManagerDialog: FgsManagerDialog? = null - } - - /** - * Creates the dialog if it doesn't exist - */ - fun create(viewLaunchedFrom: View?) { - if (fgsManagerDialog == null) { - fgsManagerDialog = FgsManagerDialog(context, executor, backgroundExecutor, - systemClock, fgsManagerDialogController) - fgsManagerDialog!!.setOnDismissListener { i: DialogInterface? -> - fgsManagerDialogController.onFinishDialog() - fgsManagerDialog = null - } - dialogLaunchAnimator.showFromView(fgsManagerDialog!!, viewLaunchedFrom!!) - } - } -} \ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt b/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt new file mode 100644 index 0000000000000..eb3415639db60 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt @@ -0,0 +1,430 @@ +/* + * 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 android.app.IActivityManager +import android.app.IForegroundServiceObserver +import android.content.Context +import android.content.pm.PackageManager +import android.graphics.drawable.Drawable +import android.os.IBinder +import android.os.PowerExemptionManager +import android.os.RemoteException +import android.provider.DeviceConfig.NAMESPACE_SYSTEMUI +import android.text.format.DateUtils +import android.util.ArrayMap +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Button +import android.widget.ImageView +import android.widget.TextView +import androidx.annotation.GuardedBy +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.android.internal.config.sysui.SystemUiDeviceConfigFlags.TASK_MANAGER_ENABLED +import com.android.systemui.R +import com.android.systemui.animation.DialogLaunchAnimator +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.statusbar.phone.SystemUIDialog +import com.android.systemui.util.DeviceConfigProxy +import com.android.systemui.util.time.SystemClock +import java.util.Objects +import java.util.concurrent.Executor +import javax.inject.Inject +import kotlin.math.max + +class FgsManagerController @Inject constructor( + private val context: Context, + @Main private val mainExecutor: Executor, + @Background private val backgroundExecutor: Executor, + private val systemClock: SystemClock, + private val activityManager: IActivityManager, + private val packageManager: PackageManager, + private val deviceConfigProxy: DeviceConfigProxy, + private val dialogLaunchAnimator: DialogLaunchAnimator +) : IForegroundServiceObserver.Stub() { + + companion object { + private val LOG_TAG = FgsManagerController::class.java.simpleName + } + + private var isAvailable = false + + private val lock = Any() + + @GuardedBy("lock") + var initialized = false + + @GuardedBy("lock") + private val runningServiceTokens = mutableMapOf() + + @GuardedBy("lock") + private var dialog: SystemUIDialog? = null + + @GuardedBy("lock") + private val appListAdapter: AppListAdapter = AppListAdapter() + + @GuardedBy("lock") + private var runningApps: ArrayMap = ArrayMap() + + interface OnNumberOfPackagesChangedListener { + fun onNumberOfPackagesChanged(numPackages: Int) + } + + interface OnDialogDismissedListener { + fun onDialogDismissed() + } + + fun init() { + synchronized(lock) { + if (initialized) { + return + } + try { + activityManager.registerForegroundServiceObserver(this) + } catch (e: RemoteException) { + e.rethrowFromSystemServer() + } + + deviceConfigProxy.addOnPropertiesChangedListener(NAMESPACE_SYSTEMUI, + backgroundExecutor) { + isAvailable = it.getBoolean(TASK_MANAGER_ENABLED, isAvailable) + } + + isAvailable = deviceConfigProxy + .getBoolean(NAMESPACE_SYSTEMUI, TASK_MANAGER_ENABLED, false) + + initialized = true + } + } + + override fun onForegroundStateChanged( + token: IBinder, + packageName: String, + userId: Int, + isForeground: Boolean + ) { + synchronized(lock) { + val numPackagesBefore = getNumRunningPackagesLocked() + 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) + } + } + + val numPackagesAfter = getNumRunningPackagesLocked() + + if (numPackagesAfter != numPackagesBefore) { + onNumberOfPackagesChangedListeners.forEach { + backgroundExecutor.execute { it.onNumberOfPackagesChanged(numPackagesAfter) } + } + } + + updateAppItemsLocked() + } + } + + @GuardedBy("lock") + val onNumberOfPackagesChangedListeners: MutableSet = + mutableSetOf() + + @GuardedBy("lock") + val onDialogDismissedListeners: MutableSet = mutableSetOf() + + fun addOnNumberOfPackagesChangedListener(listener: OnNumberOfPackagesChangedListener) { + synchronized(lock) { + onNumberOfPackagesChangedListeners.add(listener) + } + } + + fun removeOnNumberOfPackagesChangedListener(listener: OnNumberOfPackagesChangedListener) { + synchronized(lock) { + onNumberOfPackagesChangedListeners.remove(listener) + } + } + + fun addOnDialogDismissedListener(listener: OnDialogDismissedListener) { + synchronized(lock) { + onDialogDismissedListeners.add(listener) + } + } + + fun removeOnDialogDismissedListener(listener: OnDialogDismissedListener) { + synchronized(lock) { + onDialogDismissedListeners.remove(listener) + } + } + + fun isAvailable(): Boolean { + return isAvailable + } + + fun getNumRunningPackages(): Int { + synchronized(lock) { + return getNumRunningPackagesLocked() + } + } + + private fun getNumRunningPackagesLocked() = + runningServiceTokens.keys.count { it.uiControl != UIControl.HIDE_ENTRY } + + fun shouldUpdateFooterVisibility() = dialog == null + + fun showDialog(viewLaunchedFrom: View?) { + synchronized(lock) { + if (dialog == null) { + + val dialog = SystemUIDialog(context) + dialog.setTitle(R.string.fgs_manager_dialog_title) + + val dialogContext = dialog.context + + val recyclerView = RecyclerView(dialogContext) + recyclerView.layoutManager = LinearLayoutManager(dialogContext) + recyclerView.adapter = appListAdapter + + dialog.setView(recyclerView) + + this.dialog = dialog + + dialog.setOnDismissListener { + synchronized(lock) { + this.dialog = null + updateAppItemsLocked() + } + onDialogDismissedListeners.forEach { + mainExecutor.execute(it::onDialogDismissed) + } + } + + mainExecutor.execute { + viewLaunchedFrom + ?.let { dialogLaunchAnimator.showFromView(dialog, it) } ?: dialog.show() + } + + backgroundExecutor.execute { + synchronized(lock) { + updateAppItemsLocked() + } + } + } + } + } + + @GuardedBy("lock") + private fun updateAppItemsLocked() { + if (dialog == null) { + runningApps.clear() + return + } + + val addedPackages = runningServiceTokens.keys.filter { + it.uiControl != UIControl.HIDE_ENTRY && runningApps[it]?.stopped != true + } + val removedPackages = runningApps.keys.filter { !runningServiceTokens.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, + ai.loadLabel(packageManager), ai.loadIcon(packageManager)) + } + + removedPackages.forEach { pkg -> + val ra = runningApps[pkg]!! + val ra2 = ra.copy().also { + it.stopped = true + it.appLabel = ra.appLabel + it.icon = ra.icon + } + runningApps[pkg] = ra2 + } + + mainExecutor.execute { + appListAdapter + .setData(runningApps.values.toList().sortedByDescending { it.timeStarted }) + } + } + + private fun stopPackage(userId: Int, packageName: String) { + activityManager.stopAppForUser(packageName, userId) + } + + private inner class AppListAdapter : RecyclerView.Adapter() { + private val lock = Any() + + @GuardedBy("lock") + private var data: List = listOf() + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AppItemViewHolder { + return AppItemViewHolder(LayoutInflater.from(parent.context) + .inflate(R.layout.fgs_manager_app_item, parent, false)) + } + + override fun onBindViewHolder(holder: AppItemViewHolder, position: Int) { + var runningApp: RunningApp + synchronized(lock) { + runningApp = data[position] + } + with(holder) { + iconView.setImageDrawable(runningApp.icon) + appLabelView.text = runningApp.appLabel + durationView.text = DateUtils.formatDuration( + max(systemClock.elapsedRealtime() - runningApp.timeStarted, 60000), + DateUtils.LENGTH_MEDIUM) + stopButton.setOnClickListener { + stopButton.setText(R.string.fgs_manager_app_item_stop_button_stopped_label) + stopPackage(runningApp.userId, runningApp.packageName) + } + if (runningApp.uiControl == UIControl.HIDE_BUTTON) { + stopButton.visibility = View.INVISIBLE + } + if (runningApp.stopped) { + stopButton.isEnabled = false + stopButton.setText(R.string.fgs_manager_app_item_stop_button_stopped_label) + durationView.visibility = View.GONE + } else { + stopButton.isEnabled = true + stopButton.setText(R.string.fgs_manager_app_item_stop_button_label) + durationView.visibility = View.VISIBLE + } + } + } + + override fun getItemCount(): Int { + return data.size + } + + fun setData(newData: List) { + var oldData = data + data = newData + + DiffUtil.calculateDiff(object : DiffUtil.Callback() { + override fun getOldListSize(): Int { + return oldData.size + } + + override fun getNewListSize(): Int { + return newData.size + } + + override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): + Boolean { + return oldData[oldItemPosition] == newData[newItemPosition] + } + + override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): + Boolean { + return oldData[oldItemPosition].stopped == newData[newItemPosition].stopped + } + }).dispatchUpdatesTo(this) + } + } + + private inner class UserPackage( + val userId: Int, + val packageName: String + ) { + val uiControl: UIControl by lazy { + val uid = packageManager.getPackageUidAsUser(packageName, userId) + + when (activityManager.getBackgroundRestrictionExemptionReason(uid)) { + PowerExemptionManager.REASON_SYSTEM_UID, + PowerExemptionManager.REASON_DEVICE_DEMO_MODE -> UIControl.HIDE_ENTRY + + PowerExemptionManager.REASON_DEVICE_OWNER, + PowerExemptionManager.REASON_PROFILE_OWNER, + PowerExemptionManager.REASON_PROC_STATE_PERSISTENT, + PowerExemptionManager.REASON_PROC_STATE_PERSISTENT_UI, + PowerExemptionManager.REASON_ROLE_DIALER, + PowerExemptionManager.REASON_SYSTEM_MODULE -> UIControl.HIDE_BUTTON + else -> UIControl.NORMAL + } + } + + override fun equals(other: Any?): Boolean { + if (other !is UserPackage) { + return false + } + return other.packageName == packageName && other.userId == userId + } + + override fun hashCode(): Int = Objects.hash(userId, packageName) + } + + private data class StartTimeAndTokens( + val systemClock: SystemClock + ) { + val startTime = systemClock.elapsedRealtime() + val tokens = mutableSetOf() + + fun addToken(token: IBinder) { + tokens.add(token) + } + + fun removeToken(token: IBinder) { + tokens.remove(token) + } + + fun isEmpty(): Boolean { + return tokens.isEmpty() + } + } + + private class AppItemViewHolder(parent: View) : RecyclerView.ViewHolder(parent) { + val appLabelView: TextView = parent.requireViewById(R.id.fgs_manager_app_item_label) + val durationView: TextView = parent.requireViewById(R.id.fgs_manager_app_item_duration) + val iconView: ImageView = parent.requireViewById(R.id.fgs_manager_app_item_icon) + val stopButton: Button = parent.requireViewById(R.id.fgs_manager_app_item_stop_button) + } + + private data class RunningApp( + val userId: Int, + val packageName: String, + val timeStarted: Long, + val uiControl: UIControl + ) { + constructor( + userId: Int, + packageName: String, + timeStarted: Long, + uiControl: UIControl, + appLabel: CharSequence, + icon: Drawable + ) : this(userId, packageName, timeStarted, uiControl) { + this.appLabel = appLabel + this.icon = icon + } + + // variables to keep out of the generated equals() + var appLabel: CharSequence = "" + var icon: Drawable? = null + var stopped = false + } + + private enum class UIControl { + NORMAL, HIDE_BUTTON, HIDE_ENTRY + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFgsManagerFooter.java b/packages/SystemUI/src/com/android/systemui/qs/QSFgsManagerFooter.java index 082de1609423e..55d4a53ced7b2 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSFgsManagerFooter.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSFgsManagerFooter.java @@ -16,13 +16,9 @@ package com.android.systemui.qs; -import static android.provider.DeviceConfig.NAMESPACE_SYSTEMUI; - -import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.TASK_MANAGER_ENABLED; import static com.android.systemui.qs.dagger.QSFragmentModule.QS_FGS_MANAGER_FOOTER_VIEW; import android.content.Context; -import android.provider.DeviceConfig; import android.view.View; import android.widget.ImageView; import android.widget.TextView; @@ -30,8 +26,6 @@ import android.widget.TextView; import com.android.systemui.R; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; -import com.android.systemui.fgsmanager.FgsManagerDialogFactory; -import com.android.systemui.statusbar.policy.RunningFgsController; import java.util.concurrent.Executor; @@ -41,24 +35,25 @@ import javax.inject.Named; /** * Footer entry point for the foreground service manager */ -public class QSFgsManagerFooter implements View.OnClickListener { +public class QSFgsManagerFooter implements View.OnClickListener, + FgsManagerController.OnDialogDismissedListener, + FgsManagerController.OnNumberOfPackagesChangedListener { private final View mRootView; private final TextView mFooterText; private final Context mContext; private final Executor mMainExecutor; private final Executor mExecutor; - private final RunningFgsController mRunningFgsController; - private final FgsManagerDialogFactory mFgsManagerDialogFactory; + + private final FgsManagerController mFgsManagerController; private boolean mIsInitialized = false; - private boolean mIsAvailable = false; + private int mNumPackages; @Inject QSFgsManagerFooter(@Named(QS_FGS_MANAGER_FOOTER_VIEW) View rootView, - @Main Executor mainExecutor, RunningFgsController runningFgsController, - @Background Executor executor, - FgsManagerDialogFactory fgsManagerDialogFactory) { + @Main Executor mainExecutor, @Background Executor executor, + FgsManagerController fgsManagerController) { mRootView = rootView; mFooterText = mRootView.findViewById(R.id.footer_text); ImageView icon = mRootView.findViewById(R.id.primary_footer_icon); @@ -66,8 +61,7 @@ public class QSFgsManagerFooter implements View.OnClickListener { mContext = rootView.getContext(); mMainExecutor = mainExecutor; mExecutor = executor; - mRunningFgsController = runningFgsController; - mFgsManagerDialogFactory = fgsManagerDialogFactory; + mFgsManagerController = fgsManagerController; } public void init() { @@ -75,22 +69,28 @@ public class QSFgsManagerFooter implements View.OnClickListener { return; } + mFgsManagerController.init(); + mRootView.setOnClickListener(this); - mRunningFgsController.addCallback(packages -> refreshState()); - - DeviceConfig.addOnPropertiesChangedListener(NAMESPACE_SYSTEMUI, mExecutor, - (DeviceConfig.OnPropertiesChangedListener) properties -> { - mIsAvailable = properties.getBoolean(TASK_MANAGER_ENABLED, mIsAvailable); - }); - mIsAvailable = DeviceConfig.getBoolean(NAMESPACE_SYSTEMUI, TASK_MANAGER_ENABLED, false); - mIsInitialized = true; } + public void setListening(boolean listening) { + if (listening) { + mFgsManagerController.addOnDialogDismissedListener(this); + mFgsManagerController.addOnNumberOfPackagesChangedListener(this); + mNumPackages = mFgsManagerController.getNumRunningPackages(); + refreshState(); + } else { + mFgsManagerController.removeOnDialogDismissedListener(this); + mFgsManagerController.removeOnNumberOfPackagesChangedListener(this); + } + } + @Override public void onClick(View view) { - mFgsManagerDialogFactory.create(mRootView); + mFgsManagerController.showDialog(mRootView); } public void refreshState() { @@ -101,17 +101,25 @@ public class QSFgsManagerFooter implements View.OnClickListener { return mRootView; } - private boolean isAvailable() { - return mIsAvailable; - } - public void handleRefreshState() { - int numPackages = mRunningFgsController.getPackagesWithFgs().size(); mMainExecutor.execute(() -> { mFooterText.setText(mContext.getResources().getQuantityString( - R.plurals.fgs_manager_footer_label, numPackages, numPackages)); - mRootView.setVisibility(numPackages > 0 && isAvailable() ? View.VISIBLE : View.GONE); + R.plurals.fgs_manager_footer_label, mNumPackages, mNumPackages)); + if (mFgsManagerController.shouldUpdateFooterVisibility()) { + mRootView.setVisibility(mNumPackages > 0 + && mFgsManagerController.isAvailable() ? View.VISIBLE : View.GONE); + } }); } + @Override + public void onDialogDismissed() { + refreshState(); + } + + @Override + public void onNumberOfPackagesChanged(int numPackages) { + mNumPackages = numPackages; + refreshState(); + } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java index cbfe944a2c230..8f268b5cffe4c 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java @@ -192,6 +192,7 @@ public class QSPanelController extends QSPanelControllerBase { refreshAllTiles(); } + mQSFgsManagerFooter.setListening(listening); mQsSecurityFooter.setListening(listening); // Set the listening as soon as the QS fragment starts listening regardless of the diff --git a/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java index 48255b5360fc4..6d1bbeed5372c 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java +++ b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java @@ -34,8 +34,6 @@ import com.android.systemui.statusbar.policy.CastController; import com.android.systemui.statusbar.policy.DataSaverController; import com.android.systemui.statusbar.policy.DeviceControlsController; import com.android.systemui.statusbar.policy.HotspotController; -import com.android.systemui.statusbar.policy.RunningFgsController; -import com.android.systemui.statusbar.policy.RunningFgsControllerImpl; import com.android.systemui.statusbar.policy.WalletController; import com.android.systemui.util.settings.SecureSettings; @@ -91,9 +89,4 @@ public interface QSModule { /** */ @Binds QSHost provideQsHost(QSTileHost controllerImpl); - - /** */ - @Binds - RunningFgsController provideRunningFgsController( - RunningFgsControllerImpl runningFgsController); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RunningFgsController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RunningFgsController.kt deleted file mode 100644 index c6dbdb1b09a5e..0000000000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RunningFgsController.kt +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright (C) 2021 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.statusbar.policy - -/** - * Interface for tracking packages with running foreground services and demoting foreground status - */ -interface RunningFgsController : CallbackController { - - /** - * @return A list of [UserPackageTime]s which have running foreground service(s) - */ - fun getPackagesWithFgs(): List - - /** - * Stops all foreground services running as a package - * @param userId the userId the package is running under - * @param packageName the packageName - */ - fun stopFgs(userId: Int, packageName: String) - - /** - * Returns when the list of packages with foreground services changes - */ - interface Callback { - /** - * The thing that - * @param packages the list of packages - */ - fun onFgsPackagesChanged(packages: List) - } - - /** - * A triplet where each element is a package running - * under a user that has had at least one foreground service running since timeMillis. - * Time should be derived from [SystemClock.elapsedRealtime]. - */ - data class UserPackageTime(val userId: Int, val packageName: String, val startTimeMillis: Long) -} \ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RunningFgsControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RunningFgsControllerImpl.kt deleted file mode 100644 index 6d3345df4f3e0..0000000000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RunningFgsControllerImpl.kt +++ /dev/null @@ -1,171 +0,0 @@ -/* - * Copyright (C) 2021 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.statusbar.policy - -import android.app.IActivityManager -import android.app.IForegroundServiceObserver -import android.os.IBinder -import android.os.RemoteException -import android.util.Log -import androidx.annotation.GuardedBy -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleOwner -import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Background -import com.android.systemui.statusbar.policy.RunningFgsController.Callback -import com.android.systemui.statusbar.policy.RunningFgsController.UserPackageTime -import com.android.systemui.util.time.SystemClock -import java.util.concurrent.Executor -import javax.inject.Inject - -/** - * Implementation for [RunningFgsController] - */ -@SysUISingleton -class RunningFgsControllerImpl @Inject constructor( - @Background private val executor: Executor, - private val systemClock: SystemClock, - private val activityManager: IActivityManager -) : RunningFgsController, IForegroundServiceObserver.Stub() { - - companion object { - private val LOG_TAG = RunningFgsControllerImpl::class.java.simpleName - } - - private val lock = Any() - - @GuardedBy("lock") - var initialized = false - - @GuardedBy("lock") - private val runningServiceTokens = mutableMapOf() - - @GuardedBy("lock") - private val callbacks = mutableSetOf() - - fun init() { - synchronized(lock) { - if (initialized) { - return - } - try { - activityManager.registerForegroundServiceObserver(this) - } catch (e: RemoteException) { - e.rethrowFromSystemServer() - } - - initialized = true - } - } - - override fun addCallback(listener: Callback) { - init() - synchronized(lock) { callbacks.add(listener) } - } - - override fun removeCallback(listener: Callback) { - init() - synchronized(lock) { - if (!callbacks.remove(listener)) { - Log.e(LOG_TAG, "Callback was not registered.", RuntimeException()) - } - } - } - - override fun observe(lifecycle: Lifecycle?, listener: Callback?): Callback { - init() - return super.observe(lifecycle, listener) - } - - override fun observe(owner: LifecycleOwner?, listener: Callback?): Callback { - init() - return super.observe(owner, listener) - } - - override fun getPackagesWithFgs(): List { - init() - return synchronized(lock) { getPackagesWithFgsLocked() } - } - - private fun getPackagesWithFgsLocked(): List = - runningServiceTokens.map { - UserPackageTime(it.key.userId, it.key.packageName, it.value.fgsStartTime) - } - - override fun stopFgs(userId: Int, packageName: String) { - init() - try { - activityManager.stopAppForUser(packageName, userId) - } catch (e: RemoteException) { - e.rethrowFromSystemServer() - } - } - - private data class UserPackageKey( - val userId: Int, - val packageName: String - ) - - private class StartTimeAndTokensValue(systemClock: SystemClock) { - val fgsStartTime = systemClock.elapsedRealtime() - var tokens = mutableSetOf() - fun addToken(token: IBinder): Boolean { - return tokens.add(token) - } - - fun removeToken(token: IBinder): Boolean { - return tokens.remove(token) - } - - val isEmpty: Boolean - get() = tokens.isEmpty() - } - - override fun onForegroundStateChanged( - token: IBinder, - packageName: String, - userId: Int, - isForeground: Boolean - ) { - val result = synchronized(lock) { - val userPackageKey = UserPackageKey(userId, packageName) - if (isForeground) { - var addedNew = false - runningServiceTokens.getOrPut(userPackageKey) { - addedNew = true - StartTimeAndTokensValue(systemClock) - }.addToken(token) - if (!addedNew) { - return - } - } else { - val startTimeAndTokensValue = runningServiceTokens[userPackageKey] - if (startTimeAndTokensValue?.removeToken(token) == false) { - Log.e(LOG_TAG, - "Stopped foreground service was not known to be running.") - return - } - if (!startTimeAndTokensValue!!.isEmpty) { - return - } - runningServiceTokens.remove(userPackageKey) - } - getPackagesWithFgsLocked().toList() - } - - callbacks.forEach { executor.execute { it.onFgsPackagesChanged(result) } } - } -} \ No newline at end of file diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/RunningFgsControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/RunningFgsControllerTest.java deleted file mode 100644 index 6059afe8a70bf..0000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/RunningFgsControllerTest.java +++ /dev/null @@ -1,340 +0,0 @@ -/* - * Copyright (C) 2021 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.statusbar; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.atLeastOnce; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import android.app.IActivityManager; -import android.app.IForegroundServiceObserver; -import android.os.Binder; -import android.os.IBinder; -import android.os.RemoteException; -import android.testing.AndroidTestingRunner; -import android.util.Pair; - -import androidx.lifecycle.Lifecycle; -import androidx.lifecycle.LifecycleOwner; -import androidx.test.filters.MediumTest; - -import com.android.systemui.SysuiTestCase; -import com.android.systemui.statusbar.policy.RunningFgsController; -import com.android.systemui.statusbar.policy.RunningFgsController.UserPackageTime; -import com.android.systemui.statusbar.policy.RunningFgsControllerImpl; -import com.android.systemui.util.concurrency.FakeExecutor; -import com.android.systemui.util.time.FakeSystemClock; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.MockitoAnnotations; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Objects; -import java.util.Random; -import java.util.function.Consumer; - -@MediumTest -@RunWith(AndroidTestingRunner.class) -public class RunningFgsControllerTest extends SysuiTestCase { - - private RunningFgsController mController; - - private FakeSystemClock mSystemClock = new FakeSystemClock(); - private FakeExecutor mExecutor = new FakeExecutor(mSystemClock); - private TestCallback mCallback = new TestCallback(); - - @Mock - private IActivityManager mActivityManager; - @Mock - private Lifecycle mLifecycle; - @Mock - private LifecycleOwner mLifecycleOwner; - - @Before - public void setUp() { - MockitoAnnotations.initMocks(this); - when(mLifecycleOwner.getLifecycle()).thenReturn(mLifecycle); - mController = new RunningFgsControllerImpl(mExecutor, mSystemClock, mActivityManager); - } - - @Test - public void testInitRegistersListenerInImpl() throws RemoteException { - ((RunningFgsControllerImpl) mController).init(); - verify(mActivityManager, times(1)).registerForegroundServiceObserver(any()); - } - - @Test - public void testAddCallbackCallsInitInImpl() { - verifyInitIsCalled(controller -> controller.addCallback(mCallback)); - } - - @Test - public void testRemoveCallbackCallsInitInImpl() { - verifyInitIsCalled(controller -> controller.removeCallback(mCallback)); - } - - @Test - public void testObserve1CallsInitInImpl() { - verifyInitIsCalled(controller -> controller.observe(mLifecycle, mCallback)); - } - - @Test - public void testObserve2CallsInitInImpl() { - verifyInitIsCalled(controller -> controller.observe(mLifecycleOwner, mCallback)); - } - - @Test - public void testGetPackagesWithFgsCallsInitInImpl() { - verifyInitIsCalled(controller -> controller.getPackagesWithFgs()); - } - - @Test - public void testStopFgsCallsInitInImpl() { - verifyInitIsCalled(controller -> controller.stopFgs(0, "")); - } - - /** - * Tests that callbacks can be added - */ - @Test - public void testAddCallback() throws RemoteException { - String testPackageName = "testPackageName"; - int testUserId = 0; - - IForegroundServiceObserver observer = prepareObserver(); - mController.addCallback(mCallback); - - observer.onForegroundStateChanged(new Binder(), testPackageName, testUserId, true); - - mExecutor.advanceClockToLast(); - mExecutor.runAllReady(); - - assertEquals("Callback should have been invoked exactly once.", - 1, mCallback.mInvocations.size()); - - List userPackageTimes = mCallback.mInvocations.get(0); - assertEquals("There should have only been one package in callback. packages=" - + userPackageTimes, - 1, userPackageTimes.size()); - - UserPackageTime upt = userPackageTimes.get(0); - assertEquals(testPackageName, upt.getPackageName()); - assertEquals(testUserId, upt.getUserId()); - } - - /** - * Tests that callbacks can be removed. This test is only meaningful if - * {@link #testAddCallback()} can pass. - */ - @Test - public void testRemoveCallback() throws RemoteException { - String testPackageName = "testPackageName"; - int testUserId = 0; - - IForegroundServiceObserver observer = prepareObserver(); - mController.addCallback(mCallback); - mController.removeCallback(mCallback); - - observer.onForegroundStateChanged(new Binder(), testPackageName, testUserId, true); - - mExecutor.advanceClockToLast(); - mExecutor.runAllReady(); - - assertEquals("Callback should not have been invoked.", - 0, mCallback.mInvocations.size()); - } - - /** - * Tests packages are added when the controller receives a callback from activity manager for - * a foreground service start. - */ - @Test - public void testGetPackagesWithFgsAddingPackages() throws RemoteException { - int numPackages = 20; - int numUsers = 3; - - IForegroundServiceObserver observer = prepareObserver(); - - assertEquals("List should be empty", 0, mController.getPackagesWithFgs().size()); - - List> addedPackages = new ArrayList<>(); - for (int pkgNumber = 0; pkgNumber < numPackages; pkgNumber++) { - for (int userId = 0; userId < numUsers; userId++) { - String packageName = "package.name." + pkgNumber; - addedPackages.add(new Pair(userId, packageName)); - - observer.onForegroundStateChanged(new Binder(), packageName, userId, true); - - containsAllAddedPackages(addedPackages, mController.getPackagesWithFgs()); - } - } - } - - /** - * Tests packages are removed when the controller receives a callback from activity manager for - * a foreground service ending. - */ - @Test - public void testGetPackagesWithFgsRemovingPackages() throws RemoteException { - int numPackages = 20; - int numUsers = 3; - int arrayLength = numPackages * numUsers; - - String[] packages = new String[arrayLength]; - int[] users = new int[arrayLength]; - IBinder[] tokens = new IBinder[arrayLength]; - for (int pkgNumber = 0; pkgNumber < numPackages; pkgNumber++) { - for (int userId = 0; userId < numUsers; userId++) { - int i = pkgNumber * numUsers + userId; - packages[i] = "package.name." + pkgNumber; - users[i] = userId; - tokens[i] = new Binder(); - } - } - - IForegroundServiceObserver observer = prepareObserver(); - - for (int i = 0; i < packages.length; i++) { - observer.onForegroundStateChanged(tokens[i], packages[i], users[i], true); - } - - assertEquals(packages.length, mController.getPackagesWithFgs().size()); - - List removeOrder = new ArrayList<>(); - for (int i = 0; i < packages.length; i++) { - removeOrder.add(i); - } - Collections.shuffle(removeOrder, new Random(12345)); - - for (int idx : removeOrder) { - removePackageAndAssertRemovedFromList(observer, tokens[idx], packages[idx], users[idx]); - } - - assertEquals(0, mController.getPackagesWithFgs().size()); - } - - /** - * Tests a call on stopFgs forwards to activity manager. - */ - @Test - public void testStopFgs() throws RemoteException { - String pkgName = "package.name"; - mController.stopFgs(0, pkgName); - verify(mActivityManager).stopAppForUser(pkgName, 0); - } - - /** - * Tests a package which starts multiple services is only listed once and is only removed once - * all services are stopped. - */ - @Test - public void testSinglePackageWithMultipleServices() throws RemoteException { - String packageName = "package.name"; - int userId = 0; - IBinder serviceToken1 = new Binder(); - IBinder serviceToken2 = new Binder(); - - IForegroundServiceObserver observer = prepareObserver(); - - assertEquals(0, mController.getPackagesWithFgs().size()); - - observer.onForegroundStateChanged(serviceToken1, packageName, userId, true); - assertSinglePackage(packageName, userId); - - observer.onForegroundStateChanged(serviceToken2, packageName, userId, true); - assertSinglePackage(packageName, userId); - - observer.onForegroundStateChanged(serviceToken2, packageName, userId, false); - assertSinglePackage(packageName, userId); - - observer.onForegroundStateChanged(serviceToken1, packageName, userId, false); - assertEquals(0, mController.getPackagesWithFgs().size()); - } - - private IForegroundServiceObserver prepareObserver() - throws RemoteException { - mController.getPackagesWithFgs(); - - ArgumentCaptor argumentCaptor = - ArgumentCaptor.forClass(IForegroundServiceObserver.class); - verify(mActivityManager).registerForegroundServiceObserver(argumentCaptor.capture()); - - return argumentCaptor.getValue(); - } - - private void verifyInitIsCalled(Consumer c) { - RunningFgsControllerImpl spiedController = Mockito.spy( - ((RunningFgsControllerImpl) mController)); - c.accept(spiedController); - verify(spiedController, atLeastOnce()).init(); - } - - private void containsAllAddedPackages(List> addedPackages, - List runningFgsPackages) { - for (Pair userPkg : addedPackages) { - assertTrue(userPkg + " was not found in returned list", - runningFgsPackages.stream().anyMatch( - upt -> userPkg.first == upt.getUserId() - && Objects.equals(upt.getPackageName(), userPkg.second))); - } - for (UserPackageTime upt : runningFgsPackages) { - int userId = upt.getUserId(); - String packageName = upt.getPackageName(); - assertTrue("Unknown " - + " in returned list", - addedPackages.stream().anyMatch(userPkg -> userPkg.first == userId - && Objects.equals(packageName, userPkg.second))); - } - } - - private void removePackageAndAssertRemovedFromList(IForegroundServiceObserver observer, - IBinder token, String pkg, int userId) throws RemoteException { - observer.onForegroundStateChanged(token, pkg, userId, false); - List packagesWithFgs = mController.getPackagesWithFgs(); - assertFalse("Package \"" + pkg + "\" was not removed", - packagesWithFgs.stream().anyMatch(upt -> - Objects.equals(upt.getPackageName(), pkg) && upt.getUserId() == userId)); - } - - private void assertSinglePackage(String packageName, int userId) { - assertEquals(1, mController.getPackagesWithFgs().size()); - assertEquals(packageName, mController.getPackagesWithFgs().get(0).getPackageName()); - assertEquals(userId, mController.getPackagesWithFgs().get(0).getUserId()); - } - - private static class TestCallback implements RunningFgsController.Callback { - - private List> mInvocations = new ArrayList<>(); - - @Override - public void onFgsPackagesChanged(List packages) { - mInvocations.add(packages); - } - } -}