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