1 /*
2  * 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 android.permissionui.cts
18 
19 import android.Manifest
20 import android.app.AppOpsManager
21 import android.app.Instrumentation
22 import android.app.ecm.EnhancedConfirmationManager
23 import android.content.Context
24 import android.content.pm.PackageManager
25 import android.os.Build
26 import android.os.Process
27 import android.permission.flags.Flags
28 import android.platform.test.annotations.AppModeFull
29 import android.platform.test.annotations.RequiresFlagsEnabled
30 import android.platform.test.flag.junit.CheckFlagsRule
31 import android.platform.test.flag.junit.DeviceFlagsValueProvider
32 import androidx.test.filters.SdkSuppress
33 import androidx.test.platform.app.InstrumentationRegistry
34 import androidx.test.uiautomator.By
35 import com.android.compatibility.common.util.CddTest
36 import com.android.compatibility.common.util.SystemUtil.callWithShellPermissionIdentity
37 import com.android.compatibility.common.util.SystemUtil.eventually
38 import com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity
39 import org.junit.Assert.assertEquals
40 import org.junit.Assert.assertFalse
41 import org.junit.Assert.assertNotNull
42 import org.junit.Assert.assertNull
43 import org.junit.Assert.assertTrue
44 import org.junit.Assume
45 import org.junit.Before
46 import org.junit.Rule
47 import org.junit.Test
48 
49 @AppModeFull(reason = "Instant apps cannot install packages")
50 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM, codeName = "VanillaIceCream")
51 @RequiresFlagsEnabled(Flags.FLAG_ENHANCED_CONFIRMATION_MODE_APIS_ENABLED)
52 @CddTest(requirement = "9.18/C-0-1")
53 class EnhancedConfirmationManagerTest : BaseUsePermissionTest() {
54     private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
55     private val context: Context = instrumentation.targetContext
<lambda>null56     private val ecm by lazy { context.getSystemService(EnhancedConfirmationManager::class.java)!! }
<lambda>null57     private val appOpsManager by lazy { context.getSystemService(AppOpsManager::class.java)!! }
<lambda>null58     private val packageManager by lazy { context.packageManager }
59 
60     @Rule
61     @JvmField
62     val mCheckFlagsRule: CheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule()
63 
64     @Before
assumeNotAutoTvOrWearnull65     fun assumeNotAutoTvOrWear() {
66         Assume.assumeFalse(packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK))
67         Assume.assumeFalse(
68             packageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)
69         )
70         Assume.assumeFalse(packageManager.hasSystemFeature(PackageManager.FEATURE_WATCH))
71     }
72 
73     @RequiresFlagsEnabled(Flags.FLAG_ENHANCED_CONFIRMATION_MODE_APIS_ENABLED)
74     @Test
installedAppStartsWithModeDefaultnull75     fun installedAppStartsWithModeDefault() {
76         installPackageWithInstallSourceAndMetadataFromStore(APP_APK_NAME_LATEST)
77         eventually {
78             runWithShellPermissionIdentity {
79                 assertEquals(
80                     getAppEcmState(context, appOpsManager, APP_PACKAGE_NAME),
81                     AppOpsManager.MODE_DEFAULT
82                 )
83             }
84         }
85     }
86 
87     @RequiresFlagsEnabled(Flags.FLAG_ENHANCED_CONFIRMATION_MODE_APIS_ENABLED)
88     @Test
givenStoreAppThenIsNotRestrictedFromProtectedSettingnull89     fun givenStoreAppThenIsNotRestrictedFromProtectedSetting() {
90         installPackageWithInstallSourceAndMetadataFromStore(APP_APK_NAME_LATEST)
91         runWithShellPermissionIdentity {
92             eventually { assertFalse(ecm.isRestricted(APP_PACKAGE_NAME, PROTECTED_SETTING)) }
93         }
94     }
95 
96     @RequiresFlagsEnabled(Flags.FLAG_ENHANCED_CONFIRMATION_MODE_APIS_ENABLED)
97     @Test
givenLocalAppThenIsRestrictedFromProtectedSettingnull98     fun givenLocalAppThenIsRestrictedFromProtectedSetting() {
99         installPackageWithInstallSourceAndMetadataFromLocalFile(APP_APK_NAME_LATEST)
100         runWithShellPermissionIdentity {
101             eventually { assertTrue(ecm.isRestricted(APP_PACKAGE_NAME, PROTECTED_SETTING)) }
102         }
103     }
104 
105     @RequiresFlagsEnabled(Flags.FLAG_ENHANCED_CONFIRMATION_MODE_APIS_ENABLED)
106     @Test
givenDownloadedThenAppIsRestrictedFromProtectedSettingnull107     fun givenDownloadedThenAppIsRestrictedFromProtectedSetting() {
108         installPackageWithInstallSourceAndMetadataFromDownloadedFile(APP_APK_NAME_LATEST)
109         runWithShellPermissionIdentity {
110             eventually { assertTrue(ecm.isRestricted(APP_PACKAGE_NAME, PROTECTED_SETTING)) }
111         }
112     }
113 
114     @RequiresFlagsEnabled(Flags.FLAG_ENHANCED_CONFIRMATION_MODE_APIS_ENABLED)
115     @Test
givenExplicitlyRestrictedAppThenIsRestrictedFromProtectedSettingnull116     fun givenExplicitlyRestrictedAppThenIsRestrictedFromProtectedSetting() {
117         installPackageWithInstallSourceAndMetadataFromStore(APP_APK_NAME_LATEST)
118         eventually {
119             runWithShellPermissionIdentity {
120                 assertEquals(
121                     getAppEcmState(context, appOpsManager, APP_PACKAGE_NAME),
122                     AppOpsManager.MODE_DEFAULT
123                 )
124             }
125         }
126         runWithShellPermissionIdentity {
127             eventually { assertFalse(ecm.isRestricted(APP_PACKAGE_NAME, PROTECTED_SETTING)) }
128             setAppEcmState(context, appOpsManager, APP_PACKAGE_NAME, AppOpsManager.MODE_ERRORED)
129             eventually { assertTrue(ecm.isRestricted(APP_PACKAGE_NAME, PROTECTED_SETTING)) }
130         }
131     }
132 
133     @RequiresFlagsEnabled(Flags.FLAG_ENHANCED_CONFIRMATION_MODE_APIS_ENABLED)
134     @Test
givenRestrictedAppThenIsNotRestrictedFromNonProtectedSettingnull135     fun givenRestrictedAppThenIsNotRestrictedFromNonProtectedSetting() {
136         installPackageWithInstallSourceAndMetadataFromDownloadedFile(APP_APK_NAME_LATEST)
137         runWithShellPermissionIdentity {
138             eventually { assertFalse(ecm.isRestricted(APP_PACKAGE_NAME, NON_PROTECTED_SETTING)) }
139         }
140     }
141 
142     @RequiresFlagsEnabled(Flags.FLAG_ENHANCED_CONFIRMATION_MODE_APIS_ENABLED)
143     @Test
givenRestrictedAppThenClearRestrictionNotAllowedByDefaultnull144     fun givenRestrictedAppThenClearRestrictionNotAllowedByDefault() {
145         installPackageWithInstallSourceAndMetadataFromDownloadedFile(APP_APK_NAME_LATEST)
146         runWithShellPermissionIdentity {
147             eventually { assertFalse(ecm.isClearRestrictionAllowed(APP_PACKAGE_NAME)) }
148         }
149     }
150 
151     @RequiresFlagsEnabled(Flags.FLAG_ENHANCED_CONFIRMATION_MODE_APIS_ENABLED)
152     @Test
givenRestrictedAppWhenClearRestrictionThenNotRestrictedFromProtectedSettingnull153     fun givenRestrictedAppWhenClearRestrictionThenNotRestrictedFromProtectedSetting() {
154         installPackageWithInstallSourceAndMetadataFromDownloadedFile(APP_APK_NAME_LATEST)
155         runWithShellPermissionIdentity {
156             eventually { assertTrue(ecm.isRestricted(APP_PACKAGE_NAME, PROTECTED_SETTING)) }
157             ecm.setClearRestrictionAllowed(APP_PACKAGE_NAME)
158             eventually { assertTrue(ecm.isClearRestrictionAllowed(APP_PACKAGE_NAME)) }
159             ecm.clearRestriction(APP_PACKAGE_NAME)
160             eventually { assertFalse(ecm.isRestricted(APP_PACKAGE_NAME, PROTECTED_SETTING)) }
161         }
162     }
163 
164     @RequiresFlagsEnabled(Flags.FLAG_ENHANCED_CONFIRMATION_MODE_APIS_ENABLED)
165     @Test
createRestrictedSettingDialogIntentReturnsIntentnull166     fun createRestrictedSettingDialogIntentReturnsIntent() {
167         installPackageWithInstallSourceAndMetadataFromDownloadedFile(APP_APK_NAME_LATEST)
168 
169         val intent = ecm.createRestrictedSettingDialogIntent(APP_PACKAGE_NAME, PROTECTED_SETTING)
170 
171         assertNotNull(intent)
172     }
173 
174     @RequiresFlagsEnabled(Flags.FLAG_ENHANCED_CONFIRMATION_MODE_APIS_ENABLED)
175     @Test
grantDialogBlocksRestrictedPermissionsOfSameGroupTogethernull176     fun grantDialogBlocksRestrictedPermissionsOfSameGroupTogether() {
177         installPackageWithInstallSourceFromDownloadedFileAndAllowHardRestrictedPerms(
178             APP_APK_NAME_LATEST
179         )
180         val permissionAndExpectedGrantResults =
181             arrayOf(
182                 GROUP_2_PERMISSION_1_RESTRICTED to false,
183                 GROUP_2_PERMISSION_2_RESTRICTED to false
184             )
185 
186         requestAppPermissionsAndAssertResult(*permissionAndExpectedGrantResults) {
187             click(By.res(ALERT_DIALOG_OK_BUTTON), TIMEOUT_MILLIS)
188         }
189         assertTrue(isClearRestrictionAllowed(APP_PACKAGE_NAME))
190 
191         requestAppPermissionsAndAssertResult(
192             *permissionAndExpectedGrantResults,
193             waitForWindowTransition = false
194         ) {
195             assertNoEcmDialogShown()
196         }
197         assertTrue(isClearRestrictionAllowed(APP_PACKAGE_NAME))
198     }
199 
200     @RequiresFlagsEnabled(Flags.FLAG_ENHANCED_CONFIRMATION_MODE_APIS_ENABLED)
201     @Test
grantDialogBlocksRestrictedGroupsThenRequestsUnrestrictedGroupsDespiteOutOfOrderRequestnull202     fun grantDialogBlocksRestrictedGroupsThenRequestsUnrestrictedGroupsDespiteOutOfOrderRequest() {
203         installPackageWithInstallSourceFromDownloadedFileAndAllowHardRestrictedPerms(
204             APP_APK_NAME_LATEST
205         )
206 
207         requestAppPermissionsAndAssertResult(
208             GROUP_3_PERMISSION_1_UNRESTRICTED to false,
209             GROUP_2_PERMISSION_1_RESTRICTED to false,
210             GROUP_3_PERMISSION_2_UNRESTRICTED to false,
211             GROUP_2_PERMISSION_2_RESTRICTED to false,
212             waitForWindowTransition = false
213         ) {
214             doAndWaitForWindowTransition { click(By.res(ALERT_DIALOG_OK_BUTTON), TIMEOUT_MILLIS) }
215             doAndWaitForWindowTransition { clickPermissionRequestDenyButton() }
216         }
217         assertTrue(isClearRestrictionAllowed(APP_PACKAGE_NAME))
218 
219         requestAppPermissionsAndAssertResult(
220             GROUP_3_PERMISSION_1_UNRESTRICTED to true,
221             GROUP_3_PERMISSION_2_UNRESTRICTED to true,
222             waitForWindowTransition = false
223         ) {
224             doAndWaitForWindowTransition { clickPermissionRequestAllowForegroundButton() }
225         }
226         assertTrue(isClearRestrictionAllowed(APP_PACKAGE_NAME))
227     }
228 
229     @RequiresFlagsEnabled(Flags.FLAG_ENHANCED_CONFIRMATION_MODE_APIS_ENABLED)
230     @Test
grantDialogBlocksRestrictedGroupsThenRequestsUnrestrictedHighPriorityGroupsnull231     fun grantDialogBlocksRestrictedGroupsThenRequestsUnrestrictedHighPriorityGroups() {
232         installPackageWithInstallSourceFromDownloadedFileAndAllowHardRestrictedPerms(
233             APP_APK_NAME_LATEST
234         )
235 
236         requestAppPermissionsAndAssertResult(
237             GROUP_3_PERMISSION_1_UNRESTRICTED to true,
238             GROUP_2_PERMISSION_1_RESTRICTED to false,
239             waitForWindowTransition = false
240         ) {
241             doAndWaitForWindowTransition { click(By.res(ALERT_DIALOG_OK_BUTTON), TIMEOUT_MILLIS) }
242             doAndWaitForWindowTransition { clickPermissionRequestAllowForegroundButton() }
243         }
244         assertTrue(isClearRestrictionAllowed(APP_PACKAGE_NAME))
245     }
246 
247     @RequiresFlagsEnabled(Flags.FLAG_ENHANCED_CONFIRMATION_MODE_APIS_ENABLED)
248     @Test
grantDialogBlocksRestrictedGroupsThenRequestsUnrestrictedLowPriorityGroupsnull249     fun grantDialogBlocksRestrictedGroupsThenRequestsUnrestrictedLowPriorityGroups() {
250         installPackageWithInstallSourceFromDownloadedFileAndAllowHardRestrictedPerms(
251             APP_APK_NAME_LATEST
252         )
253 
254         requestAppPermissionsAndAssertResult(
255             GROUP_4_PERMISSION_1_UNRESTRICTED to true,
256             GROUP_2_PERMISSION_1_RESTRICTED to false,
257             waitForWindowTransition = false
258         ) {
259             doAndWaitForWindowTransition { click(By.res(ALERT_DIALOG_OK_BUTTON), TIMEOUT_MILLIS) }
260             doAndWaitForWindowTransition { clickPermissionRequestAllowForegroundButton() }
261         }
262         assertTrue(isClearRestrictionAllowed(APP_PACKAGE_NAME))
263     }
264 
<lambda>null265     private fun isClearRestrictionAllowed(packageName: String) = callWithShellPermissionIdentity {
266         ecm.isClearRestrictionAllowed(packageName)
267     }
268 
assertNoEcmDialogShownnull269     private fun assertNoEcmDialogShown() {
270         assertNull(
271             "expected to not see dialog",
272             waitFindObjectOrNull(By.res(ALERT_DIALOG_OK_BUTTON), UNEXPECTED_TIMEOUT_MILLIS.toLong())
273         )
274     }
275 
276     companion object {
277         private const val GROUP_2_PERMISSION_1_RESTRICTED = Manifest.permission.SEND_SMS
278         private const val GROUP_2_PERMISSION_2_RESTRICTED = Manifest.permission.READ_SMS
279         private const val GROUP_3_PERMISSION_1_UNRESTRICTED =
280             Manifest.permission.ACCESS_FINE_LOCATION
281         private const val GROUP_3_PERMISSION_2_UNRESTRICTED =
282             Manifest.permission.ACCESS_COARSE_LOCATION
283         private const val GROUP_4_PERMISSION_1_UNRESTRICTED = Manifest.permission.BODY_SENSORS
284 
285         private const val NON_PROTECTED_SETTING = "example_setting_which_is_not_protected"
286         private const val PROTECTED_SETTING = "android:bind_accessibility_service"
287 
288         @Throws(PackageManager.NameNotFoundException::class)
setAppEcmStatenull289         private fun setAppEcmState(
290             context: Context,
291             appOpsManager: AppOpsManager,
292             packageName: String,
293             mode: Int
294         ) =
295             appOpsManager.setMode(
296                 AppOpsManager.OPSTR_ACCESS_RESTRICTED_SETTINGS,
297                 getPackageUid(context, packageName),
298                 packageName,
299                 mode
300             )
301 
302         @Throws(PackageManager.NameNotFoundException::class)
303         private fun getAppEcmState(
304             context: Context,
305             appOpsManager: AppOpsManager,
306             packageName: String
307         ) =
308             appOpsManager.noteOpNoThrow(
309                 AppOpsManager.OPSTR_ACCESS_RESTRICTED_SETTINGS,
310                 getPackageUid(context, packageName),
311                 packageName,
312                 context.attributionTag,
313                 /* message */ null
314             )
315 
316         @Throws(PackageManager.NameNotFoundException::class)
317         private fun getPackageUid(context: Context, packageName: String) =
318             getApplicationInfoAsUser(context, packageName).uid
319 
320         @Throws(PackageManager.NameNotFoundException::class)
321         private fun getApplicationInfoAsUser(context: Context, packageName: String) =
322             packageManager.getApplicationInfoAsUser(
323                 packageName,
324                 /* flags */ 0,
325                 Process.myUserHandle()
326             )
327     }
328 }
329