1 /*
2  * 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 
17 package android.permissionui.cts
18 
19 import android.app.Instrumentation
20 import android.app.UiAutomation
21 import android.content.Context
22 import android.content.pm.PackageInstaller.PACKAGE_SOURCE_DOWNLOADED_FILE
23 import android.content.pm.PackageInstaller.PACKAGE_SOURCE_LOCAL_FILE
24 import android.content.pm.PackageInstaller.PACKAGE_SOURCE_OTHER
25 import android.content.pm.PackageInstaller.PACKAGE_SOURCE_STORE
26 import android.content.pm.PackageInstaller.PACKAGE_SOURCE_UNSPECIFIED
27 import android.content.pm.PackageManager
28 import android.os.Build
29 import android.os.PersistableBundle
30 import android.os.Process
31 import android.permission.cts.CtsNotificationListenerHelperRule
32 import android.permission.cts.CtsNotificationListenerServiceUtils
33 import android.permission.cts.CtsNotificationListenerServiceUtils.getNotification
34 import android.permission.cts.CtsNotificationListenerServiceUtils.getNotificationForPackageAndId
35 import android.permission.cts.PermissionUtils
36 import android.permission.cts.TestUtils
37 import android.permissionui.cts.AppMetadata.createAppMetadataWithLocationSharingNoAds
38 import android.permissionui.cts.AppMetadata.createAppMetadataWithNoSharing
39 import android.platform.test.annotations.RequiresFlagsEnabled
40 import android.platform.test.flag.junit.DeviceFlagsValueProvider
41 import android.provider.DeviceConfig
42 import android.safetylabel.SafetyLabelConstants
43 import android.safetylabel.SafetyLabelConstants.SAFETY_LABEL_CHANGE_NOTIFICATIONS_ENABLED
44 import androidx.test.InstrumentationRegistry
45 import androidx.test.filters.FlakyTest
46 import androidx.test.filters.SdkSuppress
47 import androidx.test.uiautomator.By
48 import com.android.compatibility.common.util.DeviceConfigStateChangerRule
49 import com.android.compatibility.common.util.SystemUtil
50 import com.android.compatibility.common.util.SystemUtil.eventually
51 import com.android.compatibility.common.util.SystemUtil.waitForBroadcasts
52 import com.google.common.truth.Truth.assertThat
53 import org.junit.After
54 import org.junit.Assume
55 import org.junit.Before
56 import org.junit.ClassRule
57 import org.junit.Rule
58 import org.junit.Test
59 
60 /** End-to-end test for SafetyLabelChangesJobService. */
61 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE, codeName = "UpsideDownCake")
62 @FlakyTest
63 class SafetyLabelChangesJobServiceTest : BaseUsePermissionTest() {
64 
65     @get:Rule
66     val checkFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule()
67 
68     @get:Rule
69     val safetyLabelChangeNotificationsEnabledConfig =
70         DeviceConfigStateChangerRule(
71             context,
72             DeviceConfig.NAMESPACE_PRIVACY,
73             SafetyLabelConstants.SAFETY_LABEL_CHANGE_NOTIFICATIONS_ENABLED,
74             true.toString()
75         )
76 
77     /**
78      * This rule serves to limit the max number of safety labels that can be persisted, so that
79      * repeated tests don't overwhelm the disk storage on the device.
80      */
81     @get:Rule
82     val deviceConfigMaxSafetyLabelsPersistedPerApp =
83         DeviceConfigStateChangerRule(
84             context,
85             DeviceConfig.NAMESPACE_PRIVACY,
86             PROPERTY_MAX_SAFETY_LABELS_PERSISTED_PER_APP,
87             "2"
88         )
89 
90     @get:Rule
91     val deviceConfigDataSharingUpdatesPeriod =
92         DeviceConfigStateChangerRule(
93             BasePermissionTest.context,
94             DeviceConfig.NAMESPACE_PRIVACY,
95             PROPERTY_DATA_SHARING_UPDATE_PERIOD_MILLIS,
96             "600000"
97         )
98 
99     @Before
setupnull100     fun setup() {
101         val packageManager = context.packageManager
102         Assume.assumeFalse(packageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE))
103         Assume.assumeFalse(packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK))
104         Assume.assumeFalse(packageManager.hasSystemFeature(PackageManager.FEATURE_WATCH))
105 
106         SystemUtil.runShellCommand("input keyevent KEYCODE_WAKEUP")
107         SystemUtil.runShellCommand("wm dismiss-keyguard")
108 
109         CtsNotificationListenerServiceUtils.cancelNotifications(permissionControllerPackageName)
110         resetPermissionControllerAndSimulateReboot()
111     }
112 
113     @After
cancelJobsAndNotificationsnull114     fun cancelJobsAndNotifications() {
115         cancelJob(SAFETY_LABEL_CHANGES_DETECT_UPDATES_JOB_ID)
116         cancelJob(SAFETY_LABEL_CHANGES_PERIODIC_NOTIFICATION_JOB_ID)
117         CtsNotificationListenerServiceUtils.cancelNotifications(permissionControllerPackageName)
118     }
119 
120     @Test
runDetectUpdatesJob_initializesSafetyLabelsHistoryForAppsnull121     fun runDetectUpdatesJob_initializesSafetyLabelsHistoryForApps() {
122         installPackageNoBroadcast(APP_APK_NAME_31, createAppMetadataWithNoSharing())
123         grantLocationPermission(APP_PACKAGE_NAME)
124 
125         // Run the job to check whether the missing safety label for the above app install is
126         // identified and recorded.
127         runDetectUpdatesJob()
128         installPackageViaSession(APP_APK_NAME_31, createAppMetadataWithLocationSharingNoAds())
129         waitForBroadcasts()
130 
131         assertNotificationNotShown()
132         assertDataSharingScreenHasUpdates()
133     }
134 
135     @Test
runNotificationJob_initializesSafetyLabelsHistoryForAppsnull136     fun runNotificationJob_initializesSafetyLabelsHistoryForApps() {
137         installPackageNoBroadcast(APP_APK_NAME_31, createAppMetadataWithNoSharing())
138         grantLocationPermission(APP_PACKAGE_NAME)
139 
140         // Run the job to check whether the missing safety label for the above app install is
141         // identified and recorded.
142         runNotificationJob()
143         installPackageViaSession(APP_APK_NAME_31, createAppMetadataWithLocationSharingNoAds())
144         waitForBroadcasts()
145 
146         assertDataSharingScreenHasUpdates()
147     }
148 
149     @Test
runDetectUpdatesJob_updatesSafetyLabelHistoryForAppsnull150     fun runDetectUpdatesJob_updatesSafetyLabelHistoryForApps() {
151         installPackageViaSession(APP_APK_NAME_31, createAppMetadataWithNoSharing())
152         waitForBroadcastReceiverFinished()
153         installPackageNoBroadcast(APP_APK_NAME_31, createAppMetadataWithLocationSharingNoAds())
154         grantLocationPermission(APP_PACKAGE_NAME)
155 
156         // Run the job to check whether the missing safety label for the above app update is
157         // identified and recorded.
158         runDetectUpdatesJob()
159 
160         assertNotificationNotShown()
161         assertDataSharingScreenHasUpdates()
162     }
163 
164     @Test
runNotificationJob_updatesSafetyLabelHistoryForAppsnull165     fun runNotificationJob_updatesSafetyLabelHistoryForApps() {
166         installPackageViaSession(APP_APK_NAME_31, createAppMetadataWithNoSharing())
167         waitForBroadcastReceiverFinished()
168         installPackageNoBroadcast(APP_APK_NAME_31, createAppMetadataWithLocationSharingNoAds())
169         grantLocationPermission(APP_PACKAGE_NAME)
170 
171         // Run the job to check whether the missing safety label for the above app update is
172         // identified and recorded.
173         runNotificationJob()
174 
175         assertDataSharingScreenHasUpdates()
176     }
177 
178     @Test
runNotificationJob_whenLocationSharingUpdatesForLocationGrantedApps_showsNotificationnull179     fun runNotificationJob_whenLocationSharingUpdatesForLocationGrantedApps_showsNotification() {
180         installPackageViaSession(APP_APK_NAME_31, createAppMetadataWithNoSharing())
181         waitForBroadcasts()
182         // TODO(b/279455955): Investigate why this is necessary and remove if possible.
183         Thread.sleep(500)
184         installPackageViaSession(APP_APK_NAME_31, createAppMetadataWithLocationSharingNoAds())
185         waitForBroadcasts()
186         grantLocationPermission(APP_PACKAGE_NAME)
187 
188         runNotificationJob()
189 
190         waitForNotificationShown()
191 
192         val statusBarNotification =
193             getNotification(permissionControllerPackageName, SAFETY_LABEL_CHANGES_NOTIFICATION_ID)
194         val contentIntent = statusBarNotification!!.notification.contentIntent
195         contentIntent.send()
196 
197         assertDataSharingScreenHasUpdates()
198     }
199 
200     @Test
runNotificationJob_whenNoLocationGrantedApps_doesNotShowNotificationnull201     fun runNotificationJob_whenNoLocationGrantedApps_doesNotShowNotification() {
202         installPackageViaSession(APP_APK_NAME_31, createAppMetadataWithNoSharing())
203         waitForBroadcasts()
204         installPackageViaSession(APP_APK_NAME_31, createAppMetadataWithLocationSharingNoAds())
205         waitForBroadcasts()
206 
207         runNotificationJob()
208 
209         assertNotificationNotShown()
210     }
211 
212     @Test
runNotificationJob_whenNoLocationSharingUpdates_doesNotShowNotificationnull213     fun runNotificationJob_whenNoLocationSharingUpdates_doesNotShowNotification() {
214         installPackageViaSession(APP_APK_NAME_31, createAppMetadataWithNoSharing())
215         waitForBroadcasts()
216         grantLocationPermission(APP_PACKAGE_NAME)
217 
218         runNotificationJob()
219 
220         assertNotificationNotShown()
221     }
222 
223     @Test
runNotificationJob_packageSourceUnspecified_updatesSafetyLabelHistoryForAppsnull224     fun runNotificationJob_packageSourceUnspecified_updatesSafetyLabelHistoryForApps() {
225         installPackageViaSession(
226             APP_APK_NAME_31,
227             createAppMetadataWithNoSharing(),
228             PACKAGE_SOURCE_UNSPECIFIED
229         )
230         waitForBroadcastReceiverFinished()
231         installPackageNoBroadcast(
232             APP_APK_NAME_31,
233             createAppMetadataWithLocationSharingNoAds(),
234             PACKAGE_SOURCE_UNSPECIFIED
235         )
236         grantLocationPermission(APP_PACKAGE_NAME)
237 
238         // Run the job to check whether the missing safety label for the above app update is
239         // identified and recorded.
240         runNotificationJob()
241 
242         assertDataSharingScreenHasUpdates()
243     }
244 
245     @Test
runNotificationJob_packageSourceOther_doesNotShowNotificationnull246     fun runNotificationJob_packageSourceOther_doesNotShowNotification() {
247         installPackageViaSession(
248             APP_APK_NAME_31,
249             createAppMetadataWithNoSharing(),
250             PACKAGE_SOURCE_OTHER
251         )
252         waitForBroadcastReceiverFinished()
253         installPackageNoBroadcast(
254             APP_APK_NAME_31,
255             createAppMetadataWithLocationSharingNoAds(),
256             PACKAGE_SOURCE_OTHER
257         )
258         grantLocationPermission(APP_PACKAGE_NAME)
259 
260         // Run the job to check whether the missing safety label for the above app update is
261         // identified and recorded.
262         runNotificationJob()
263 
264         assertNotificationNotShown()
265     }
266 
267     @Test
runNotificationJob_packageSourceStore_updatesSafetyLabelHistoryForAppsnull268     fun runNotificationJob_packageSourceStore_updatesSafetyLabelHistoryForApps() {
269         installPackageViaSession(
270             APP_APK_NAME_31,
271             createAppMetadataWithNoSharing(),
272             PACKAGE_SOURCE_STORE
273         )
274         waitForBroadcastReceiverFinished()
275         installPackageNoBroadcast(
276             APP_APK_NAME_31,
277             createAppMetadataWithLocationSharingNoAds(),
278             PACKAGE_SOURCE_STORE
279         )
280         grantLocationPermission(APP_PACKAGE_NAME)
281 
282         // Run the job to check whether the missing safety label for the above app update is
283         // identified and recorded.
284         runNotificationJob()
285 
286         assertDataSharingScreenHasUpdates()
287     }
288 
289     @Test
runNotificationJob_packageSourceLocalFile_doesNotShowNotificationnull290     fun runNotificationJob_packageSourceLocalFile_doesNotShowNotification() {
291         installPackageViaSession(
292             APP_APK_NAME_31,
293             createAppMetadataWithNoSharing(),
294             PACKAGE_SOURCE_LOCAL_FILE
295         )
296         waitForBroadcastReceiverFinished()
297         installPackageNoBroadcast(
298             APP_APK_NAME_31,
299             createAppMetadataWithLocationSharingNoAds(),
300             PACKAGE_SOURCE_LOCAL_FILE
301         )
302         grantLocationPermission(APP_PACKAGE_NAME)
303 
304         // Run the job to check whether the missing safety label for the above app update is
305         // identified and recorded.
306         runNotificationJob()
307 
308         assertNotificationNotShown()
309     }
310 
311     @Test
runNotificationJob_packageSourceDownloadedFile_doesNotShowNotificationnull312     fun runNotificationJob_packageSourceDownloadedFile_doesNotShowNotification() {
313         installPackageViaSession(
314             APP_APK_NAME_31,
315             createAppMetadataWithNoSharing(),
316             PACKAGE_SOURCE_DOWNLOADED_FILE
317         )
318         waitForBroadcastReceiverFinished()
319         installPackageNoBroadcast(
320             APP_APK_NAME_31,
321             createAppMetadataWithLocationSharingNoAds(),
322             PACKAGE_SOURCE_DOWNLOADED_FILE
323         )
324         grantLocationPermission(APP_PACKAGE_NAME)
325 
326         // Run the job to check whether the missing safety label for the above app update is
327         // identified and recorded.
328         runNotificationJob()
329 
330         assertNotificationNotShown()
331     }
332 
333     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM, codeName =
334     "VanillaIceCream")
335     @RequiresFlagsEnabled(android.content.pm.Flags.FLAG_ASL_IN_APK_APP_METADATA_SOURCE)
336     @Test
runNotificationJob_packageSourceUnspecified_aslInApk_doesNotShowNotificationnull337     fun runNotificationJob_packageSourceUnspecified_aslInApk_doesNotShowNotification() {
338         installPackageViaSession(
339             APP_APK_NAME_31,
340             createAppMetadataWithNoSharing(),
341             PACKAGE_SOURCE_UNSPECIFIED
342         )
343         waitForBroadcastReceiverFinished()
344         installPackageNoBroadcast(
345             APP_APK_NAME_31_WITH_ASL,
346             packageSource = PACKAGE_SOURCE_UNSPECIFIED
347         )
348         grantLocationPermission(APP_PACKAGE_NAME)
349 
350         // Run the job to check whether the missing safety label for the above app update is
351         // identified and recorded.
352         runNotificationJob()
353 
354         assertNotificationNotShown()
355     }
356 
357     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM, codeName =
358     "VanillaIceCream")
359     @RequiresFlagsEnabled(android.content.pm.Flags.FLAG_ASL_IN_APK_APP_METADATA_SOURCE)
360     @Test
runNotificationJob_packageSourceOther_aslInApk_doesNotShowNotificationnull361     fun runNotificationJob_packageSourceOther_aslInApk_doesNotShowNotification() {
362         installPackageViaSession(
363             APP_APK_NAME_31,
364             createAppMetadataWithNoSharing(),
365             PACKAGE_SOURCE_OTHER
366         )
367         waitForBroadcastReceiverFinished()
368         installPackageNoBroadcast(
369             APP_APK_NAME_31_WITH_ASL,
370             packageSource = PACKAGE_SOURCE_OTHER
371         )
372         grantLocationPermission(APP_PACKAGE_NAME)
373 
374         // Run the job to check whether the missing safety label for the above app update is
375         // identified and recorded.
376         runNotificationJob()
377 
378         assertNotificationNotShown()
379     }
380 
381     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM, codeName =
382     "VanillaIceCream")
383     @RequiresFlagsEnabled(android.content.pm.Flags.FLAG_ASL_IN_APK_APP_METADATA_SOURCE)
384     @Test
runNotificationJob_packageSourceStore_aslInApk_doesNotShowNotificationnull385     fun runNotificationJob_packageSourceStore_aslInApk_doesNotShowNotification() {
386         installPackageViaSession(
387             APP_APK_NAME_31,
388             createAppMetadataWithNoSharing(),
389             PACKAGE_SOURCE_STORE
390         )
391         waitForBroadcastReceiverFinished()
392         installPackageNoBroadcast(
393             APP_APK_NAME_31_WITH_ASL,
394             packageSource = PACKAGE_SOURCE_STORE
395         )
396         grantLocationPermission(APP_PACKAGE_NAME)
397 
398         // Run the job to check whether the missing safety label for the above app update is
399         // identified and recorded.
400         runNotificationJob()
401 
402         assertNotificationNotShown()
403     }
404 
405     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM, codeName =
406     "VanillaIceCream")
407     @RequiresFlagsEnabled(android.content.pm.Flags.FLAG_ASL_IN_APK_APP_METADATA_SOURCE)
408     @Test
runNotificationJob_packageSourceLocalFile_aslInApk_doesNotShowNotificationnull409     fun runNotificationJob_packageSourceLocalFile_aslInApk_doesNotShowNotification() {
410         installPackageViaSession(
411             APP_APK_NAME_31,
412             createAppMetadataWithNoSharing(),
413             PACKAGE_SOURCE_LOCAL_FILE
414         )
415         waitForBroadcastReceiverFinished()
416         installPackageNoBroadcast(
417             APP_APK_NAME_31_WITH_ASL,
418             packageSource = PACKAGE_SOURCE_LOCAL_FILE
419         )
420         grantLocationPermission(APP_PACKAGE_NAME)
421 
422         // Run the job to check whether the missing safety label for the above app update is
423         // identified and recorded.
424         runNotificationJob()
425 
426         assertNotificationNotShown()
427     }
428 
429     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM, codeName =
430     "VanillaIceCream")
431     @RequiresFlagsEnabled(android.content.pm.Flags.FLAG_ASL_IN_APK_APP_METADATA_SOURCE)
432     @Test
runNotificationJob_packageSourceDownloadedFile_aslInApk_doesNotShowNotificationnull433     fun runNotificationJob_packageSourceDownloadedFile_aslInApk_doesNotShowNotification() {
434         installPackageViaSession(
435             APP_APK_NAME_31,
436             createAppMetadataWithNoSharing(),
437             PACKAGE_SOURCE_DOWNLOADED_FILE
438         )
439         waitForBroadcastReceiverFinished()
440         installPackageNoBroadcast(
441             APP_APK_NAME_31_WITH_ASL,
442             packageSource = PACKAGE_SOURCE_DOWNLOADED_FILE
443         )
444         grantLocationPermission(APP_PACKAGE_NAME)
445 
446         // Run the job to check whether the missing safety label for the above app update is
447         // identified and recorded.
448         runNotificationJob()
449 
450         assertNotificationNotShown()
451     }
452 
grantLocationPermissionnull453     private fun grantLocationPermission(packageName: String) {
454         uiAutomation.grantRuntimePermission(
455             packageName,
456             android.Manifest.permission.ACCESS_FINE_LOCATION
457         )
458     }
459 
installPackageNoBroadcastnull460     private fun installPackageNoBroadcast(
461         apkName: String,
462         appMetadata: PersistableBundle? = null,
463         packageSource: Int? = null
464     ) {
465         // Disable the safety labels feature during install to simulate installing an app without
466         // receiving an update about the change to its safety label.
467         setDeviceConfigPrivacyProperty(SAFETY_LABEL_CHANGE_NOTIFICATIONS_ENABLED, false.toString())
468         installPackageViaSession(apkName, appMetadata, packageSource)
469         waitForBroadcastReceiverFinished()
470         setDeviceConfigPrivacyProperty(SAFETY_LABEL_CHANGE_NOTIFICATIONS_ENABLED, true.toString())
471     }
472 
assertDataSharingScreenHasUpdatesnull473     private fun assertDataSharingScreenHasUpdates() {
474         startAppDataSharingUpdatesActivity()
475         try {
476             findView(By.descContains(DATA_SHARING_UPDATES), true)
477             findView(By.textContains(DATA_SHARING_UPDATES_SUBTITLE), true)
478             findView(By.textContains(UPDATES_IN_LAST_30_DAYS), true)
479             findView(By.textContains(APP_PACKAGE_NAME_SUBSTRING), true)
480             findView(By.textContains(DATA_SHARING_UPDATES_FOOTER_MESSAGE), true)
481         } finally {
482             pressBack()
483         }
484     }
485 
486     companion object {
487         private const val TIMEOUT_TIME_MS = 60_000L
488         private const val SHORT_SLEEP_MS = 2000L
489 
490         private const val SAFETY_LABEL_CHANGES_DETECT_UPDATES_JOB_ID = 8
491         private const val SAFETY_LABEL_CHANGES_PERIODIC_NOTIFICATION_JOB_ID = 9
492         private const val SET_UP_SAFETY_LABEL_CHANGES_JOB =
493             "com.android.permissioncontroller.action.SET_UP_SAFETY_LABEL_CHANGES_JOB"
494         private const val SAFETY_LABEL_CHANGES_JOB_SERVICE_RECEIVER_CLASS =
495             "com.android.permissioncontroller.permission.service.v34" +
496                 ".SafetyLabelChangesJobService\$Receiver"
497         private const val SAFETY_LABEL_CHANGES_NOTIFICATION_ID = 5
498         private const val JOB_STATUS_UNKNOWN = "unknown"
499         private const val JOB_STATUS_ACTIVE = "active"
500         private const val JOB_STATUS_WAITING = "waiting"
501 
502         private val context: Context = InstrumentationRegistry.getTargetContext()
503         private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
uiAutomationnull504         private fun uiAutomation(): UiAutomation = instrumentation.uiAutomation
505         private val permissionControllerPackageName =
506             context.packageManager.permissionControllerPackageName
507         private val userId = Process.myUserHandle().identifier
508 
509         @get:ClassRule
510         @JvmStatic
511         val ctsNotificationListenerHelper =
512             CtsNotificationListenerHelperRule(
513                 InstrumentationRegistry.getInstrumentation().targetContext
514             )
515 
516         private fun waitForNotificationShown() {
517             eventually {
518                 val notification = getNotification(false)
519                 assertThat(notification).isNotNull()
520             }
521         }
522 
assertNotificationNotShownnull523         private fun assertNotificationNotShown() {
524             eventually {
525                 val notification = getNotification(false)
526                 assertThat(notification).isNull()
527             }
528         }
529 
getNotificationnull530         private fun getNotification(cancelNotification: Boolean) =
531             getNotificationForPackageAndId(
532                     permissionControllerPackageName,
533                     SAFETY_LABEL_CHANGES_NOTIFICATION_ID,
534                     cancelNotification
535                 )
536                 ?.notification
537 
538         private fun cancelJob(jobId: Int) {
539             SystemUtil.runShellCommandOrThrow(
540                 "cmd jobscheduler cancel -u $userId $permissionControllerPackageName $jobId"
541             )
542             TestUtils.awaitJobUntilRequestedState(
543                 permissionControllerPackageName,
544                 jobId,
545                 TIMEOUT_TIME_MS,
546                 uiAutomation(),
547                 JOB_STATUS_UNKNOWN
548             )
549         }
550 
runDetectUpdatesJobnull551         private fun runDetectUpdatesJob() {
552             startJob(SAFETY_LABEL_CHANGES_DETECT_UPDATES_JOB_ID)
553             TestUtils.awaitJobUntilRequestedState(
554                 permissionControllerPackageName,
555                 SAFETY_LABEL_CHANGES_DETECT_UPDATES_JOB_ID,
556                 TIMEOUT_TIME_MS,
557                 uiAutomation(),
558                 JOB_STATUS_ACTIVE
559             )
560             TestUtils.awaitJobUntilRequestedState(
561                 permissionControllerPackageName,
562                 SAFETY_LABEL_CHANGES_DETECT_UPDATES_JOB_ID,
563                 TIMEOUT_TIME_MS,
564                 uiAutomation(),
565                 JOB_STATUS_UNKNOWN
566             )
567         }
568 
runNotificationJobnull569         private fun runNotificationJob() {
570             startJob(SAFETY_LABEL_CHANGES_PERIODIC_NOTIFICATION_JOB_ID)
571             TestUtils.awaitJobUntilRequestedState(
572                 permissionControllerPackageName,
573                 SAFETY_LABEL_CHANGES_PERIODIC_NOTIFICATION_JOB_ID,
574                 TIMEOUT_TIME_MS,
575                 uiAutomation(),
576                 JOB_STATUS_ACTIVE
577             )
578             // TODO(b/266449833): In theory we should only have to wait for "waiting" here, but
579             // sometimes jobscheduler returns "unknown".
580             TestUtils.awaitJobUntilRequestedState(
581                 permissionControllerPackageName,
582                 SAFETY_LABEL_CHANGES_PERIODIC_NOTIFICATION_JOB_ID,
583                 TIMEOUT_TIME_MS,
584                 uiAutomation(),
585                 JOB_STATUS_WAITING,
586                 JOB_STATUS_UNKNOWN
587             )
588         }
589 
startJobnull590         private fun startJob(jobId: Int) {
591             val runJobCmd =
592                 "cmd jobscheduler run -u $userId -f " + "$permissionControllerPackageName $jobId"
593             try {
594                 SystemUtil.runShellCommandOrThrow(runJobCmd)
595             } catch (e: Throwable) {
596                 throw RuntimeException(e)
597             }
598         }
599 
resetPermissionControllerAndSimulateRebootnull600         private fun resetPermissionControllerAndSimulateReboot() {
601             PermissionUtils.resetPermissionControllerJob(
602                 uiAutomation(),
603                 permissionControllerPackageName,
604                 SAFETY_LABEL_CHANGES_DETECT_UPDATES_JOB_ID,
605                 TIMEOUT_TIME_MS,
606                 SET_UP_SAFETY_LABEL_CHANGES_JOB,
607                 SAFETY_LABEL_CHANGES_JOB_SERVICE_RECEIVER_CLASS
608             )
609         }
610 
waitForBroadcastReceiverFinishednull611         private fun waitForBroadcastReceiverFinished() {
612             waitForBroadcasts()
613             // Add a short sleep to ensure that the SafetyLabelChangedBroadcastReceiver finishes its
614             // work based according to the current feature flag value before changing the flag
615             // value.
616             // While `waitForBroadcasts()` waits for broadcasts to be dispatched, it will not wait
617             // for
618             // the receivers' `onReceive` to finish.
619             Thread.sleep(SHORT_SLEEP_MS)
620         }
621     }
622 }
623