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