diff --git a/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java b/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java
index bff813e82b1f7..6a6f60e967c18 100644
--- a/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java
+++ b/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java
@@ -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() {
}
}
diff --git a/packages/SystemUI/res/drawable/ic_list.xml b/packages/SystemUI/res/drawable/ic_list.xml
new file mode 100644
index 0000000000000..7ef52991234ae
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_list.xml
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/SystemUI/res/layout/fgs_manager_app_item.xml b/packages/SystemUI/res/layout/fgs_manager_app_item.xml
new file mode 100644
index 0000000000000..d034f4e512a31
--- /dev/null
+++ b/packages/SystemUI/res/layout/fgs_manager_app_item.xml
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index 1bf2cd8a2a3fa..8b59d86ee73af 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -82,7 +82,7 @@
- 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
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index f60842071ac7d..a437ae6169995 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1295,4 +1295,6 @@
16dp
+
+ 100%
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 111e3c42bf36c..300cb2d38dffd 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -2358,4 +2358,9 @@
Select user
+
+
+ Apps running in the background
+
+ Stop
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index 7776009fc54a6..12a022d8d130f 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -989,4 +989,14 @@
+
+
+
diff --git a/packages/SystemUI/res/values/tiles_states_strings.xml b/packages/SystemUI/res/values/tiles_states_strings.xml
index ed29bc76b3508..5fdb4978df734 100644
--- a/packages/SystemUI/res/values/tiles_states_strings.xml
+++ b/packages/SystemUI/res/values/tiles_states_strings.xml
@@ -298,4 +298,14 @@
- Off
- On
+
+
+
+ - Unavailable
+ - Off
+ - On
+
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/fgsmanager/FgsManagerDialog.kt b/packages/SystemUI/src/com/android/systemui/fgsmanager/FgsManagerDialog.kt
new file mode 100644
index 0000000000000..42f3512129a39
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/fgsmanager/FgsManagerDialog.kt
@@ -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) {
+ executor.execute {
+ adapter.setData(apps)
+ }
+ }
+ }
+ )
+ appListRecyclerView.adapter = adapter
+ backgroundExecutor.execute { adapter.setData(fgsManagerDialogController.runningAppList) }
+ }
+
+ private inner class AppListAdapter : RecyclerView.Adapter() {
+ private val lock = Any()
+
+ @GuardedBy("lock")
+ private val data: MutableList = ArrayList()
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AppItemViewHolder {
+ return AppItemViewHolder(LayoutInflater.from(context)
+ .inflate(R.layout.fgs_manager_app_item, parent, false))
+ }
+
+ override fun onBindViewHolder(holder: AppItemViewHolder, position: Int) {
+ var runningApp: RunningApp
+ synchronized(lock) {
+ runningApp = data[position]
+ }
+ with(holder) {
+ iconView.setImageDrawable(runningApp.mIcon)
+ appLabelView.text = runningApp.mAppLabel
+ durationView.text = DateUtils.formatDuration(
+ Math.max(systemClock.elapsedRealtime() - runningApp.mTimeStarted, 60000),
+ DateUtils.LENGTH_MEDIUM)
+ stopButton.setOnClickListener {
+ fgsManagerDialogController
+ .stopAllFgs(runningApp.mUserId, runningApp.mPackageName)
+ }
+ }
+ }
+
+ override fun getItemCount(): Int {
+ synchronized(lock) { return data.size }
+ }
+
+ fun setData(newData: List) {
+ var oldData: List
+ synchronized(lock) {
+ oldData = ArrayList(data)
+ data.clear()
+ data.addAll(newData)
+ }
+
+ DiffUtil.calculateDiff(object : DiffUtil.Callback() {
+ override fun getOldListSize(): Int {
+ return oldData.size
+ }
+
+ override fun getNewListSize(): Int {
+ return newData.size
+ }
+
+ override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int):
+ Boolean {
+ return oldData[oldItemPosition] == newData[newItemPosition]
+ }
+
+ override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int):
+ Boolean {
+ return true // TODO, look into updating the time subtext
+ }
+ }).dispatchUpdatesTo(this)
+ }
+ }
+
+ private class AppItemViewHolder(parent: View) : RecyclerView.ViewHolder(parent) {
+ val appLabelView: TextView = parent.requireViewById(R.id.fgs_manager_app_item_label)
+ val durationView: TextView = parent.requireViewById(R.id.fgs_manager_app_item_duration)
+ val iconView: ImageView = parent.requireViewById(R.id.fgs_manager_app_item_icon)
+ val stopButton: Button = parent.requireViewById(R.id.fgs_manager_app_item_stop_button)
+ }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/fgsmanager/FgsManagerDialogController.kt b/packages/SystemUI/src/com/android/systemui/fgsmanager/FgsManagerDialogController.kt
new file mode 100644
index 0000000000000..159ed39025a1c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/fgsmanager/FgsManagerDialogController.kt
@@ -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? = null
+ @GuardedBy("lock")
+ private var listener: FgsManagerDialogCallback? = null
+
+ interface FgsManagerDialogCallback {
+ fun onRunningAppsChanged(apps: List)
+ }
+
+ data class RunningApp(
+ val mUserId: Int,
+ val mPackageName: String,
+ val mAppLabel: CharSequence,
+ val mIcon: Drawable,
+ val mTimeStarted: Long
+ )
+
+ val runningAppList: List
+ get() {
+ synchronized(lock) {
+ if (runningApps == null) {
+ onFgsPackagesChangedLocked(runningFgsController.getPackagesWithFgs())
+ }
+ return convertToRunningAppList(runningApps!!)
+ }
+ }
+
+ fun registerDialogForChanges(callback: FgsManagerDialogCallback) {
+ synchronized(lock) {
+ runningFgsController.addCallback(this)
+ listener = callback
+ backgroundHandler.removeCallbacksAndMessages(clearCacheToken)
+ }
+ }
+
+ fun onFinishDialog() {
+ synchronized(lock) {
+ listener = null
+ // Keep data such as icons cached for some time since loading can be slow
+ backgroundHandler.postDelayed(
+ {
+ synchronized(lock) {
+ runningFgsController.removeCallback(this)
+ runningApps = null
+ }
+ }, clearCacheToken, RUNNING_APP_CACHE_TIMEOUT_MILLIS)
+ }
+ }
+
+ private fun onRunningAppsChanged(apps: ArrayMap) {
+ listener?.let {
+ backgroundHandler.post { it.onRunningAppsChanged(convertToRunningAppList(apps)) }
+ }
+ }
+
+ override fun onFgsPackagesChanged(packages: List) {
+ backgroundHandler.post {
+ synchronized(lock) { onFgsPackagesChangedLocked(packages) }
+ }
+ }
+
+ /**
+ * Run on background thread
+ */
+ private fun onFgsPackagesChangedLocked(packages: List) {
+ val newRunningApps = ArrayMap()
+ for (packageWithFgs in packages) {
+ val ra = runningApps?.get(packageWithFgs)
+ if (ra == null) {
+ val userId = packageWithFgs.userId
+ val packageName = packageWithFgs.packageName
+ try {
+ val ai = packageManager.getApplicationInfo(packageName, 0)
+ var icon = packageManager.getApplicationIcon(ai)
+ icon = packageManager.getUserBadgedIcon(icon,
+ UserHandle.of(userId))
+ val label = packageManager.getApplicationLabel(ai)
+ newRunningApps[packageWithFgs] = RunningApp(userId, packageName,
+ label, icon, packageWithFgs.startTimeMillis)
+ } catch (e: NameNotFoundException) {
+ Log.e(LOG_TAG,
+ "Application info not found: $packageName", e)
+ }
+ } else {
+ newRunningApps[packageWithFgs] = ra
+ }
+ }
+ runningApps = newRunningApps
+ onRunningAppsChanged(newRunningApps)
+ }
+
+ fun stopAllFgs(userId: Int, packageName: String) {
+ runningFgsController.stopFgs(userId, packageName)
+ }
+
+ companion object {
+ private val LOG_TAG = FgsManagerDialogController::class.java.simpleName
+ private const val RUNNING_APP_CACHE_TIMEOUT_MILLIS: Long = 20_000
+
+ private fun convertToRunningAppList(apps: Map):
+ List {
+ val result = mutableListOf()
+ result.addAll(apps.values)
+ result.sortWith { a: RunningApp, b: RunningApp ->
+ b.mTimeStarted.compareTo(a.mTimeStarted)
+ }
+ return result
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/fgsmanager/FgsManagerDialogFactory.kt b/packages/SystemUI/src/com/android/systemui/fgsmanager/FgsManagerDialogFactory.kt
new file mode 100644
index 0000000000000..28749296c4cfc
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/fgsmanager/FgsManagerDialogFactory.kt
@@ -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!!)
+ }
+ }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java
index 6d1bbeed5372c..48255b5360fc4 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java
@@ -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);
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java
index 16be99827afb3..ac95bf50bb988 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java
@@ -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 mQuickAccessWalletTileProvider;
private final Provider mQRCodeScannerTileProvider;
private final Provider mOneHandedModeTileProvider;
+ private final Provider mFgsManagerTileProvider;
private final Lazy mQsHostLazy;
private final Provider mCustomTileBuilderProvider;
@@ -130,7 +132,8 @@ public class QSFactoryImpl implements QSFactory {
Provider alarmTileProvider,
Provider quickAccessWalletTileProvider,
Provider qrCodeScannerTileProvider,
- Provider oneHandedModeTileProvider) {
+ Provider oneHandedModeTileProvider,
+ Provider 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
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
index 09fad30f02ffc..eb74b1bfa8bbe 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
@@ -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 {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/FgsManagerTile.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/FgsManagerTile.kt
new file mode 100644
index 0000000000000..75cf4d1ace8e7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/FgsManagerTile.kt
@@ -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(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 {
+ 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) = refreshState()
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RunningFgsController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RunningFgsController.kt
new file mode 100644
index 0000000000000..c6dbdb1b09a5e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RunningFgsController.kt
@@ -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 {
+
+ /**
+ * @return A list of [UserPackageTime]s which have running foreground service(s)
+ */
+ fun getPackagesWithFgs(): List
+
+ /**
+ * Stops all foreground services running as a package
+ * @param userId the userId the package is running under
+ * @param packageName the packageName
+ */
+ fun stopFgs(userId: Int, packageName: String)
+
+ /**
+ * Returns when the list of packages with foreground services changes
+ */
+ interface Callback {
+ /**
+ * The thing that
+ * @param packages the list of packages
+ */
+ fun onFgsPackagesChanged(packages: List)
+ }
+
+ /**
+ * A triplet where each element is a package running
+ * under a user that has had at least one foreground service running since timeMillis.
+ * Time should be derived from [SystemClock.elapsedRealtime].
+ */
+ data class UserPackageTime(val userId: Int, val packageName: String, val startTimeMillis: Long)
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RunningFgsControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RunningFgsControllerImpl.kt
new file mode 100644
index 0000000000000..d44d36503e30e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RunningFgsControllerImpl.kt
@@ -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()
+
+ @GuardedBy("lock")
+ private val callbacks = mutableSetOf()
+
+ fun init() {
+ synchronized(lock) {
+ if (initialized) {
+ return
+ }
+ try {
+ activityManager.registerForegroundServiceObserver(this)
+ } catch (e: RemoteException) {
+ e.rethrowFromSystemServer()
+ }
+
+ initialized = true
+ }
+ }
+
+ override fun addCallback(listener: Callback) {
+ init()
+ synchronized(lock) { callbacks.add(listener) }
+ }
+
+ override fun removeCallback(listener: Callback) {
+ init()
+ synchronized(lock) {
+ if (!callbacks.remove(listener)) {
+ Log.e(LOG_TAG, "Callback was not registered.", RuntimeException())
+ }
+ }
+ }
+
+ override fun observe(lifecycle: Lifecycle?, listener: Callback?): Callback {
+ init()
+ return super.observe(lifecycle, listener)
+ }
+
+ override fun observe(owner: LifecycleOwner?, listener: Callback?): Callback {
+ init()
+ return super.observe(owner, listener)
+ }
+
+ override fun getPackagesWithFgs(): List {
+ init()
+ return synchronized(lock) { getPackagesWithFgsLocked() }
+ }
+
+ private fun getPackagesWithFgsLocked(): List =
+ runningServiceTokens.map {
+ UserPackageTime(it.key.userId, it.key.packageName, it.value.fgsStartTime)
+ }
+
+ override fun stopFgs(userId: Int, packageName: String) {
+ init()
+ try {
+ activityManager.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()
+ fun addToken(token: IBinder): Boolean {
+ return tokens.add(token)
+ }
+
+ fun removeToken(token: IBinder): Boolean {
+ return tokens.remove(token)
+ }
+
+ val isEmpty: Boolean
+ get() = tokens.isEmpty()
+ }
+
+ override fun onForegroundStateChanged(
+ token: IBinder,
+ packageName: String,
+ userId: Int,
+ isForeground: Boolean
+ ) {
+ val result = synchronized(lock) {
+ val userPackageKey = UserPackageKey(userId, packageName)
+ if (isForeground) {
+ var addedNew = false
+ runningServiceTokens.getOrPut(userPackageKey) {
+ addedNew = true
+ StartTimeAndTokensValue(systemClock)
+ }.addToken(token)
+ if (!addedNew) {
+ return
+ }
+ } else {
+ val startTimeAndTokensValue = runningServiceTokens[userPackageKey]
+ if (startTimeAndTokensValue?.removeToken(token) == false) {
+ Log.e(LOG_TAG,
+ "Stopped foreground service was not known to be running.")
+ return
+ }
+ if (!startTimeAndTokensValue!!.isEmpty) {
+ return
+ }
+ runningServiceTokens.remove(userPackageKey)
+ }
+ getPackagesWithFgsLocked().toList()
+ }
+
+ callbacks.forEach { executor.execute { it.onFgsPackagesChanged(result) } }
+ }
+}
\ No newline at end of file