• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.qs
18 
19 import android.app.IActivityManager
20 import android.app.IForegroundServiceObserver
21 import android.app.job.IUserVisibleJobObserver
22 import android.app.job.JobScheduler
23 import android.app.job.UserVisibleJobSummary
24 import android.content.BroadcastReceiver
25 import android.content.Context
26 import android.content.Intent
27 import android.content.IntentFilter
28 import android.content.pm.PackageManager
29 import android.content.pm.UserInfo
30 import android.content.res.Resources
31 import android.graphics.drawable.Drawable
32 import android.os.IBinder
33 import android.os.PowerExemptionManager
34 import android.os.RemoteException
35 import android.os.UserHandle
36 import android.provider.DeviceConfig.NAMESPACE_SYSTEMUI
37 import android.text.format.DateUtils
38 import android.util.ArrayMap
39 import android.util.IndentingPrintWriter
40 import android.view.LayoutInflater
41 import android.view.View
42 import android.view.ViewGroup
43 import android.widget.Button
44 import android.widget.ImageView
45 import android.widget.TextView
46 import androidx.annotation.GuardedBy
47 import androidx.annotation.VisibleForTesting
48 import androidx.annotation.WorkerThread
49 import androidx.recyclerview.widget.DiffUtil
50 import androidx.recyclerview.widget.LinearLayoutManager
51 import androidx.recyclerview.widget.RecyclerView
52 import com.android.internal.config.sysui.SystemUiDeviceConfigFlags.TASK_MANAGER_INFORM_JOB_SCHEDULER_OF_PENDING_APP_STOP
53 import com.android.internal.config.sysui.SystemUiDeviceConfigFlags.TASK_MANAGER_SHOW_FOOTER_DOT
54 import com.android.internal.config.sysui.SystemUiDeviceConfigFlags.TASK_MANAGER_SHOW_STOP_BUTTON_FOR_USER_ALLOWLISTED_APPS
55 import com.android.internal.config.sysui.SystemUiDeviceConfigFlags.TASK_MANAGER_SHOW_USER_VISIBLE_JOBS
56 import com.android.internal.jank.InteractionJankMonitor
57 import com.android.systemui.Dumpable
58 import com.android.systemui.Flags
59 import com.android.systemui.animation.DialogCuj
60 import com.android.systemui.animation.DialogTransitionAnimator
61 import com.android.systemui.animation.Expandable
62 import com.android.systemui.broadcast.BroadcastDispatcher
63 import com.android.systemui.dagger.SysUISingleton
64 import com.android.systemui.dagger.qualifiers.Background
65 import com.android.systemui.dagger.qualifiers.Main
66 import com.android.systemui.dump.DumpManager
67 import com.android.systemui.res.R
68 import com.android.systemui.settings.UserTracker
69 import com.android.systemui.shade.ShadeDisplayAware
70 import com.android.systemui.shade.domain.interactor.ShadeDialogContextInteractor
71 import com.android.systemui.shared.system.SysUiStatsLog
72 import com.android.systemui.statusbar.phone.SystemUIDialog
73 import com.android.systemui.util.DeviceConfigProxy
74 import com.android.systemui.util.indentIfPossible
75 import com.android.systemui.util.time.SystemClock
76 import java.io.PrintWriter
77 import java.util.Objects
78 import java.util.concurrent.Executor
79 import javax.inject.Inject
80 import kotlin.math.max
81 import kotlinx.coroutines.flow.MutableStateFlow
82 import kotlinx.coroutines.flow.StateFlow
83 import kotlinx.coroutines.flow.asStateFlow
84 
85 /** A controller for the dealing with services running in the foreground. */
86 interface FgsManagerController {
87 
88     /** The number of packages with a service running in the foreground. */
89     val numRunningPackages: Int
90 
91     /**
92      * Whether there were new changes to the foreground services since the last [shown][showDialog]
93      * dialog was dismissed.
94      */
95     val newChangesSinceDialogWasDismissed: Boolean
96 
97     /**
98      * Whether we should show a dot to indicate when [newChangesSinceDialogWasDismissed] is true.
99      */
100     val showFooterDot: StateFlow<Boolean>
101 
102     val includesUserVisibleJobs: Boolean
103 
104     /**
105      * Initialize this controller. This should be called once, before this controller is used for
106      * the first time.
107      */
108     fun init()
109 
110     /**
111      * Show the foreground services dialog. The dialog will be expanded from [expandable] if it's
112      * not `null`.
113      */
114     fun showDialog(expandable: Expandable?)
115 
116     /** Add a [OnNumberOfPackagesChangedListener]. */
117     fun addOnNumberOfPackagesChangedListener(listener: OnNumberOfPackagesChangedListener)
118 
119     /** Remove a [OnNumberOfPackagesChangedListener]. */
120     fun removeOnNumberOfPackagesChangedListener(listener: OnNumberOfPackagesChangedListener)
121 
122     /** Add a [OnDialogDismissedListener]. */
123     fun addOnDialogDismissedListener(listener: OnDialogDismissedListener)
124 
125     /** Remove a [OnDialogDismissedListener]. */
126     fun removeOnDialogDismissedListener(listener: OnDialogDismissedListener)
127 
128     @VisibleForTesting fun visibleButtonsCount(): Int
129 
130     interface OnNumberOfPackagesChangedListener {
131         /** Called when [numRunningPackages] changed. */
132         fun onNumberOfPackagesChanged(numPackages: Int)
133     }
134 
135     interface OnDialogDismissedListener {
136         /** Called when a dialog shown using [showDialog] was dismissed. */
137         fun onDialogDismissed()
138     }
139 }
140 
141 @SysUISingleton
142 class FgsManagerControllerImpl
143 @Inject
144 constructor(
145     @ShadeDisplayAware private val resources: Resources,
146     @Main private val mainExecutor: Executor,
147     @Background private val backgroundExecutor: Executor,
148     private val systemClock: SystemClock,
149     private val activityManager: IActivityManager,
150     private val jobScheduler: JobScheduler,
151     private val packageManager: PackageManager,
152     private val userTracker: UserTracker,
153     private val deviceConfigProxy: DeviceConfigProxy,
154     private val dialogTransitionAnimator: DialogTransitionAnimator,
155     private val broadcastDispatcher: BroadcastDispatcher,
156     private val dumpManager: DumpManager,
157     private val systemUIDialogFactory: SystemUIDialog.Factory,
158     private val shadeDialogContextRepository: ShadeDialogContextInteractor,
159 ) : Dumpable, FgsManagerController {
160 
161     companion object {
162         private const val INTERACTION_JANK_TAG = "active_background_apps"
163         private const val DEFAULT_TASK_MANAGER_SHOW_FOOTER_DOT = false
164         private const val DEFAULT_TASK_MANAGER_SHOW_STOP_BUTTON_FOR_USER_ALLOWLISTED_APPS = true
165         private const val DEFAULT_TASK_MANAGER_SHOW_USER_VISIBLE_JOBS = true
166         private const val DEFAULT_TASK_MANAGER_INFORM_JOB_SCHEDULER_OF_PENDING_APP_STOP = true
167     }
168 
169     override var newChangesSinceDialogWasDismissed = false
170         private set
171 
172     val _showFooterDot = MutableStateFlow(false)
173     override val showFooterDot: StateFlow<Boolean> = _showFooterDot.asStateFlow()
174 
175     private var showStopBtnForUserAllowlistedApps = false
176 
177     private var showUserVisibleJobs = DEFAULT_TASK_MANAGER_SHOW_USER_VISIBLE_JOBS
178 
179     private var informJobSchedulerOfPendingAppStop =
180         DEFAULT_TASK_MANAGER_INFORM_JOB_SCHEDULER_OF_PENDING_APP_STOP
181 
182     override val includesUserVisibleJobs: Boolean
183         get() = showUserVisibleJobs
184 
185     override val numRunningPackages: Int
186         get() {
<lambda>null187             synchronized(lock) {
188                 return getNumVisiblePackagesLocked()
189             }
190         }
191 
192     private val lock = Any()
193 
194     @GuardedBy("lock") var initialized = false
195 
196     @GuardedBy("lock") private var lastNumberOfVisiblePackages = 0
197 
198     @GuardedBy("lock") private var currentProfileIds = mutableSetOf<Int>()
199 
200     @GuardedBy("lock")
201     private val runningTaskIdentifiers = mutableMapOf<UserPackage, StartTimeAndIdentifiers>()
202 
203     @GuardedBy("lock") private var dialog: SystemUIDialog? = null
204 
205     @GuardedBy("lock") private val appListAdapter: AppListAdapter = AppListAdapter()
206 
207     /* Only mutate on the background thread */
208     private var runningApps: ArrayMap<UserPackage, RunningApp> = ArrayMap()
209 
210     private val userTrackerCallback =
211         object : UserTracker.Callback {
onUserChangednull212             override fun onUserChanged(newUser: Int, userContext: Context) {}
213 
onProfilesChangednull214             override fun onProfilesChanged(profiles: List<UserInfo>) {
215                 synchronized(lock) {
216                     currentProfileIds.clear()
217                     currentProfileIds.addAll(profiles.map { it.id })
218                     lastNumberOfVisiblePackages = 0
219                     updateNumberOfVisibleRunningPackagesLocked()
220                 }
221             }
222         }
223 
224     private val foregroundServiceObserver = ForegroundServiceObserver()
225 
226     private val userVisibleJobObserver = UserVisibleJobObserver()
227 
<lambda>null228     private val stoppableApps by lazy {
229         resources.getStringArray(com.android.internal.R.array.stoppable_fgs_system_apps)
230     }
231 
<lambda>null232     private val vendorStoppableApps by lazy {
233         resources.getStringArray(com.android.internal.R.array.vendor_stoppable_fgs_system_apps)
234     }
235 
initnull236     override fun init() {
237         synchronized(lock) {
238             if (initialized) {
239                 return
240             }
241 
242             showUserVisibleJobs =
243                 deviceConfigProxy.getBoolean(
244                     NAMESPACE_SYSTEMUI,
245                     TASK_MANAGER_SHOW_USER_VISIBLE_JOBS,
246                     DEFAULT_TASK_MANAGER_SHOW_USER_VISIBLE_JOBS,
247                 )
248 
249             informJobSchedulerOfPendingAppStop =
250                 deviceConfigProxy.getBoolean(
251                     NAMESPACE_SYSTEMUI,
252                     TASK_MANAGER_INFORM_JOB_SCHEDULER_OF_PENDING_APP_STOP,
253                     DEFAULT_TASK_MANAGER_INFORM_JOB_SCHEDULER_OF_PENDING_APP_STOP,
254                 )
255 
256             try {
257                 activityManager.registerForegroundServiceObserver(foregroundServiceObserver)
258                 // Clumping FGS and user-visible jobs here and showing a single entry and button
259                 // for them is the easiest way to get user-visible jobs showing in Task Manager.
260                 // Ideally, we would have dedicated UI in task manager for the user-visible jobs.
261                 // TODO(255768978): distinguish jobs from FGS and give users more control
262                 if (showUserVisibleJobs) {
263                     jobScheduler.registerUserVisibleJobObserver(userVisibleJobObserver)
264                 }
265             } catch (e: RemoteException) {
266                 e.rethrowFromSystemServer()
267             }
268 
269             userTracker.addCallback(userTrackerCallback, backgroundExecutor)
270 
271             currentProfileIds.addAll(userTracker.userProfiles.map { it.id })
272 
273             deviceConfigProxy.addOnPropertiesChangedListener(
274                 NAMESPACE_SYSTEMUI,
275                 backgroundExecutor,
276             ) {
277                 _showFooterDot.value =
278                     it.getBoolean(TASK_MANAGER_SHOW_FOOTER_DOT, _showFooterDot.value)
279                 showStopBtnForUserAllowlistedApps =
280                     it.getBoolean(
281                         TASK_MANAGER_SHOW_STOP_BUTTON_FOR_USER_ALLOWLISTED_APPS,
282                         showStopBtnForUserAllowlistedApps,
283                     )
284                 var wasShowingUserVisibleJobs = showUserVisibleJobs
285                 showUserVisibleJobs =
286                     it.getBoolean(TASK_MANAGER_SHOW_USER_VISIBLE_JOBS, showUserVisibleJobs)
287                 if (showUserVisibleJobs != wasShowingUserVisibleJobs) {
288                     onShowUserVisibleJobsFlagChanged()
289                 }
290                 informJobSchedulerOfPendingAppStop =
291                     it.getBoolean(
292                         TASK_MANAGER_SHOW_STOP_BUTTON_FOR_USER_ALLOWLISTED_APPS,
293                         informJobSchedulerOfPendingAppStop,
294                     )
295             }
296             _showFooterDot.value =
297                 deviceConfigProxy.getBoolean(
298                     NAMESPACE_SYSTEMUI,
299                     TASK_MANAGER_SHOW_FOOTER_DOT,
300                     DEFAULT_TASK_MANAGER_SHOW_FOOTER_DOT,
301                 )
302             showStopBtnForUserAllowlistedApps =
303                 deviceConfigProxy.getBoolean(
304                     NAMESPACE_SYSTEMUI,
305                     TASK_MANAGER_SHOW_STOP_BUTTON_FOR_USER_ALLOWLISTED_APPS,
306                     DEFAULT_TASK_MANAGER_SHOW_STOP_BUTTON_FOR_USER_ALLOWLISTED_APPS,
307                 )
308 
309             dumpManager.registerDumpable(this)
310 
311             broadcastDispatcher.registerReceiver(
312                 object : BroadcastReceiver() {
313                     override fun onReceive(context: Context, intent: Intent) {
314                         if (intent.action == Intent.ACTION_SHOW_FOREGROUND_SERVICE_MANAGER) {
315                             showDialog(null)
316                         }
317                     }
318                 },
319                 IntentFilter(Intent.ACTION_SHOW_FOREGROUND_SERVICE_MANAGER),
320                 executor = mainExecutor,
321                 flags = Context.RECEIVER_NOT_EXPORTED,
322             )
323 
324             initialized = true
325         }
326     }
327 
328     @GuardedBy("lock")
329     private val onNumberOfPackagesChangedListeners =
330         mutableSetOf<FgsManagerController.OnNumberOfPackagesChangedListener>()
331 
332     @GuardedBy("lock")
333     private val onDialogDismissedListeners =
334         mutableSetOf<FgsManagerController.OnDialogDismissedListener>()
335 
addOnNumberOfPackagesChangedListenernull336     override fun addOnNumberOfPackagesChangedListener(
337         listener: FgsManagerController.OnNumberOfPackagesChangedListener
338     ) {
339         synchronized(lock) { onNumberOfPackagesChangedListeners.add(listener) }
340     }
341 
removeOnNumberOfPackagesChangedListenernull342     override fun removeOnNumberOfPackagesChangedListener(
343         listener: FgsManagerController.OnNumberOfPackagesChangedListener
344     ) {
345         synchronized(lock) { onNumberOfPackagesChangedListeners.remove(listener) }
346     }
347 
addOnDialogDismissedListenernull348     override fun addOnDialogDismissedListener(
349         listener: FgsManagerController.OnDialogDismissedListener
350     ) {
351         synchronized(lock) { onDialogDismissedListeners.add(listener) }
352     }
353 
removeOnDialogDismissedListenernull354     override fun removeOnDialogDismissedListener(
355         listener: FgsManagerController.OnDialogDismissedListener
356     ) {
357         synchronized(lock) { onDialogDismissedListeners.remove(listener) }
358     }
359 
getNumVisiblePackagesLockednull360     private fun getNumVisiblePackagesLocked(): Int {
361         return runningTaskIdentifiers.keys.count {
362             it.uiControl != UIControl.HIDE_ENTRY && currentProfileIds.contains(it.userId)
363         }
364     }
365 
updateNumberOfVisibleRunningPackagesLockednull366     private fun updateNumberOfVisibleRunningPackagesLocked() {
367         val num = getNumVisiblePackagesLocked()
368         if (num != lastNumberOfVisiblePackages) {
369             lastNumberOfVisiblePackages = num
370             newChangesSinceDialogWasDismissed = true
371             onNumberOfPackagesChangedListeners.forEach {
372                 backgroundExecutor.execute { it.onNumberOfPackagesChanged(num) }
373             }
374         }
375     }
376 
visibleButtonsCountnull377     override fun visibleButtonsCount(): Int {
378         synchronized(lock) {
379             return getNumVisibleButtonsLocked()
380         }
381     }
382 
getNumVisibleButtonsLockednull383     private fun getNumVisibleButtonsLocked(): Int {
384         return runningTaskIdentifiers.keys.count {
385             it.uiControl != UIControl.HIDE_BUTTON && currentProfileIds.contains(it.userId)
386         }
387     }
388 
showDialognull389     override fun showDialog(expandable: Expandable?) {
390         synchronized(lock) {
391             if (dialog == null) {
392                 val dialog = systemUIDialogFactory.create(shadeDialogContextRepository.context)
393                 dialog.setTitle(R.string.fgs_manager_dialog_title)
394                 dialog.setMessage(R.string.fgs_manager_dialog_message)
395 
396                 val dialogContext = dialog.context
397 
398                 val recyclerView = RecyclerView(dialogContext)
399                 recyclerView.layoutManager = LinearLayoutManager(dialogContext)
400                 recyclerView.adapter = appListAdapter
401 
402                 val topSpacing =
403                     dialogContext.resources.getDimensionPixelSize(
404                         R.dimen.fgs_manager_list_top_spacing
405                     )
406                 dialog.setView(recyclerView, 0, topSpacing, 0, 0)
407 
408                 this.dialog = dialog
409 
410                 dialog.setOnDismissListener {
411                     newChangesSinceDialogWasDismissed = false
412                     synchronized(lock) {
413                         this.dialog = null
414                         updateAppItemsLocked()
415                     }
416                     onDialogDismissedListeners.forEach {
417                         mainExecutor.execute(it::onDialogDismissed)
418                     }
419                 }
420 
421                 mainExecutor.execute {
422                     val controller =
423                         expandable?.dialogTransitionController(
424                             DialogCuj(
425                                 InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN,
426                                 INTERACTION_JANK_TAG,
427                             )
428                         )
429                     if (controller != null) {
430                         dialogTransitionAnimator.show(dialog, controller)
431                     } else {
432                         dialog.show()
433                     }
434                 }
435 
436                 updateAppItemsLocked(refreshUiControls = true)
437             }
438         }
439     }
440 
441     @GuardedBy("lock")
updateAppItemsLockednull442     private fun updateAppItemsLocked(refreshUiControls: Boolean = false) {
443         if (dialog == null) {
444             backgroundExecutor.execute { clearRunningApps() }
445             return
446         }
447 
448         val packagesToStartTime = runningTaskIdentifiers.mapValues { it.value.startTime }
449         val profileIds = currentProfileIds.toSet()
450         backgroundExecutor.execute {
451             updateAppItems(packagesToStartTime, profileIds, refreshUiControls)
452         }
453     }
454 
455     /** Must be called on the background thread. */
456     @WorkerThread
updateAppItemsnull457     private fun updateAppItems(
458         packages: Map<UserPackage, Long>,
459         profileIds: Set<Int>,
460         refreshUiControls: Boolean = true,
461     ) {
462         if (refreshUiControls) {
463             packages.forEach { (pkg, _) -> pkg.updateUiControl() }
464         }
465 
466         val addedPackages =
467             packages.keys.filter {
468                 profileIds.contains(it.userId) &&
469                     it.uiControl != UIControl.HIDE_ENTRY &&
470                     runningApps[it]?.stopped != true
471             }
472         val removedPackages = runningApps.keys.filter { it !in packages }
473 
474         addedPackages.forEach {
475             val ai = packageManager.getApplicationInfoAsUser(it.packageName, 0, it.userId)
476             runningApps[it] =
477                 RunningApp(
478                     it.userId,
479                     it.packageName,
480                     packages[it]!!,
481                     it.uiControl,
482                     packageManager.getApplicationLabel(ai),
483                     packageManager.getUserBadgedIcon(
484                         packageManager.getApplicationIcon(ai),
485                         UserHandle.of(it.userId),
486                     ),
487                 )
488             logEvent(stopped = false, it.packageName, it.userId, runningApps[it]!!.timeStarted)
489         }
490 
491         removedPackages.forEach { pkg ->
492             val ra = runningApps[pkg]!!
493             val ra2 =
494                 ra.copy().also {
495                     it.stopped = true
496                     it.appLabel = ra.appLabel
497                     it.icon = ra.icon
498                 }
499             runningApps[pkg] = ra2
500         }
501 
502         mainExecutor.execute {
503             appListAdapter.setData(
504                 runningApps.values.toList().sortedByDescending { it.timeStarted }
505             )
506         }
507     }
508 
509     /** Must be called on the background thread. */
510     @WorkerThread
clearRunningAppsnull511     private fun clearRunningApps() {
512         runningApps.clear()
513     }
514 
stopPackagenull515     private fun stopPackage(userId: Int, packageName: String, timeStarted: Long) {
516         logEvent(stopped = true, packageName, userId, timeStarted)
517         val userPackageKey = UserPackage(userId, packageName)
518         if (showUserVisibleJobs || informJobSchedulerOfPendingAppStop) {
519             // TODO(255768978): allow fine-grained job control
520             jobScheduler.notePendingUserRequestedAppStop(packageName, userId, "task manager")
521         }
522         activityManager.stopAppForUser(packageName, userId)
523     }
524 
onShowUserVisibleJobsFlagChangednull525     private fun onShowUserVisibleJobsFlagChanged() {
526         if (showUserVisibleJobs) {
527             jobScheduler.registerUserVisibleJobObserver(userVisibleJobObserver)
528         } else {
529             jobScheduler.unregisterUserVisibleJobObserver(userVisibleJobObserver)
530 
531             synchronized(lock) {
532                 for ((userPackage, startTimeAndIdentifiers) in runningTaskIdentifiers) {
533                     if (startTimeAndIdentifiers.hasFgs()) {
534                         // The app still has FGS running, so all we need to do is remove
535                         // the job summaries
536                         startTimeAndIdentifiers.clearJobSummaries()
537                     } else {
538                         // The app only has user-visible jobs running, so remove it from
539                         // the map altogether
540                         runningTaskIdentifiers.remove(userPackage)
541                     }
542                 }
543 
544                 updateNumberOfVisibleRunningPackagesLocked()
545 
546                 updateAppItemsLocked()
547             }
548         }
549     }
550 
logEventnull551     private fun logEvent(stopped: Boolean, packageName: String, userId: Int, timeStarted: Long) {
552         val timeLogged = systemClock.elapsedRealtime()
553         val event =
554             if (stopped) {
555                 SysUiStatsLog.TASK_MANAGER_EVENT_REPORTED__EVENT__STOPPED
556             } else {
557                 SysUiStatsLog.TASK_MANAGER_EVENT_REPORTED__EVENT__VIEWED
558             }
559         backgroundExecutor.execute {
560             val uid = packageManager.getPackageUidAsUser(packageName, userId)
561             SysUiStatsLog.write(
562                 SysUiStatsLog.TASK_MANAGER_EVENT_REPORTED,
563                 uid,
564                 event,
565                 timeLogged - timeStarted,
566             )
567         }
568     }
569 
570     private inner class AppListAdapter : RecyclerView.Adapter<AppItemViewHolder>() {
571         private val lock = Any()
572 
573         @GuardedBy("lock") private var data: List<RunningApp> = listOf()
574 
onCreateViewHoldernull575         override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AppItemViewHolder {
576             return AppItemViewHolder(
577                 LayoutInflater.from(parent.context)
578                     .inflate(R.layout.fgs_manager_app_item, parent, false)
579             )
580         }
581 
onBindViewHoldernull582         override fun onBindViewHolder(holder: AppItemViewHolder, position: Int) {
583             var runningApp: RunningApp
584             synchronized(lock) { runningApp = data[position] }
585             with(holder) {
586                 iconView.setImageDrawable(runningApp.icon)
587                 appLabelView.text = runningApp.appLabel
588                 durationView.text =
589                     DateUtils.formatDuration(
590                         max(systemClock.elapsedRealtime() - runningApp.timeStarted, 60000),
591                         DateUtils.LENGTH_MEDIUM,
592                     )
593                 stopButton.setOnClickListener {
594                     stopButton.setText(R.string.fgs_manager_app_item_stop_button_stopped_label)
595                     stopPackage(runningApp.userId, runningApp.packageName, runningApp.timeStarted)
596                 }
597                 if (runningApp.uiControl == UIControl.HIDE_BUTTON) {
598                     stopButton.visibility = View.INVISIBLE
599                 }
600                 if (runningApp.stopped) {
601                     stopButton.isEnabled = false
602                     stopButton.setText(R.string.fgs_manager_app_item_stop_button_stopped_label)
603                     durationView.visibility = View.GONE
604                 } else {
605                     stopButton.isEnabled = true
606                     stopButton.setText(R.string.fgs_manager_app_item_stop_button_label)
607                     durationView.visibility = View.VISIBLE
608                 }
609             }
610         }
611 
getItemCountnull612         override fun getItemCount(): Int {
613             return data.size
614         }
615 
setDatanull616         fun setData(newData: List<RunningApp>) {
617             var oldData = data
618             data = newData
619 
620             DiffUtil.calculateDiff(
621                     object : DiffUtil.Callback() {
622                         override fun getOldListSize(): Int {
623                             return oldData.size
624                         }
625 
626                         override fun getNewListSize(): Int {
627                             return newData.size
628                         }
629 
630                         override fun areItemsTheSame(
631                             oldItemPosition: Int,
632                             newItemPosition: Int,
633                         ): Boolean {
634                             return oldData[oldItemPosition] == newData[newItemPosition]
635                         }
636 
637                         override fun areContentsTheSame(
638                             oldItemPosition: Int,
639                             newItemPosition: Int,
640                         ): Boolean {
641                             return oldData[oldItemPosition].stopped ==
642                                 newData[newItemPosition].stopped
643                         }
644                     }
645                 )
646                 .dispatchUpdatesTo(this)
647         }
648     }
649 
650     private inner class ForegroundServiceObserver : IForegroundServiceObserver.Stub() {
onForegroundStateChangednull651         override fun onForegroundStateChanged(
652             token: IBinder,
653             packageName: String,
654             userId: Int,
655             isForeground: Boolean,
656         ) {
657             synchronized(lock) {
658                 val userPackageKey = UserPackage(userId, packageName)
659                 if (isForeground) {
660                     runningTaskIdentifiers
661                         .getOrPut(userPackageKey) { StartTimeAndIdentifiers(systemClock) }
662                         .addFgsToken(token)
663                 } else {
664                     if (
665                         runningTaskIdentifiers[userPackageKey]
666                             ?.also { it.removeFgsToken(token) }
667                             ?.isEmpty() == true
668                     ) {
669                         runningTaskIdentifiers.remove(userPackageKey)
670                     }
671                 }
672 
673                 updateNumberOfVisibleRunningPackagesLocked()
674 
675                 updateAppItemsLocked()
676             }
677         }
678     }
679 
680     private inner class UserVisibleJobObserver : IUserVisibleJobObserver.Stub() {
onUserVisibleJobStateChangednull681         override fun onUserVisibleJobStateChanged(
682             summary: UserVisibleJobSummary,
683             isRunning: Boolean,
684         ) {
685             synchronized(lock) {
686                 val userPackageKey =
687                     UserPackage(
688                         UserHandle.getUserId(summary.callingUid),
689                         summary.callingPackageName,
690                     )
691                 if (isRunning) {
692                     runningTaskIdentifiers
693                         .getOrPut(userPackageKey) { StartTimeAndIdentifiers(systemClock) }
694                         .addJobSummary(summary)
695                 } else {
696                     if (
697                         runningTaskIdentifiers[userPackageKey]
698                             ?.also { it.removeJobSummary(summary) }
699                             ?.isEmpty() == true
700                     ) {
701                         runningTaskIdentifiers.remove(userPackageKey)
702                     }
703                 }
704 
705                 updateNumberOfVisibleRunningPackagesLocked()
706 
707                 updateAppItemsLocked()
708             }
709         }
710     }
711 
712     private inner class UserPackage(val userId: Int, val packageName: String) {
<lambda>null713         val uid by lazy { packageManager.getPackageUidAsUser(packageName, userId) }
714         var backgroundRestrictionExemptionReason = PowerExemptionManager.REASON_DENIED
715 
716         private var uiControlInitialized = false
717         var uiControl: UIControl = UIControl.NORMAL
718             get() {
719                 if (!uiControlInitialized) {
720                     updateUiControl()
721                 }
722                 return field
723             }
724             private set
725 
updateUiControlnull726         fun updateUiControl() {
727             backgroundRestrictionExemptionReason =
728                 activityManager.getBackgroundRestrictionExemptionReason(uid)
729             uiControl =
730                 when (backgroundRestrictionExemptionReason) {
731                     PowerExemptionManager.REASON_SYSTEM_UID,
732                     PowerExemptionManager.REASON_DEVICE_DEMO_MODE -> UIControl.HIDE_ENTRY
733 
734                     PowerExemptionManager.REASON_SYSTEM_ALLOW_LISTED,
735                     PowerExemptionManager.REASON_DEVICE_OWNER,
736                     PowerExemptionManager.REASON_DISALLOW_APPS_CONTROL,
737                     PowerExemptionManager.REASON_DPO_PROTECTED_APP,
738                     PowerExemptionManager.REASON_PROFILE_OWNER,
739                     PowerExemptionManager.REASON_ACTIVE_DEVICE_ADMIN,
740                     PowerExemptionManager.REASON_PROC_STATE_PERSISTENT,
741                     PowerExemptionManager.REASON_PROC_STATE_PERSISTENT_UI,
742                     PowerExemptionManager.REASON_ROLE_DIALER,
743                     PowerExemptionManager.REASON_SYSTEM_MODULE,
744                     PowerExemptionManager.REASON_SYSTEM_EXEMPT_APP_OP -> UIControl.HIDE_BUTTON
745 
746                     PowerExemptionManager.REASON_ALLOWLISTED_PACKAGE ->
747                         if (showStopBtnForUserAllowlistedApps) {
748                             UIControl.NORMAL
749                         } else {
750                             UIControl.HIDE_BUTTON
751                         }
752                     else -> UIControl.NORMAL
753                 }
754             // If the app wants to be a good citizen by being stoppable, even if the category it
755             // belongs to is exempted for background restriction, let it be stoppable by user.
756             if (Flags.stoppableFgsSystemApp()) {
757                 if (isStoppableApp(packageName)) {
758                     uiControl = UIControl.NORMAL
759                 }
760             }
761 
762             uiControlInitialized = true
763         }
764 
isStoppableAppnull765         fun isStoppableApp(packageName: String): Boolean {
766             return stoppableApps.contains(packageName) || vendorStoppableApps.contains(packageName)
767         }
768 
equalsnull769         override fun equals(other: Any?): Boolean {
770             if (other !is UserPackage) {
771                 return false
772             }
773             return other.packageName == packageName && other.userId == userId
774         }
775 
hashCodenull776         override fun hashCode(): Int = Objects.hash(userId, packageName)
777 
778         fun dump(pw: PrintWriter) {
779             pw.println("UserPackage: [")
780             pw.indentIfPossible {
781                 pw.println("userId=$userId")
782                 pw.println("packageName=$packageName")
783                 pw.println("uiControl=$uiControl (reason=$backgroundRestrictionExemptionReason)")
784             }
785             pw.println("]")
786         }
787     }
788 
789     private data class StartTimeAndIdentifiers(val systemClock: SystemClock) {
790         val startTime = systemClock.elapsedRealtime()
791         val fgsTokens = mutableSetOf<IBinder>()
792         val jobSummaries = mutableSetOf<UserVisibleJobSummary>()
793 
addJobSummarynull794         fun addJobSummary(summary: UserVisibleJobSummary) {
795             jobSummaries.add(summary)
796         }
797 
clearJobSummariesnull798         fun clearJobSummaries() {
799             jobSummaries.clear()
800         }
801 
removeJobSummarynull802         fun removeJobSummary(summary: UserVisibleJobSummary) {
803             jobSummaries.remove(summary)
804         }
805 
addFgsTokennull806         fun addFgsToken(token: IBinder) {
807             fgsTokens.add(token)
808         }
809 
removeFgsTokennull810         fun removeFgsToken(token: IBinder) {
811             fgsTokens.remove(token)
812         }
813 
hasFgsnull814         fun hasFgs(): Boolean {
815             return !fgsTokens.isEmpty()
816         }
817 
hasRunningJobsnull818         fun hasRunningJobs(): Boolean {
819             return !jobSummaries.isEmpty()
820         }
821 
isEmptynull822         fun isEmpty(): Boolean {
823             return fgsTokens.isEmpty() && jobSummaries.isEmpty()
824         }
825 
dumpnull826         fun dump(pw: PrintWriter) {
827             pw.println("StartTimeAndIdentifiers: [")
828             pw.indentIfPossible {
829                 pw.println(
830                     "startTime=$startTime (time running =" +
831                         " ${systemClock.elapsedRealtime() - startTime}ms)"
832                 )
833                 pw.println("fgs tokens: [")
834                 pw.indentIfPossible {
835                     for (token in fgsTokens) {
836                         pw.println("$token")
837                     }
838                 }
839                 pw.println("job summaries: [")
840                 pw.indentIfPossible {
841                     for (summary in jobSummaries) {
842                         pw.println("$summary")
843                     }
844                 }
845                 pw.println("]")
846             }
847             pw.println("]")
848         }
849     }
850 
851     private class AppItemViewHolder(parent: View) : RecyclerView.ViewHolder(parent) {
852         val appLabelView: TextView = parent.requireViewById(R.id.fgs_manager_app_item_label)
853         val durationView: TextView = parent.requireViewById(R.id.fgs_manager_app_item_duration)
854         val iconView: ImageView = parent.requireViewById(R.id.fgs_manager_app_item_icon)
855         val stopButton: Button = parent.requireViewById(R.id.fgs_manager_app_item_stop_button)
856     }
857 
858     private data class RunningApp(
859         val userId: Int,
860         val packageName: String,
861         val timeStarted: Long,
862         val uiControl: UIControl,
863     ) {
864         constructor(
865             userId: Int,
866             packageName: String,
867             timeStarted: Long,
868             uiControl: UIControl,
869             appLabel: CharSequence,
870             icon: Drawable,
871         ) : this(userId, packageName, timeStarted, uiControl) {
872             this.appLabel = appLabel
873             this.icon = icon
874         }
875 
876         // variables to keep out of the generated equals()
877         var appLabel: CharSequence = ""
878         var icon: Drawable? = null
879         var stopped = false
880 
dumpnull881         fun dump(pw: PrintWriter, systemClock: SystemClock) {
882             pw.println("RunningApp: [")
883             pw.indentIfPossible {
884                 pw.println("userId=$userId")
885                 pw.println("packageName=$packageName")
886                 pw.println(
887                     "timeStarted=$timeStarted (time since start =" +
888                         " ${systemClock.elapsedRealtime() - timeStarted}ms)"
889                 )
890                 pw.println("uiControl=$uiControl")
891                 pw.println("appLabel=$appLabel")
892                 pw.println("icon=$icon")
893                 pw.println("stopped=$stopped")
894             }
895             pw.println("]")
896         }
897     }
898 
899     private enum class UIControl {
900         NORMAL,
901         HIDE_BUTTON,
902         HIDE_ENTRY,
903     }
904 
dumpnull905     override fun dump(printwriter: PrintWriter, args: Array<out String>) {
906         val pw = IndentingPrintWriter(printwriter)
907         synchronized(lock) {
908             pw.println("current user profiles = $currentProfileIds")
909             pw.println("newChangesSinceDialogWasShown=$newChangesSinceDialogWasDismissed")
910             pw.println("Running task identifiers: [")
911             pw.indentIfPossible {
912                 runningTaskIdentifiers.forEach { (userPackage, startTimeAndIdentifiers) ->
913                     pw.println("{")
914                     pw.indentIfPossible {
915                         userPackage.dump(pw)
916                         startTimeAndIdentifiers.dump(pw)
917                     }
918                     pw.println("}")
919                 }
920             }
921             pw.println("]")
922 
923             pw.println("Loaded package UI info: [")
924             pw.indentIfPossible {
925                 runningApps.forEach { (userPackage, runningApp) ->
926                     pw.println("{")
927                     pw.indentIfPossible {
928                         userPackage.dump(pw)
929                         runningApp.dump(pw, systemClock)
930                     }
931                     pw.println("}")
932                 }
933             }
934             pw.println("]")
935         }
936     }
937 }
938