Clean up foreground service manager controllers
Use the same controller for the UI and the dialog and remove the RunningFgsController since it was very specific in what it provided. Also have the dialog disable the stop button when apps are stopped while showing. Also create exemption list for which apps can't be stopped or shouldn't even be visible. Test: Manual Bug: 201579707 Change-Id: I1e528acaa01d539d89163d7241fd0aba9c19a82f
This commit is contained in:
@@ -2367,6 +2367,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>
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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) } }
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user