Merge "Implement Task Manager UI"
This commit is contained in:
committed by
Android (Google) Code Review
commit
e068cd5544
@@ -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() {
|
||||
}
|
||||
}
|
||||
|
||||
42
packages/SystemUI/res/drawable/ic_list.xml
Normal file
42
packages/SystemUI/res/drawable/ic_list.xml
Normal 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>
|
||||
57
packages/SystemUI/res/layout/fgs_manager_app_item.xml
Normal file
57
packages/SystemUI/res/layout/fgs_manager_app_item.xml
Normal 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>
|
||||
@@ -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 -->
|
||||
|
||||
@@ -1295,4 +1295,6 @@
|
||||
<!-- ************************************************************************* -->
|
||||
|
||||
<dimen name="keyguard_unfold_translation_x">16dp</dimen>
|
||||
|
||||
<dimen name="fgs_manager_min_width_minor">100%</dimen>
|
||||
</resources>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -989,4 +989,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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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) } }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user