1 /*
<lambda>null2  * Copyright (C) 2023 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 package android.permissionui.cts
17 
18 import android.Manifest
19 import android.app.Instrumentation
20 import android.app.UiAutomation
21 import android.app.compat.CompatChanges
22 import android.content.AttributionSource
23 import android.content.Context
24 import android.content.Intent
25 import android.content.pm.PackageManager
26 import android.hardware.camera2.CameraManager
27 import android.os.Build
28 import android.os.Process
29 import android.os.SystemClock
30 import android.os.SystemProperties
31 import android.os.UserManager
32 import android.permission.PermissionManager
33 import android.permission.cts.MtsIgnore
34 import android.platform.test.annotations.AsbSecurityTest
35 import android.platform.test.rule.ScreenRecordRule
36 import android.provider.DeviceConfig
37 import android.provider.Settings
38 import android.safetycenter.SafetyCenterManager
39 import android.server.wm.WindowManagerStateHelper
40 import android.util.Log
41 import androidx.annotation.RequiresApi
42 import androidx.test.filters.FlakyTest
43 import androidx.test.filters.SdkSuppress
44 import androidx.test.platform.app.InstrumentationRegistry
45 import androidx.test.uiautomator.By
46 import androidx.test.uiautomator.BySelector
47 import androidx.test.uiautomator.StaleObjectException
48 import androidx.test.uiautomator.UiDevice
49 import androidx.test.uiautomator.UiObject2
50 import androidx.test.uiautomator.UiSelector
51 import com.android.compatibility.common.util.CddTest
52 import com.android.compatibility.common.util.DisableAnimationRule
53 import com.android.compatibility.common.util.SystemUtil.callWithShellPermissionIdentity
54 import com.android.compatibility.common.util.SystemUtil.eventually
55 import com.android.compatibility.common.util.SystemUtil.runShellCommand
56 import com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow
57 import com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity
58 import com.android.compatibility.common.util.UiAutomatorUtils2
59 import com.android.compatibility.common.util.UiAutomatorUtils2.assertWithUiDump
60 import com.android.modules.utils.build.SdkLevel
61 import com.android.sts.common.util.StsExtraBusinessLogicTestCase
62 import java.util.regex.Pattern
63 import org.junit.After
64 import org.junit.Assert
65 import org.junit.Assert.assertEquals
66 import org.junit.Assert.assertFalse
67 import org.junit.Assert.assertNotNull
68 import org.junit.Assert.assertNull
69 import org.junit.Assert.assertTrue
70 import org.junit.Assume.assumeFalse
71 import org.junit.Assume.assumeTrue
72 import org.junit.Before
73 import org.junit.Rule
74 import org.junit.Test
75 
76 private const val APK_PATH =
77     "/data/local/tmp/cts-permissionui/CtsAppThatAccessesMicAndCameraPermission.apk"
78 private const val APP_LABEL = "CtsCameraMicAccess"
79 private const val APP_PKG = "android.permissionui.cts.appthataccessescameraandmic"
80 private const val SHELL_PKG = "com.android.shell"
81 private const val USE_CAMERA = "use_camera"
82 private const val USE_MICROPHONE = "use_microphone"
83 private const val USE_HOTWORD = "use_hotword"
84 private const val FINISH_EARLY = "finish_early"
85 private const val USE_INTENT_ACTION = "test.action.USE_CAMERA_OR_MIC"
86 private const val PRIVACY_CHIP_ID = "com.android.systemui:id/privacy_chip"
87 private const val PRIVACY_ITEM_ID = "com.android.systemui:id/privacy_item"
88 private const val INDICATORS_FLAG = "camera_mic_icons_enabled"
89 private const val WEAR_MIC_LABEL = "Microphone"
90 private const val PERMISSION_INDICATORS_NOT_PRESENT = 162547999L
91 private const val IDLE_TIMEOUT_MILLIS: Long = 2000
92 private const val TIMEOUT_MILLIS: Long = 20000
93 private const val TV_MIC_INDICATOR_WINDOW_TITLE = "MicrophoneCaptureIndicator"
94 private const val MIC_LABEL_NAME = "microphone_toggle_label_qs"
95 private const val CAMERA_LABEL_NAME = "camera_toggle_label_qs"
96 private val HOTWORD_DETECTION_SERVICE_REQUIRED =
97     SystemProperties.getBoolean("ro.hotword.detection_service_required", false)
98 
99 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.S, codeName = "S")
100 @ScreenRecordRule.ScreenRecord
101 @FlakyTest
102 class CameraMicIndicatorsPermissionTest : StsExtraBusinessLogicTestCase {
103     private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
104     private val context: Context = instrumentation.context
105     private val uiAutomation: UiAutomation = instrumentation.uiAutomation
106     private val uiDevice: UiDevice = UiDevice.getInstance(instrumentation)
107     private val packageManager: PackageManager = context.packageManager
108     private val permissionManager: PermissionManager =
109         context.getSystemService(PermissionManager::class.java)!!
110 
111     private val isTv = packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
112     private val isCar = packageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)
113     private val isWatch = packageManager.hasSystemFeature(PackageManager.FEATURE_WATCH)
114     private val originalCameraLabel =
115         packageManager
116             .getPermissionGroupInfo(Manifest.permission_group.CAMERA, 0)
117             .loadLabel(packageManager)
118             .toString()
119     private val originalMicLabel =
120         packageManager
121             .getPermissionGroupInfo(Manifest.permission_group.MICROPHONE, 0)
122             .loadLabel(packageManager)
123             .toString()
124     private val cameraLabel = originalCameraLabel.lowercase()
125     private val micLabel = originalMicLabel.lowercase()
126     private var wasEnabled = false
127     private var isScreenOn = false
128     private var screenTimeoutBeforeTest: Long = 0L
129     private lateinit var carMicPrivacyChipId: String
130     private lateinit var carCameraPrivacyChipId: String
131 
132     @get:Rule val disableAnimationRule = DisableAnimationRule()
133 
134     @get:Rule val screenRecordRule = ScreenRecordRule(false, false)
135 
136     constructor() : super()
137 
138     companion object {
139         private const val AUTO_MIC_INDICATOR_DISMISSAL_TIMEOUT_MS = 30_000L
140         const val SAFETY_CENTER_ENABLED = "safety_center_is_enabled"
141         const val DELAY_MILLIS = 3000L
142         private val TAG = CameraMicIndicatorsPermissionTest::class.java.simpleName
143     }
144 
145     private val safetyCenterEnabled = callWithShellPermissionIdentity {
146         DeviceConfig.getString(
147             DeviceConfig.NAMESPACE_PRIVACY,
148             SAFETY_CENTER_ENABLED,
149             false.toString()
150         )!!
151     }
152 
153     private fun uninstall() {
154         val output = runShellCommand("pm uninstall $APP_PKG").trim()
155         assertEquals("Success", output)
156     }
157 
158     private fun install() {
159         val output = runShellCommandOrThrow("pm install -g $APK_PATH").trim()
160         assertEquals("Success", output)
161     }
162 
163     @Before
164     fun setUp() {
165         // Camera and Mic are not supported for secondary user visible as a background user.
166         assumeFalse(isAutomotiveWithVisibleBackgroundUser())
167         runWithShellPermissionIdentity {
168             screenTimeoutBeforeTest =
169                 Settings.System.getLong(context.contentResolver, Settings.System.SCREEN_OFF_TIMEOUT)
170             Settings.System.putLong(
171                 context.contentResolver,
172                 Settings.System.SCREEN_OFF_TIMEOUT,
173                 1800000L
174             )
175         }
176 
177         if (!isScreenOn) {
178             uiDevice.wakeUp()
179             runShellCommand(instrumentation, "wm dismiss-keyguard")
180             Thread.sleep(DELAY_MILLIS)
181             isScreenOn = true
182         }
183         uiDevice.findObject(By.text("Close"))?.click()
184         wasEnabled = setIndicatorsEnabledStateIfNeeded(true)
185         // If the change Id is not present, then isChangeEnabled will return true. To bypass this,
186         // the change is set to "false" if present.
187         assumeFalse(
188             "feature not present on this device",
189             callWithShellPermissionIdentity {
190                 CompatChanges.isChangeEnabled(PERMISSION_INDICATORS_NOT_PRESENT, Process.SYSTEM_UID)
191             }
192         )
193         install()
194     }
195 
196     private fun setIndicatorsEnabledStateIfNeeded(shouldBeEnabled: Boolean): Boolean {
197         var currentlyEnabled = false
198         runWithShellPermissionIdentity {
199             currentlyEnabled =
200                 DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_PRIVACY, INDICATORS_FLAG, true)
201             if (currentlyEnabled != shouldBeEnabled) {
202                 DeviceConfig.setProperty(
203                     DeviceConfig.NAMESPACE_PRIVACY,
204                     INDICATORS_FLAG,
205                     shouldBeEnabled.toString(),
206                     false
207                 )
208             }
209         }
210         return currentlyEnabled
211     }
212 
213     @After
214     fun tearDown() {
215         if (isAutomotiveWithVisibleBackgroundUser()) {
216             return
217         }
218         uninstall()
219         if (isCar) {
220             // Deselect the indicator since it persists otherwise
221             pressBack()
222         }
223         eventually(
224             { assertIndicatorsShown(false, false, false) },
225             AUTO_MIC_INDICATOR_DISMISSAL_TIMEOUT_MS
226         )
227         if (!wasEnabled) {
228             setIndicatorsEnabledStateIfNeeded(false)
229         }
230         runWithShellPermissionIdentity {
231             Settings.System.putLong(
232                 context.contentResolver,
233                 Settings.System.SCREEN_OFF_TIMEOUT,
234                 screenTimeoutBeforeTest
235             )
236         }
237         changeSafetyCenterFlag(safetyCenterEnabled)
238         if (!isTv) {
239             pressBack()
240             pressBack()
241         }
242         pressHome()
243         pressHome()
244     }
245 
246     private fun openApp(
247         useMic: Boolean,
248         useCamera: Boolean,
249         useHotword: Boolean,
250         finishEarly: Boolean = false
251     ) {
252         context.startActivity(
253             Intent(USE_INTENT_ACTION).apply {
254                 putExtra(USE_CAMERA, useCamera)
255                 putExtra(USE_MICROPHONE, useMic)
256                 putExtra(USE_HOTWORD, useHotword)
257                 putExtra(FINISH_EARLY, finishEarly)
258                 addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
259             }
260         )
261     }
262 
263     @Test
264     @CddTest(requirement = "9.8.2/H-5-1,T-5-1,A-2-1")
265     fun testCameraIndicator() {
266         // If camera is not available skip the test
267         assumeTrue(packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY))
268         val manager = context.getSystemService(CameraManager::class.java)!!
269         assumeTrue(manager.cameraIdList.isNotEmpty())
270         changeSafetyCenterFlag(false.toString())
271         assumeSafetyCenterDisabled()
272         testCameraAndMicIndicator(useMic = false, useCamera = true)
273     }
274 
275     @Test
276     @CddTest(requirement = "9.8.2/H-4-1,T-4-1,A-1-1")
277     fun testMicIndicator() {
278         changeSafetyCenterFlag(false.toString())
279         assumeSafetyCenterDisabled()
280         testCameraAndMicIndicator(useMic = true, useCamera = false)
281     }
282 
283     @Test
284     @AsbSecurityTest(cveBugId = [258672042])
285     @MtsIgnore(bugId = 351903707)
286     fun testMicIndicatorWithManualFinishOpStillShows() {
287         testCameraAndMicIndicator(
288             useMic = true,
289             useCamera = false,
290             finishEarly = true,
291             safetyCenterEnabled = getSafetyCenterEnabled()
292         )
293     }
294 
295     @Test
296     @CddTest(requirement = "9.8.2/H-4-1,T-4-1,A-1-1")
297     fun testHotwordIndicatorBehavior() {
298         changeSafetyCenterFlag(false.toString())
299         assumeSafetyCenterDisabled()
300         testCameraAndMicIndicator(useMic = false, useCamera = false, useHotword = true)
301     }
302 
303     @Test
304     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU, codeName = "Tiramisu")
305     fun testChainUsageWithOtherUsage() {
306         // TV has only the mic icon
307         assumeFalse(isTv)
308         // Car has separate panels for mic and camera for now.
309         // TODO(b/218788634): enable this test for car once the new camera indicator is implemented.
310         assumeFalse(isCar)
311         // If camera is not available skip the test
312         assumeTrue(packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY))
313         changeSafetyCenterFlag(false.toString())
314         assumeSafetyCenterDisabled()
315         testCameraAndMicIndicator(useMic = false, useCamera = true, chainUsage = true)
316     }
317 
318     @Test
319     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU, codeName = "Tiramisu")
320     fun testSafetyCenterCameraIndicator() {
321         assumeFalse(isTv)
322         assumeFalse(isCar)
323         val manager = context.getSystemService(CameraManager::class.java)!!
324         assumeTrue(manager.cameraIdList.isNotEmpty())
325         changeSafetyCenterFlag(true.toString())
326         assumeSafetyCenterEnabled()
327         testCameraAndMicIndicator(useMic = false, useCamera = true, safetyCenterEnabled = true)
328     }
329 
330     @Test
331     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU, codeName = "Tiramisu")
332     fun testSafetyCenterMicIndicator() {
333         assumeFalse(isTv)
334         assumeFalse(isCar)
335         changeSafetyCenterFlag(true.toString())
336         assumeSafetyCenterEnabled()
337         testCameraAndMicIndicator(useMic = true, useCamera = false, safetyCenterEnabled = true)
338     }
339 
340     @Test
341     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU, codeName = "Tiramisu")
342     fun testSafetyCenterHotwordIndicatorBehavior() {
343         assumeFalse(isTv)
344         assumeFalse(isCar)
345         assumeTrue(HOTWORD_DETECTION_SERVICE_REQUIRED)
346         changeSafetyCenterFlag(true.toString())
347         assumeSafetyCenterEnabled()
348         testCameraAndMicIndicator(
349             useMic = false,
350             useCamera = false,
351             useHotword = true,
352             safetyCenterEnabled = true
353         )
354     }
355 
356     @Test
357     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU, codeName = "Tiramisu")
358     fun testSafetyCenterChainUsageWithOtherUsage() {
359         assumeFalse(isTv)
360         assumeFalse(isCar)
361         changeSafetyCenterFlag(true.toString())
362         assumeSafetyCenterEnabled()
363         testCameraAndMicIndicator(
364             useMic = false,
365             useCamera = true,
366             chainUsage = true,
367             safetyCenterEnabled = true
368         )
369     }
370 
371     private fun testCameraAndMicIndicator(
372         useMic: Boolean,
373         useCamera: Boolean,
374         useHotword: Boolean = false,
375         chainUsage: Boolean = false,
376         safetyCenterEnabled: Boolean = false,
377         finishEarly: Boolean = false
378     ) {
379         Log.d(
380             TAG,
381             "testCameraAndMicIndicator useMic=$useMic useCamera=$useCamera " +
382                 "safetyCenterEnabled=$safetyCenterEnabled finishEarly=$finishEarly"
383         )
384         // If camera is not available skip the test
385         if (useCamera) {
386             assumeTrue(packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY))
387         }
388         var chainAttribution: AttributionSource? = null
389         openApp(useMic, useCamera, useHotword, finishEarly)
390         try {
391             eventually {
392                 val appView =
393                     if (isWatch) {
394                         // Title is disabled by default on watch apps
395                         uiDevice.findObject(UiSelector().packageName(APP_PKG))
396                     } else {
397                         uiDevice.findObject(UiSelector().textContains(APP_LABEL))
398                     }
399 
400                 assertWithUiDump {
401                     assertTrue("View with text $APP_LABEL not found", appView.exists())
402                 }
403             }
404             if (chainUsage) {
405                 chainAttribution = createChainAttribution()
406                 runWithShellPermissionIdentity {
407                     val ret =
408                         permissionManager.checkPermissionForStartDataDelivery(
409                             Manifest.permission.RECORD_AUDIO,
410                             chainAttribution!!,
411                             ""
412                         )
413                     assertEquals(PermissionManager.PERMISSION_GRANTED, ret)
414                 }
415             }
416 
417             Log.d(TAG, "assert to make sure Indicators are displayed")
418             assertIndicatorsShown(useMic, useCamera, useHotword, chainUsage, safetyCenterEnabled)
419 
420             if (finishEarly) {
421                 // Assert that the indicator doesn't go away
422                 var failed = false
423                 try {
424                     // Check if the indicator has gone away. This will throw an exception if the
425                     // indicator is still present
426                     Log.d(TAG, "checking on indicators again")
427                     assertIndicatorsShown(false, false, false)
428                     Log.d(TAG, "indicators are gone")
429                     // If we successfully asserted that the indicator went away, fail the test
430                     failed = true
431                 } catch (t: Throwable) {
432                     Log.d(TAG, "indicators are continuing to show")
433                     // expected
434                 }
435                 if (failed) {
436                     assertWithUiDump { Assert.fail("Expected the indicator to remain present") }
437                 }
438             }
439         } finally {
440             if (chainAttribution != null) {
441                 runWithShellPermissionIdentity {
442                     permissionManager.finishDataDelivery(
443                         Manifest.permission.RECORD_AUDIO,
444                         chainAttribution
445                     )
446                 }
447             }
448         }
449     }
450 
451     private fun assertIndicatorsShown(
452         useMic: Boolean,
453         useCamera: Boolean,
454         useHotword: Boolean = false,
455         chainUsage: Boolean = false,
456         safetyCenterEnabled: Boolean = false,
457     ) {
458         if (isTv) {
459             assertTvIndicatorsShown(useMic, useCamera, useHotword)
460         } else if (isCar) {
461             assertCarIndicatorsShown(useMic, useCamera, useHotword, chainUsage)
462         } else if (isWatch) {
463             assertWatchIndicatorsShown(useMic, useCamera, useHotword)
464         } else {
465             uiDevice.openQuickSettings()
466             val micInUse =
467                 if (SdkLevel.isAtLeastU() && HOTWORD_DETECTION_SERVICE_REQUIRED) {
468                     useMic || useHotword
469                 } else {
470                     useMic
471                 }
472             if (!micInUse && !useCamera) {
473                 // We're asserting the indicator is gone. Wait up to IDLE_TIMEOUT after the quick
474                 // settings is opened to see if we find the indicator, so we don't automatically
475                 // assert the indicator is gone, just because we didn't open quick settings fast
476                 // enough.
477                 UiAutomatorUtils2.waitFindObjectOrNull(By.res(PRIVACY_CHIP_ID), IDLE_TIMEOUT_MILLIS)
478             }
479             assertPrivacyChipAndIndicatorsPresent(
480                 micInUse,
481                 useCamera,
482                 chainUsage,
483                 safetyCenterEnabled
484             )
485             uiDevice.pressBack()
486         }
487     }
488 
489     private fun assertWatchIndicatorsShown(
490         useMic: Boolean,
491         useCamera: Boolean,
492         useHotword: Boolean
493     ) {
494         if (useMic || useHotword || (!useMic && !useCamera && !useHotword)) {
495             val iconView = UiAutomatorUtils2.waitFindObjectOrNull(By.descContains(WEAR_MIC_LABEL))
496             if (useMic) {
497                 assertNotNull("Did not find mic chip", iconView)
498             } else {
499                 assertNull("Found mic chip, but did not expect to", iconView)
500                 // waitFindObject leaves the watch on the notification screen
501                 pressBack()
502             }
503         }
504     }
505 
506     private fun assertTvIndicatorsShown(useMic: Boolean, useCamera: Boolean, useHotword: Boolean) {
507         if (useMic || useHotword || (!useMic && !useCamera && !useHotword)) {
508             eventually {
509                 val found =
510                     WindowManagerStateHelper().waitFor(
511                         "Waiting for the mic indicator window to come up"
512                     ) {
513                         it.containsWindow(TV_MIC_INDICATOR_WINDOW_TITLE) &&
514                             it.isWindowVisible(TV_MIC_INDICATOR_WINDOW_TITLE)
515                     }
516                 if (useMic) {
517                     assertTrue("Did not find chip", found)
518                 } else {
519                     assertFalse("Found chip, but did not expect to", found)
520                 }
521             }
522         }
523         if (useCamera) {
524             // There is no camera indicator on TVs.
525         }
526     }
527 
528     private fun assertCarIndicatorsShown(
529         useMic: Boolean,
530         useCamera: Boolean,
531         useHotword: Boolean,
532         chainUsage: Boolean
533     ) {
534         eventually {
535             // Ensure the privacy chip is present (or not)
536             carMicPrivacyChipId = context.getString(R.string.car_mic_privacy_chip_id)
537             carCameraPrivacyChipId = context.getString(R.string.car_camera_privacy_chip_id)
538             var micPrivacyChip = uiDevice.findObject(By.res(carMicPrivacyChipId))
539             var cameraPrivacyChip = uiDevice.findObject(By.res(carCameraPrivacyChipId))
540             if (useMic) {
541                 assertNotNull("Did not find mic chip", micPrivacyChip)
542                 // Click to chip to show the panel.
543                 micPrivacyChip.click()
544             } else if (useCamera) {
545                 assertNotNull("Did not find camera chip", cameraPrivacyChip)
546                 // Click to chip to show the panel.
547                 cameraPrivacyChip.click()
548             } else {
549                 assertNull("Found mic chip, but did not expect to", micPrivacyChip)
550                 assertNull("Found camera chip, but did not expect to", cameraPrivacyChip)
551             }
552         }
553 
554         eventually {
555             if (chainUsage) {
556                 // Not applicable for car
557                 assertChainMicAndOtherCameraUsed(false)
558                 return@eventually
559             }
560             if (useMic) {
561                 // There should be a mic privacy panel after mic privacy chip is clicked
562                 val micLabelView = uiDevice.findObject(UiSelector().textContains(micLabel))
563                 assertTrue("View with text $micLabel not found", micLabelView.exists())
564                 val appView = uiDevice.findObject(UiSelector().textContains(APP_LABEL))
565                 assertTrue("View with text $APP_LABEL not found", appView.exists())
566             } else if (useCamera) {
567                 // There should be a camera privacy panel after camera privacy chip is clicked
568                 val cameraLabelView = uiDevice.findObject(UiSelector().textContains(cameraLabel))
569                 assertTrue("View with text $cameraLabel not found", cameraLabelView.exists())
570                 val appView = uiDevice.findObject(UiSelector().textContains(APP_LABEL))
571                 assertTrue("View with text $APP_LABEL not found", appView.exists())
572             } else {
573                 // There should be no privacy panel when using hot word
574                 val micLabelView = uiDevice.findObject(UiSelector().textContains(micLabel))
575                 assertFalse(
576                     "View with text $micLabel found, but did not expect to",
577                     micLabelView.exists()
578                 )
579                 val cameraLabelView = uiDevice.findObject(UiSelector().textContains(cameraLabel))
580                 assertFalse(
581                     "View with text $cameraLabel found, but did not expect to",
582                     cameraLabelView.exists()
583                 )
584                 val appView = uiDevice.findObject(UiSelector().textContains(APP_LABEL))
585                 assertFalse(
586                     "View with text $APP_LABEL found, but did not expect to",
587                     appView.exists()
588                 )
589             }
590         }
591     }
592 
593     private fun assertPrivacyChipAndIndicatorsPresent(
594         useMic: Boolean,
595         useCamera: Boolean,
596         chainUsage: Boolean,
597         safetyCenterEnabled: Boolean = false
598     ) {
599         // Ensure the privacy chip is present
600         if (useCamera || useMic) {
601             eventually {
602                 val privacyChip = UiAutomatorUtils2.waitFindObjectOrNull(By.res(PRIVACY_CHIP_ID))
603                 assertWithUiDump {
604                     assertNotNull("view with id $PRIVACY_CHIP_ID not found", privacyChip)
605                 }
606                 privacyChip.click()
607             }
608         } else {
609             Log.d(TAG, "waiting for PRIVACY_CHIP_ID to disappear")
610             assertWithUiDump { UiAutomatorUtils2.waitUntilObjectGone(By.res(PRIVACY_CHIP_ID)) }
611             return
612         }
613 
614         eventually {
615             if (chainUsage) {
616                 assertChainMicAndOtherCameraUsed(safetyCenterEnabled)
617                 return@eventually
618             }
619             if (useMic) {
620                 if (safetyCenterEnabled) {
621                     assertSafetyCenterMicViewNotNull()
622                 } else {
623                     val iconView = waitFindObject(By.descContains(micLabel))
624                     assertWithUiDump {
625                         assertNotNull("View with description '$micLabel' not found", iconView)
626                     }
627                 }
628             }
629             if (useCamera) {
630                 if (safetyCenterEnabled) {
631                     assertSafetyCenterCameraViewNotNull()
632                 } else {
633                     val iconView = waitFindObject(By.descContains(cameraLabel))
634                     assertWithUiDump {
635                         assertNotNull("View with description '$cameraLabel' not found", iconView)
636                     }
637                 }
638             }
639             var appView = waitFindObject(By.textContains(APP_LABEL))
640             assertWithUiDump { assertNotNull("View with text $APP_LABEL not found", appView) }
641         }
642         uiDevice.pressBack()
643     }
644 
645     private fun createChainAttribution(): AttributionSource? {
646         var attrSource: AttributionSource? = null
647         runWithShellPermissionIdentity {
648             try {
649                 val appUid = packageManager.getPackageUid(APP_PKG, 0)
650                 val childAttribution = AttributionSource(appUid, APP_PKG, null)
651                 val attribution =
652                     AttributionSource(
653                         Process.myUid(),
654                         context.packageName,
655                         null,
656                         null,
657                         permissionManager.registerAttributionSource(childAttribution)
658                     )
659                 attrSource = permissionManager.registerAttributionSource(attribution)
660             } catch (e: PackageManager.NameNotFoundException) {
661                 Assert.fail("Expected to find a UID for $APP_LABEL")
662             }
663         }
664         return attrSource
665     }
666 
667     private fun assertChainMicAndOtherCameraUsed(safetyCenterEnabled: Boolean) {
668         val shellLabel =
669             try {
670                 context.packageManager
671                     .getApplicationInfo(SHELL_PKG, 0)
672                     .loadLabel(context.packageManager)
673                     .toString()
674             } catch (e: PackageManager.NameNotFoundException) {
675                 "Did not find shell package"
676             }
677 
678         if (safetyCenterEnabled) {
679             assertSafetyCenterMicViewNotNull()
680             assertSafetyCenterCameraViewNotNull()
681             var shellView = waitFindObject(By.textContains(shellLabel))
682             assertNotNull("View with text $shellLabel not found", shellView)
683         } else {
684             val usageViews = uiDevice.findObjects(By.res(PRIVACY_ITEM_ID))
685             assertEquals("Expected two usage views", 2, usageViews.size)
686             val appViews = uiDevice.findObjects(By.textContains(APP_LABEL))
687             assertEquals("Expected two $APP_LABEL view", 2, appViews.size)
688             val shellView = uiDevice.findObjects(By.textContains(shellLabel))
689             assertEquals("Expected only one shell view", 1, shellView.size)
690         }
691     }
692 
693     private fun pressBack() {
694         uiDevice.pressBack()
695     }
696 
697     private fun pressHome() {
698         uiDevice.pressHome()
699     }
700 
701     private fun changeSafetyCenterFlag(safetyCenterEnabled: String) {
702         runWithShellPermissionIdentity {
703             DeviceConfig.setProperty(
704                 DeviceConfig.NAMESPACE_PRIVACY,
705                 SAFETY_CENTER_ENABLED,
706                 safetyCenterEnabled,
707                 false
708             )
709         }
710     }
711 
712     @RequiresApi(Build.VERSION_CODES.TIRAMISU)
713     private fun assumeSafetyCenterEnabled() {
714         assumeTrue(getSafetyCenterEnabled())
715     }
716 
717     private fun assumeSafetyCenterDisabled() {
718         assumeFalse(getSafetyCenterEnabled())
719     }
720 
721     private fun getSafetyCenterEnabled(): Boolean {
722         val safetyCenterManager =
723             context.getSystemService(SafetyCenterManager::class.java) ?: return false
724         return runWithShellPermissionIdentity<Boolean> { safetyCenterManager.isSafetyCenterEnabled }
725     }
726 
727     protected fun waitFindObject(selector: BySelector): UiObject2? {
728         return findObjectWithRetry({ t -> UiAutomatorUtils2.waitFindObject(selector, t) })
729     }
730 
731     private fun findObjectWithRetry(
732         automatorMethod: (timeoutMillis: Long) -> UiObject2?,
733         timeoutMillis: Long = TIMEOUT_MILLIS
734     ): UiObject2? {
735         val startTime = SystemClock.elapsedRealtime()
736         return try {
737             automatorMethod(timeoutMillis)
738         } catch (e: StaleObjectException) {
739             val remainingTime = timeoutMillis - (SystemClock.elapsedRealtime() - startTime)
740             if (remainingTime <= 0) {
741                 throw e
742             }
743             automatorMethod(remainingTime)
744         }
745     }
746 
747     private fun getPermissionControllerString(resourceName: String): String {
748         val permissionControllerPkg = context.packageManager.permissionControllerPackageName
749         try {
750             val permissionControllerContext =
751                 context.createPackageContext(permissionControllerPkg, 0)
752             val resourceId =
753                 permissionControllerContext.resources.getIdentifier(
754                     resourceName,
755                     "string",
756                     "com.android.permissioncontroller"
757                 )
758             return permissionControllerContext.getString(resourceId)
759         } catch (e: PackageManager.NameNotFoundException) {
760             throw RuntimeException(e)
761         }
762     }
763 
764     private fun assertSafetyCenterMicViewNotNull() {
765         val safetyCenterMicLabel = getPermissionControllerString(MIC_LABEL_NAME)
766         val micView = waitFindObject(byOneOfText(originalMicLabel, safetyCenterMicLabel))
767         assertNotNull(
768             "View with text '$originalMicLabel' or '$safetyCenterMicLabel' not found",
769             micView
770         )
771     }
772 
773     private fun assertSafetyCenterCameraViewNotNull() {
774         val safetyCenterCameraLabel = getPermissionControllerString(CAMERA_LABEL_NAME)
775         val cameraView = waitFindObject(byOneOfText(originalCameraLabel, safetyCenterCameraLabel))
776         assertNotNull(
777             "View with text '$originalCameraLabel' or '$safetyCenterCameraLabel' not found",
778             cameraView
779         )
780     }
781 
782     private fun byOneOfText(vararg textValues: String) =
783         By.text(Pattern.compile(textValues.joinToString(separator = "|") { Pattern.quote(it) }))
784 
785     fun isAutomotiveWithVisibleBackgroundUser(): Boolean {
786         val userManager = context.getSystemService(UserManager::class.java)
787         return packageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE) &&
788                 userManager.isVisibleBackgroundUsersSupported()
789     }
790 }
791