Merge "Clean up foreground service manager controllers"
This commit is contained in:
committed by
Android (Google) Code Review
commit
7722960559
@@ -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>
|
||||
|
||||
@@ -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