1 /* <lambda>null2 * Copyright (C) 2024 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.car.carlaunchercommon.shortcuts 18 19 import android.app.Activity 20 import android.app.ActivityManager 21 import android.app.admin.DevicePolicyManager 22 import android.car.media.CarMediaManager 23 import android.content.ComponentName 24 import android.content.Context 25 import android.content.pm.ApplicationInfo 26 import android.content.pm.PackageManager 27 import android.os.UserHandle 28 import android.os.UserManager 29 import android.util.Log 30 import android.view.WindowManager 31 import android.widget.Toast 32 import androidx.annotation.VisibleForTesting 33 import com.android.car.carlaunchercommon.R 34 import com.android.car.hidden.apis.HiddenApiAccess.hasBaseUserRestriction 35 import com.android.car.hidden.apis.HiddenApiAccess.isDebuggable 36 import com.android.car.ui.AlertDialogBuilder 37 import com.android.car.ui.shortcutspopup.CarUiShortcutsPopup 38 39 /** 40 * @property context the [Context] for the user that the app is running in. 41 * @property mediaServiceComponents list of [ComponentName] of the services the adhere to the media 42 * service interface 43 */ 44 open class ForceStopShortcutItem( 45 private val context: Context, 46 private val packageName: String, 47 private val displayName: CharSequence, 48 private val carMediaManager: CarMediaManager?, 49 private val mediaServiceComponents: Set<ComponentName> 50 ) : CarUiShortcutsPopup.ShortcutItem { 51 // todo(b/312718542): hidden class(CarMediaManager) usage 52 53 companion object { 54 private const val TAG = "ForceStopShortcutItem" 55 private val DEBUG = isDebuggable() 56 } 57 58 override fun data(): CarUiShortcutsPopup.ItemData { 59 return CarUiShortcutsPopup.ItemData( 60 R.drawable.ic_force_stop_caution_icon, 61 context.resources.getString( 62 R.string.stop_app_shortcut_label 63 ) 64 ) 65 } 66 67 override fun onClick(): Boolean { 68 val builder = getAlertDialogBuilder(context) 69 .setTitle(R.string.stop_app_dialog_title) 70 .setMessage(R.string.stop_app_dialog_text) 71 .setPositiveButton(android.R.string.ok) { _, _ -> 72 forceStop(packageName, displayName) 73 } 74 .setNegativeButton( 75 android.R.string.cancel, 76 null // listener 77 ) 78 builder.create().let { 79 if (context !is Activity || context.window.decorView.windowToken == null) { 80 // If the context is not an Activity or lacks a valid window token, 81 // it's likely we're in a non-Activity context (e.g., Service, SystemUI). 82 // To ensure the AlertDialog is displayed properly, we explicitly set its window 83 // type to SYSTEM_ALERT, allowing it to overlay other windows, even from SystemUI. 84 it.window?.setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT) 85 } 86 it.show() 87 } 88 return true 89 } 90 91 override fun isEnabled(): Boolean { 92 return shouldAllowStopApp(packageName) 93 } 94 95 /** 96 * @param packageName name of the package to stop the app 97 * @return true if an app should show the Stop app action 98 */ 99 private fun shouldAllowStopApp(packageName: String): Boolean { 100 val dm = context.getSystemService(DevicePolicyManager::class.java) 101 if (dm == null || dm.packageHasActiveAdmins(packageName)) { 102 return false 103 } 104 try { 105 val appInfo = context.packageManager.getApplicationInfo( 106 packageName, 107 PackageManager.ApplicationInfoFlags.of(PackageManager.GET_META_DATA.toLong()) 108 ) 109 // Show only if the User has no restrictions to force stop this app 110 if (hasUserRestriction(appInfo)) { 111 return false 112 } 113 // Show only if the app is running 114 if (appInfo.flags and ApplicationInfo.FLAG_STOPPED == 0) { 115 return true 116 } 117 } catch (e: PackageManager.NameNotFoundException) { 118 if (DEBUG) Log.d(TAG, "shouldAllowStopApp() package $packageName was not found") 119 } 120 return false 121 } 122 123 /** 124 * @return true if the user has restrictions to force stop an app with `appInfo` 125 */ 126 private fun hasUserRestriction(appInfo: ApplicationInfo): Boolean { 127 val restriction = UserManager.DISALLOW_APPS_CONTROL 128 val userManager = context.getSystemService(UserManager::class.java) 129 if (userManager == null) { 130 if (DEBUG) Log.e(TAG, " Disabled because UserManager is null") 131 return true 132 } 133 if (!userManager.hasUserRestriction(restriction)) { 134 return false 135 } 136 val user = UserHandle.getUserHandleForUid(appInfo.uid) 137 if (hasBaseUserRestriction(userManager, restriction, user)) { 138 if (DEBUG) Log.d(TAG, " Disabled because $user has $restriction restriction") 139 return true 140 } 141 // Not disabled for this User 142 return false 143 } 144 145 /** 146 * Force stops an app 147 */ 148 @VisibleForTesting 149 fun forceStop(packageName: String, displayName: CharSequence) { 150 // Both MEDIA_SOURCE_MODE_BROWSE and MEDIA_SOURCE_MODE_PLAYBACK should be replaced to their 151 // previous available values 152 maybeReplaceMediaSource(packageName, CarMediaManager.MEDIA_SOURCE_MODE_BROWSE) 153 maybeReplaceMediaSource(packageName, CarMediaManager.MEDIA_SOURCE_MODE_PLAYBACK) 154 155 val activityManager = context.getSystemService(ActivityManager::class.java) ?: return 156 // todo(b/312718542): hidden api(ActivityManager.forceStopPackage) usage 157 activityManager.forceStopPackage(packageName) 158 val message = context.resources.getString(R.string.stop_app_success_toast_text, displayName) 159 createToast(context, message, Toast.LENGTH_LONG).show() 160 } 161 162 /** 163 * Updates the MediaSource to second most recent if [packageName] is current media source. 164 * @param mode media source mode (ex. [CarMediaManager.MEDIA_SOURCE_MODE_BROWSE]) 165 */ 166 private fun maybeReplaceMediaSource(packageName: String, mode: Int) { 167 if (!isCurrentMediaSource(packageName, mode)) { 168 if (DEBUG) Log.e(TAG, "Not current media source") 169 return 170 } 171 // find the most recent source from history not equal to force-stopping package 172 val mediaSources = carMediaManager?.getLastMediaSources(mode) 173 var componentName = mediaSources?.firstOrNull { it?.packageName != packageName } 174 if (componentName == null) { 175 // no recent package found, find from all available media services. 176 componentName = mediaServiceComponents.firstOrNull { it.packageName != packageName } 177 } 178 if (componentName == null) { 179 if (DEBUG) Log.e(TAG, "Stop-app, no alternative media service found") 180 return 181 } 182 carMediaManager?.setMediaSource(componentName, mode) 183 } 184 185 private fun isCurrentMediaSource(packageName: String, mode: Int): Boolean { 186 val componentName = carMediaManager?.getMediaSource(mode) 187 ?: return false // There is no current media source. 188 if (DEBUG) Log.e(TAG, "isCurrentMediaSource: $packageName, $componentName") 189 return componentName.packageName == packageName 190 } 191 192 /** 193 * Should be overridden in the test to provide a mock [AlertDialogBuilder] 194 */ 195 @VisibleForTesting 196 open fun getAlertDialogBuilder(context: Context) = AlertDialogBuilder(context) 197 198 /** 199 * Should be overridden in the test to provide a mock [Toast] 200 */ 201 @VisibleForTesting 202 open fun createToast(context: Context, text: CharSequence, duration: Int): Toast = 203 Toast.makeText(context, text, duration) 204 } 205