Merge "Clean up foreground service manager controllers"

This commit is contained in:
TreeHugger Robot
2022-02-03 16:49:19 +00:00
committed by Android (Google) Code Review
11 changed files with 472 additions and 956 deletions

View File

@@ -2379,6 +2379,8 @@
<string name="fgs_manager_dialog_title">Active apps</string>
<!-- Label of the button to stop an app from running [CHAR LIMIT=12]-->
<string name="fgs_manager_app_item_stop_button_label">Stop</string>
<!-- Label of the button to stop an app from running but the app is already stopped and the button is disabled [CHAR LIMIT=12]-->
<string name="fgs_manager_app_item_stop_button_stopped_label">Stopped</string>
<!-- Label for button to copy edited text back to the clipboard [CHAR LIMIT=20] -->
<string name="clipboard_edit_text_copy">Copy</string>

View File

@@ -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<RunningApp>) {
executor.execute {
adapter.setData(apps)
}
}
}
)
appListRecyclerView.adapter = adapter
backgroundExecutor.execute { adapter.setData(fgsManagerDialogController.runningAppList) }
}
private inner class AppListAdapter : RecyclerView.Adapter<AppItemViewHolder>() {
private val lock = Any()
@GuardedBy("lock")
private val data: MutableList<RunningApp> = 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<RunningApp>) {
var oldData: List<RunningApp>
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)
}
}

View File

@@ -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<UserPackageTime, RunningApp>? = null
@GuardedBy("lock")
private var listener: FgsManagerDialogCallback? = null
interface FgsManagerDialogCallback {
fun onRunningAppsChanged(apps: List<RunningApp>)
}
data class RunningApp(
val mUserId: Int,
val mPackageName: String,
val mAppLabel: CharSequence,
val mIcon: Drawable,
val mTimeStarted: Long
)
val runningAppList: List<RunningApp>
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<UserPackageTime, RunningApp>) {
listener?.let {
backgroundHandler.post { it.onRunningAppsChanged(convertToRunningAppList(apps)) }
}
}
override fun onFgsPackagesChanged(packages: List<UserPackageTime>) {
backgroundHandler.post {
synchronized(lock) { onFgsPackagesChangedLocked(packages) }
}
}
/**
* Run on background thread
*/
private fun onFgsPackagesChangedLocked(packages: List<UserPackageTime>) {
val newRunningApps = ArrayMap<UserPackageTime, RunningApp>()
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<UserPackageTime, RunningApp>):
List<RunningApp> {
val result = mutableListOf<RunningApp>()
result.addAll(apps.values)
result.sortWith { a: RunningApp, b: RunningApp ->
b.mTimeStarted.compareTo(a.mTimeStarted)
}
return result
}
}
}

View File

@@ -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!!)
}
}
}

View File

@@ -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<UserPackage, StartTimeAndTokens>()
@GuardedBy("lock")
private var dialog: SystemUIDialog? = null
@GuardedBy("lock")
private val appListAdapter: AppListAdapter = AppListAdapter()
@GuardedBy("lock")
private var runningApps: ArrayMap<UserPackage, RunningApp> = 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<OnNumberOfPackagesChangedListener> =
mutableSetOf()
@GuardedBy("lock")
val onDialogDismissedListeners: MutableSet<OnDialogDismissedListener> = 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<AppItemViewHolder>() {
private val lock = Any()
@GuardedBy("lock")
private var data: List<RunningApp> = 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<RunningApp>) {
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<IBinder>()
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
}
}

View File

@@ -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();
}
}

View File

@@ -192,6 +192,7 @@ public class QSPanelController extends QSPanelControllerBase<QSPanel> {
refreshAllTiles();
}
mQSFgsManagerFooter.setListening(listening);
mQsSecurityFooter.setListening(listening);
// Set the listening as soon as the QS fragment starts listening regardless of the

View File

@@ -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);
}

View File

@@ -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<RunningFgsController.Callback> {
/**
* @return A list of [UserPackageTime]s which have running foreground service(s)
*/
fun getPackagesWithFgs(): List<UserPackageTime>
/**
* 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<UserPackageTime>)
}
/**
* A triplet <user, packageName, timeMillis> 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)
}

View File

@@ -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<UserPackageKey, StartTimeAndTokensValue>()
@GuardedBy("lock")
private val callbacks = mutableSetOf<Callback>()
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<UserPackageTime> {
init()
return synchronized(lock) { getPackagesWithFgsLocked() }
}
private fun getPackagesWithFgsLocked(): List<UserPackageTime> =
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<IBinder>()
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) } }
}
}

View File

@@ -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<UserPackageTime> 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<Pair<Integer, String>> 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<Integer> 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<IForegroundServiceObserver> argumentCaptor =
ArgumentCaptor.forClass(IForegroundServiceObserver.class);
verify(mActivityManager).registerForegroundServiceObserver(argumentCaptor.capture());
return argumentCaptor.getValue();
}
private void verifyInitIsCalled(Consumer<RunningFgsControllerImpl> c) {
RunningFgsControllerImpl spiedController = Mockito.spy(
((RunningFgsControllerImpl) mController));
c.accept(spiedController);
verify(spiedController, atLeastOnce()).init();
}
private void containsAllAddedPackages(List<Pair<Integer, String>> addedPackages,
List<UserPackageTime> runningFgsPackages) {
for (Pair<Integer, String> 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 <user=" + userId + ", package=" + packageName + ">"
+ " 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<UserPackageTime> 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<List<UserPackageTime>> mInvocations = new ArrayList<>();
@Override
public void onFgsPackagesChanged(List<UserPackageTime> packages) {
mInvocations.add(packages);
}
}
}