Implement Task Manager UI

This adds the dialog and a quicksettings tile entrypoint to show the
user apps which have foreground services running.

TODOs: Use security footer instead of quicksettings tile
       Implement the missing UX of the stopped FGS
       Tests

Test: atest SysetmUITests
Bug: 201579707

Change-Id: I2185a22b789404b97e98a16c11c0a296159ddd89
This commit is contained in:
Evan Severson
2021-10-13 14:58:37 -07:00
parent 974c389845
commit 1f6bb02a63
17 changed files with 822 additions and 3 deletions

View File

@@ -512,6 +512,11 @@ public final class SystemUiDeviceConfigFlags {
*/
public static final String DEFAULT_QR_CODE_SCANNER = "default_qr_code_scanner";
/**
* (boolean) Whether the task manager entrypoint is enabled.
*/
public static final String TASK_MANAGER_ENABLED = "task_manager_enabled";
private SystemUiDeviceConfigFlags() {
}
}

View File

@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<!-- Remove when Fgs manager tile is removed -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M2,4h4v4h-4z"
android:fillColor="#000000"/>
<path
android:pathData="M8,4h14v4h-14z"
android:fillColor="#000000"/>
<path
android:pathData="M2,10h4v4h-4z"
android:fillColor="#000000"/>
<path
android:pathData="M8,10h14v4h-14z"
android:fillColor="#000000"/>
<path
android:pathData="M2,16h4v4h-4z"
android:fillColor="#000000"/>
<path
android:pathData="M8,16h14v4h-14z"
android:fillColor="#000000"/>
</vector>

View File

@@ -0,0 +1,57 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="32dp"
android:orientation="horizontal"
android:gravity="center">
<ImageView
android:id="@+id/fgs_manager_app_item_icon"
android:layout_width="28dp"
android:layout_height="28dp"
android:layout_marginRight="12dp" />
<LinearLayout
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/fgs_manager_app_item_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="start"
style="@style/TextAppearance.Dialog.Body" />
<TextView
android:id="@+id/fgs_manager_app_item_duration"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="start"
style="@style/FgsManagerAppDuration" />
</LinearLayout>
<Button
android:id="@+id/fgs_manager_app_item_stop_button"
android:layout_width="wrap_content"
android:layout_height="48dp"
android:text="@string/fgs_manager_app_item_stop_button_label"
android:layout_marginLeft="12dp"
style="?android:attr/buttonBarNeutralButtonStyle" />
</LinearLayout>

View File

@@ -82,7 +82,7 @@
<!-- Tiles native to System UI. Order should match "quick_settings_tiles_default" -->
<string name="quick_settings_tiles_stock" translatable="false">
internet,bt,flashlight,dnd,alarm,airplane,controls,wallet,rotation,battery,cast,screenrecord,mictoggle,cameratoggle,location,hotspot,inversion,saver,dark,work,night,reverse,reduce_brightness,qr_code_scanner,onehanded
internet,bt,flashlight,dnd,alarm,airplane,controls,wallet,rotation,battery,cast,screenrecord,mictoggle,cameratoggle,location,hotspot,inversion,saver,dark,work,night,reverse,reduce_brightness,qr_code_scanner,onehanded,fgsmanager
</string>
<!-- The tiles to display in QuickSettings -->

View File

@@ -1291,4 +1291,6 @@
<!-- ************************************************************************* -->
<dimen name="keyguard_unfold_translation_x">16dp</dimen>
<dimen name="fgs_manager_min_width_minor">100%</dimen>
</resources>

View File

@@ -2358,4 +2358,9 @@
<!-- Title for User Switch dialog. [CHAR LIMIT=20] -->
<string name="qs_user_switch_dialog_title">Select user</string>
<!-- Title for dialog listing applications currently running in the backing [CHAR LIMIT=NONE]-->
<string name="fgs_manager_dialog_title">Apps running in the background</string>
<!-- Label of the button to stop the app from running in the background [CHAR LIMIT=12]-->
<string name="fgs_manager_app_item_stop_button_label">Stop</string>
</resources>

View File

@@ -984,4 +984,14 @@
<style name="InternetDialog.Divider.Active"/>
<style name="FgsManagerDialogTitle">
<item name="android:fontFamily">@*android:string/config_bodyFontFamily</item>
<item name="android:textStyle">bold</item>
<item name="android:textDirection">locale</item>
</style>
<style name="FgsManagerAppDuration">
<item name="android:fontFamily">?android:attr/textAppearanceSmall</item>
<item name="android:textDirection">locale</item>
</style>
</resources>

View File

@@ -298,4 +298,14 @@
<item>Off</item>
<item>On</item>
</string-array>
<!-- State names for fgsmanager tile: unavailable, off, on.
This subtitle is shown when the tile is in that particular state but does not set its own
subtitle, so some of these may never appear on screen. They should still be translated as
if they could appear.[CHAR LIMIT=32] -->
<string-array name="tile_states_fgsmanager">
<item>Unavailable</item>
<item>Off</item>
<item>On</item>
</string-array>
</resources>

View File

@@ -0,0 +1,141 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.fgsmanager
import android.content.Context
import android.os.Bundle
import android.text.format.DateUtils
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.GuardedBy
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.android.systemui.R
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.fgsmanager.FgsManagerDialogController.RunningApp
import com.android.systemui.statusbar.phone.SystemUIDialog
import com.android.systemui.util.time.SystemClock
import java.util.concurrent.Executor
/**
* Dialog which shows a list of running foreground services and offers controls to them
*/
class FgsManagerDialog(
context: Context,
private val executor: Executor,
@Background private val backgroundExecutor: Executor,
private val systemClock: SystemClock,
private val fgsManagerDialogController: FgsManagerDialogController
) : SystemUIDialog(context, R.style.Theme_SystemUI_Dialog) {
private val appListRecyclerView: RecyclerView = RecyclerView(this.context)
private val adapter: AppListAdapter = AppListAdapter()
init {
setTitle(R.string.fgs_manager_dialog_title)
setView(appListRecyclerView)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
appListRecyclerView.layoutManager = LinearLayoutManager(context)
fgsManagerDialogController.registerDialogForChanges(
object : FgsManagerDialogController.FgsManagerDialogCallback {
override fun onRunningAppsChanged(apps: List<RunningApp>) {
executor.execute {
adapter.setData(apps)
}
}
}
)
appListRecyclerView.adapter = adapter
backgroundExecutor.execute { adapter.setData(fgsManagerDialogController.runningAppList) }
}
private inner class AppListAdapter : RecyclerView.Adapter<AppItemViewHolder>() {
private val lock = Any()
@GuardedBy("lock")
private val data: MutableList<RunningApp> = ArrayList()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AppItemViewHolder {
return AppItemViewHolder(LayoutInflater.from(context)
.inflate(R.layout.fgs_manager_app_item, parent, false))
}
override fun onBindViewHolder(holder: AppItemViewHolder, position: Int) {
var runningApp: RunningApp
synchronized(lock) {
runningApp = data[position]
}
with(holder) {
iconView.setImageDrawable(runningApp.mIcon)
appLabelView.text = runningApp.mAppLabel
durationView.text = DateUtils.formatDuration(
Math.max(systemClock.elapsedRealtime() - runningApp.mTimeStarted, 60000),
DateUtils.LENGTH_MEDIUM)
stopButton.setOnClickListener {
fgsManagerDialogController
.stopAllFgs(runningApp.mUserId, runningApp.mPackageName)
}
}
}
override fun getItemCount(): Int {
synchronized(lock) { return data.size }
}
fun setData(newData: List<RunningApp>) {
var oldData: List<RunningApp>
synchronized(lock) {
oldData = ArrayList(data)
data.clear()
data.addAll(newData)
}
DiffUtil.calculateDiff(object : DiffUtil.Callback() {
override fun getOldListSize(): Int {
return oldData.size
}
override fun getNewListSize(): Int {
return newData.size
}
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int):
Boolean {
return oldData[oldItemPosition] == newData[newItemPosition]
}
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int):
Boolean {
return true // TODO, look into updating the time subtext
}
}).dispatchUpdatesTo(this)
}
}
private class AppItemViewHolder(parent: View) : RecyclerView.ViewHolder(parent) {
val appLabelView: TextView = parent.requireViewById(R.id.fgs_manager_app_item_label)
val durationView: TextView = parent.requireViewById(R.id.fgs_manager_app_item_duration)
val iconView: ImageView = parent.requireViewById(R.id.fgs_manager_app_item_icon)
val stopButton: Button = parent.requireViewById(R.id.fgs_manager_app_item_stop_button)
}
}

View File

@@ -0,0 +1,151 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.fgsmanager
import android.content.pm.PackageManager
import android.content.pm.PackageManager.NameNotFoundException
import android.graphics.drawable.Drawable
import android.os.Handler
import android.os.UserHandle
import android.util.ArrayMap
import android.util.Log
import androidx.annotation.GuardedBy
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.statusbar.policy.RunningFgsController
import com.android.systemui.statusbar.policy.RunningFgsController.UserPackageTime
import javax.inject.Inject
/**
* Controls events relevant to FgsManagerDialog
*/
class FgsManagerDialogController @Inject constructor(
private val packageManager: PackageManager,
@Background private val backgroundHandler: Handler,
private val runningFgsController: RunningFgsController
) : RunningFgsController.Callback {
private val lock = Any()
private val clearCacheToken = Any()
@GuardedBy("lock")
private var runningApps: Map<UserPackageTime, RunningApp>? = null
@GuardedBy("lock")
private var listener: FgsManagerDialogCallback? = null
interface FgsManagerDialogCallback {
fun onRunningAppsChanged(apps: List<RunningApp>)
}
data class RunningApp(
val mUserId: Int,
val mPackageName: String,
val mAppLabel: CharSequence,
val mIcon: Drawable,
val mTimeStarted: Long
)
val runningAppList: List<RunningApp>
get() {
synchronized(lock) {
if (runningApps == null) {
onFgsPackagesChangedLocked(runningFgsController.getPackagesWithFgs())
}
return convertToRunningAppList(runningApps!!)
}
}
fun registerDialogForChanges(callback: FgsManagerDialogCallback) {
synchronized(lock) {
runningFgsController.addCallback(this)
listener = callback
backgroundHandler.removeCallbacksAndMessages(clearCacheToken)
}
}
fun onFinishDialog() {
synchronized(lock) {
listener = null
// Keep data such as icons cached for some time since loading can be slow
backgroundHandler.postDelayed(
{
synchronized(lock) {
runningFgsController.removeCallback(this)
runningApps = null
}
}, clearCacheToken, RUNNING_APP_CACHE_TIMEOUT_MILLIS)
}
}
private fun onRunningAppsChanged(apps: ArrayMap<UserPackageTime, RunningApp>) {
listener?.let {
backgroundHandler.post { it.onRunningAppsChanged(convertToRunningAppList(apps)) }
}
}
override fun onFgsPackagesChanged(packages: List<UserPackageTime>) {
backgroundHandler.post {
synchronized(lock) { onFgsPackagesChangedLocked(packages) }
}
}
/**
* Run on background thread
*/
private fun onFgsPackagesChangedLocked(packages: List<UserPackageTime>) {
val newRunningApps = ArrayMap<UserPackageTime, RunningApp>()
for (packageWithFgs in packages) {
val ra = runningApps?.get(packageWithFgs)
if (ra == null) {
val userId = packageWithFgs.userId
val packageName = packageWithFgs.packageName
try {
val ai = packageManager.getApplicationInfo(packageName, 0)
var icon = packageManager.getApplicationIcon(ai)
icon = packageManager.getUserBadgedIcon(icon,
UserHandle.of(userId))
val label = packageManager.getApplicationLabel(ai)
newRunningApps[packageWithFgs] = RunningApp(userId, packageName,
label, icon, packageWithFgs.startTimeMillis)
} catch (e: NameNotFoundException) {
Log.e(LOG_TAG,
"Application info not found: $packageName", e)
}
} else {
newRunningApps[packageWithFgs] = ra
}
}
runningApps = newRunningApps
onRunningAppsChanged(newRunningApps)
}
fun stopAllFgs(userId: Int, packageName: String) {
runningFgsController.stopFgs(userId, packageName)
}
companion object {
private val LOG_TAG = FgsManagerDialogController::class.java.simpleName
private const val RUNNING_APP_CACHE_TIMEOUT_MILLIS: Long = 20_000
private fun convertToRunningAppList(apps: Map<UserPackageTime, RunningApp>):
List<RunningApp> {
val result = mutableListOf<RunningApp>()
result.addAll(apps.values)
result.sortWith { a: RunningApp, b: RunningApp ->
b.mTimeStarted.compareTo(a.mTimeStarted)
}
return result
}
}
}

View File

@@ -0,0 +1,63 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.fgsmanager
import android.content.Context
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.animation.DialogLaunchAnimator
import android.content.DialogInterface
import android.view.View
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.util.time.SystemClock
import java.util.concurrent.Executor
import javax.inject.Inject
/**
* Factory to create [FgsManagerDialog] instances
*/
@SysUISingleton
class FgsManagerDialogFactory
@Inject constructor(
private val context: Context,
@Main private val executor: Executor,
@Background private val backgroundExecutor: Executor,
private val systemClock: SystemClock,
private val dialogLaunchAnimator: DialogLaunchAnimator,
private val fgsManagerDialogController: FgsManagerDialogController
) {
val lock = Any()
companion object {
private var fgsManagerDialog: FgsManagerDialog? = null
}
/**
* Creates the dialog if it doesn't exist
*/
fun create(viewLaunchedFrom: View?) {
if (fgsManagerDialog == null) {
fgsManagerDialog = FgsManagerDialog(context, executor, backgroundExecutor,
systemClock, fgsManagerDialogController)
fgsManagerDialog!!.setOnDismissListener { i: DialogInterface? ->
fgsManagerDialogController.onFinishDialog()
fgsManagerDialog = null
}
dialogLaunchAnimator.showFromView(fgsManagerDialog!!, viewLaunchedFrom!!)
}
}
}

View File

@@ -34,6 +34,8 @@ 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;
@@ -89,4 +91,9 @@ public interface QSModule {
/** */
@Binds
QSHost provideQsHost(QSTileHost controllerImpl);
/** */
@Binds
RunningFgsController provideRunningFgsController(
RunningFgsControllerImpl runningFgsController);
}

View File

@@ -36,6 +36,7 @@ import com.android.systemui.qs.tiles.ColorInversionTile;
import com.android.systemui.qs.tiles.DataSaverTile;
import com.android.systemui.qs.tiles.DeviceControlsTile;
import com.android.systemui.qs.tiles.DndTile;
import com.android.systemui.qs.tiles.FgsManagerTile;
import com.android.systemui.qs.tiles.FlashlightTile;
import com.android.systemui.qs.tiles.HotspotTile;
import com.android.systemui.qs.tiles.InternetTile;
@@ -94,6 +95,7 @@ public class QSFactoryImpl implements QSFactory {
private final Provider<QuickAccessWalletTile> mQuickAccessWalletTileProvider;
private final Provider<QRCodeScannerTile> mQRCodeScannerTileProvider;
private final Provider<OneHandedModeTile> mOneHandedModeTileProvider;
private final Provider<FgsManagerTile> mFgsManagerTileProvider;
private final Lazy<QSHost> mQsHostLazy;
private final Provider<CustomTile.Builder> mCustomTileBuilderProvider;
@@ -130,7 +132,8 @@ public class QSFactoryImpl implements QSFactory {
Provider<AlarmTile> alarmTileProvider,
Provider<QuickAccessWalletTile> quickAccessWalletTileProvider,
Provider<QRCodeScannerTile> qrCodeScannerTileProvider,
Provider<OneHandedModeTile> oneHandedModeTileProvider) {
Provider<OneHandedModeTile> oneHandedModeTileProvider,
Provider<FgsManagerTile> fgsManagerTileProvider) {
mQsHostLazy = qsHostLazy;
mCustomTileBuilderProvider = customTileBuilderProvider;
@@ -163,6 +166,7 @@ public class QSFactoryImpl implements QSFactory {
mQuickAccessWalletTileProvider = quickAccessWalletTileProvider;
mQRCodeScannerTileProvider = qrCodeScannerTileProvider;
mOneHandedModeTileProvider = oneHandedModeTileProvider;
mFgsManagerTileProvider = fgsManagerTileProvider;
}
public QSTile createTile(String tileSpec) {
@@ -233,6 +237,8 @@ public class QSFactoryImpl implements QSFactory {
return mQRCodeScannerTileProvider.get();
case "onehanded":
return mOneHandedModeTileProvider.get();
case "fgsmanager":
return mFgsManagerTileProvider.get();
}
// Custom tiles

View File

@@ -652,7 +652,8 @@ internal object SubtitleArrayMapping {
"wallet" to R.array.tile_states_wallet,
"qr_code_scanner" to R.array.tile_states_qr_code_scanner,
"alarm" to R.array.tile_states_alarm,
"onehanded" to R.array.tile_states_onehanded
"onehanded" to R.array.tile_states_onehanded,
"fgsmanager" to R.array.tile_states_fgsmanager
)
fun getSubtitleId(spec: String?): Int {

View File

@@ -0,0 +1,96 @@
/*
* 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.qs.tiles
import android.content.Intent
import android.os.Handler
import android.os.Looper
import android.provider.DeviceConfig
import android.view.View
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags
import com.android.internal.logging.MetricsLogger
import com.android.systemui.DejankUtils
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.plugins.ActivityStarter
import com.android.systemui.plugins.FalsingManager
import com.android.systemui.plugins.qs.QSTile
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.qs.QSHost
import com.android.systemui.qs.logging.QSLogger
import com.android.systemui.qs.tileimpl.QSTileImpl
import com.android.systemui.statusbar.policy.RunningFgsController
import com.android.systemui.statusbar.policy.RunningFgsController.UserPackageTime
import java.util.concurrent.Executor
import javax.inject.Inject
/**
* Quicksettings tile for the foreground services manager (task manager)
*/
class FgsManagerTile @Inject constructor(
host: QSHost?,
@Background backgroundLooper: Looper?,
@Background private val backgroundExecutor: Executor?,
@Main mainHandler: Handler?,
falsingManager: FalsingManager?,
metricsLogger: MetricsLogger?,
statusBarStateController: StatusBarStateController?,
activityStarter: ActivityStarter?,
qsLogger: QSLogger?,
private val fgsManagerDialogFactory: FgsManagerDialogFactory,
private val runningFgsController: RunningFgsController
) : QSTileImpl<QSTile.State?>(host, backgroundLooper, mainHandler, falsingManager, metricsLogger,
statusBarStateController, activityStarter, qsLogger), RunningFgsController.Callback {
override fun handleInitialize() {
super.handleInitialize()
mUiHandler.post { runningFgsController.observe(lifecycle, this) }
}
override fun isAvailable(): Boolean {
return DejankUtils.whitelistIpcs<Boolean> {
DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI,
SystemUiDeviceConfigFlags.TASK_MANAGER_ENABLED, false)
}
}
override fun newTileState(): QSTile.State {
return QSTile.State()
}
override fun handleClick(view: View?) {
mUiHandler.post { fgsManagerDialogFactory.create(view) }
}
override fun handleUpdateState(state: QSTile.State?, arg: Any?) {
state?.label = tileLabel
state?.secondaryLabel = runningFgsController.getPackagesWithFgs().size.toString()
state?.handlesLongClick = false
state?.icon = ResourceIcon.get(R.drawable.ic_list)
}
override fun getMetricsCategory(): Int = 0
override fun getLongClickIntent(): Intent? = null
// Inline the string so we don't waste translator time since this isn't used in the mocks.
// TODO If mocks change need to remember to move this to strings.xml
override fun getTileLabel(): CharSequence = "Active apps"
override fun onFgsPackagesChanged(packages: List<UserPackageTime>) = refreshState()
}

View File

@@ -0,0 +1,52 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.statusbar.policy
/**
* Interface for tracking packages with running foreground services and demoting foreground status
*/
interface RunningFgsController : CallbackController<RunningFgsController.Callback> {
/**
* @return A list of [UserPackageTime]s which have running foreground service(s)
*/
fun getPackagesWithFgs(): List<UserPackageTime>
/**
* Stops all foreground services running as a package
* @param userId the userId the package is running under
* @param packageName the packageName
*/
fun stopFgs(userId: Int, packageName: String)
/**
* Returns when the list of packages with foreground services changes
*/
interface Callback {
/**
* The thing that
* @param packages the list of packages
*/
fun onFgsPackagesChanged(packages: List<UserPackageTime>)
}
/**
* A triplet <user, packageName, timeMillis> where each element is a package running
* under a user that has had at least one foreground service running since timeMillis.
* Time should be derived from [SystemClock.elapsedRealtime].
*/
data class UserPackageTime(val userId: Int, val packageName: String, val startTimeMillis: Long)
}

View File

@@ -0,0 +1,171 @@
/*
* 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.makeServicesNonForeground(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) } }
}
}