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