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