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