[Safety Labels] Add skeleton job + notification
Create the SafetyLabelChangesJobService. The goal of this service is to
to create a notification once per month warning the user if any safety
labels data policy changes have occurred. (And, in the future, other
tasks.)
This job is scheduled after every reboot, if and only if the
SAFETY_LABEL_CHANGE_NOTIFICATIONS_ENABLED and
PERMISSION_RATIONALE_ENABLED flags are both enabled.
Change-Id: Iff0a7e0441cdb23079458ce4824bea09749ca12f
Test: atest SafetyLabelChangesJobServiceTest
Bug: 261662686
Bug: 262751896
diff --git a/PermissionController/AndroidManifest.xml b/PermissionController/AndroidManifest.xml
index eda0d00..a155cba4 100644
--- a/PermissionController/AndroidManifest.xml
+++ b/PermissionController/AndroidManifest.xml
@@ -191,6 +191,20 @@
</intent-filter>
</receiver>
+ <service
+ android:name="com.android.permissioncontroller.permission.service.v34.SafetyLabelChangesJobService"
+ android:enabled="@bool/is_at_least_u"
+ android:permission="android.permission.BIND_JOB_SERVICE"
+ android:exported="false" />
+
+ <receiver
+ android:name="com.android.permissioncontroller.permission.service.v34.SafetyLabelChangesJobService$Receiver"
+ android:enabled="@bool/is_at_least_u"
+ android:exported="false">
+ <intent-filter>
+ <action android:name="android.intent.action.BOOT_COMPLETED" />
+ </intent-filter>
+ </receiver>
<receiver
android:name="com.android.permissioncontroller.privacysources.AccessibilityOnBootReceiver"
diff --git a/PermissionController/res/values-v34/bools.xml b/PermissionController/res/values-v34/bools.xml
new file mode 100644
index 0000000..a0e3741
--- /dev/null
+++ b/PermissionController/res/values-v34/bools.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ Copyright 2023 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<resources>
+ <bool name="is_at_least_u">true</bool>
+</resources>
diff --git a/PermissionController/res/values-v34/strings.xml b/PermissionController/res/values-v34/strings.xml
index fc4456a..844e5ff 100644
--- a/PermissionController/res/values-v34/strings.xml
+++ b/PermissionController/res/values-v34/strings.xml
@@ -167,6 +167,11 @@
<!-- Message indicating that no apps have provided recent updates. [CHAR LIMIT=80] -->
<string name="no_recent_updates">No apps have provided recent updates.</string>
+ <!-- Notification channel title for safety label changes notification [CHAR LIMIT=65] -->
+ <string name="safety_label_changes_notification_title">Safety label notification title</string>
+ <!-- Notification channel description for safety label changes notification [CHAR LIMIT=240] -->
+ <string name="safety_label_changes_notification_desc">Safety label notification description</string>
+
<!-- Safety Label Change Notifications End -->
</resources>
diff --git a/PermissionController/res/values/bools.xml b/PermissionController/res/values/bools.xml
index 17bb60c..e977618 100644
--- a/PermissionController/res/values/bools.xml
+++ b/PermissionController/res/values/bools.xml
@@ -18,4 +18,5 @@
<resources>
<bool name="is_at_least_t">false</bool>
+ <bool name="is_at_least_u">false</bool>
</resources>
diff --git a/PermissionController/src/com/android/permissioncontroller/Constants.java b/PermissionController/src/com/android/permissioncontroller/Constants.java
index 2d79549..9efa229 100644
--- a/PermissionController/src/com/android/permissioncontroller/Constants.java
+++ b/PermissionController/src/com/android/permissioncontroller/Constants.java
@@ -22,6 +22,7 @@
import com.android.permissioncontroller.hibernation.HibernationJobService;
import com.android.permissioncontroller.permission.service.PermissionEventCleanupJobService;
+import com.android.permissioncontroller.permission.service.v34.SafetyLabelChangesJobService;
/**
* App-global constants
@@ -78,6 +79,17 @@
*/
public static final int SAFETY_CENTER_BACKGROUND_REFRESH_JOB_ID = 7;
+ /**
+ * ID for the periodic job in
+ * {@link SafetyLabelChangesJobService}.
+ */
+ public static final int PERIODIC_SAFETY_LABEL_CHANGES_JOB_ID = 8;
+
+ /**
+ * ID for the on-demand, but delayed job in
+ * {@link SafetyLabelChangesJobService}.
+ */
+ public static final int SAFETY_LABEL_CHANGES_JOB_ID = 9;
/**
* Name of file to containing the packages we already showed a notification for.
@@ -118,6 +130,12 @@
public static final int ACCESSIBILITY_CHECK_NOTIFICATION_ID = 4;
/**
+ * ID for notification shown by
+ * {@link SafetyLabelChangesJobService}.
+ */
+ public static final int SAFETY_LABEL_CHANGES_NOTIFICATION_ID = 5;
+
+ /**
* String action for navigating to the auto revoke screen.
*/
public static final String ACTION_MANAGE_AUTO_REVOKE = "manageAutoRevoke";
@@ -141,8 +159,9 @@
* Channel of the notifications shown by
* {@link com.android.permissioncontroller.permission.service.LocationAccessCheck},
* {@link com.android.permissioncontroller.privacysources.NotificationListenerCheck},
- * {@link com.android.permissioncontroller.hibernation.HibernationPolicyKt}, and
- * {@link com.android.permissioncontroller.auto.DrivingDecisionReminderService}
+ * {@link com.android.permissioncontroller.hibernation.HibernationPolicyKt},
+ * {@link com.android.permissioncontroller.auto.DrivingDecisionReminderService}, and
+ * {@link SafetyLabelChangesJobService}
*/
public static final String PERMISSION_REMINDER_CHANNEL_ID = "permission reminders";
diff --git a/PermissionController/src/com/android/permissioncontroller/permission/service/v34/SafetyLabelChangesJobService.kt b/PermissionController/src/com/android/permissioncontroller/permission/service/v34/SafetyLabelChangesJobService.kt
new file mode 100644
index 0000000..dfc30b3
--- /dev/null
+++ b/PermissionController/src/com/android/permissioncontroller/permission/service/v34/SafetyLabelChangesJobService.kt
@@ -0,0 +1,207 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.permissioncontroller.permission.service.v34
+
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.job.JobInfo
+import android.app.job.JobParameters
+import android.app.job.JobScheduler
+import android.app.job.JobService
+import android.content.BroadcastReceiver
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.Intent.ACTION_BOOT_COMPLETED
+import android.os.Build
+import android.util.Log
+import androidx.annotation.RequiresApi
+import androidx.core.app.NotificationCompat
+import com.android.permissioncontroller.Constants.PERIODIC_SAFETY_LABEL_CHANGES_JOB_ID
+import com.android.permissioncontroller.Constants.PERMISSION_REMINDER_CHANNEL_ID
+import com.android.permissioncontroller.Constants.SAFETY_LABEL_CHANGES_JOB_ID
+import com.android.permissioncontroller.Constants.SAFETY_LABEL_CHANGES_NOTIFICATION_ID
+import com.android.permissioncontroller.PermissionControllerApplication
+import com.android.permissioncontroller.R
+import com.android.permissioncontroller.permission.utils.KotlinUtils
+import com.android.permissioncontroller.permission.utils.Utils.getSystemServiceSafe
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.launch
+
+/**
+ * Runs a monthly job that performs Safety Labels-related tasks, e.g., data policy changes
+ * notification, hygiene, etc.
+ */
+@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+class SafetyLabelChangesJobService : JobService() {
+ class Receiver : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ if (!KotlinUtils.isSafetyLabelChangeNotificationsEnabled()) {
+ return
+ }
+ if (intent.action != ACTION_BOOT_COMPLETED) {
+ return
+ }
+ Log.i(LOG_TAG, "Received broadcast")
+ schedulePeriodicJob(context)
+ }
+ }
+
+ /**
+ * Called twice each interval, first for the periodic job
+ * [PERIODIC_SAFETY_LABEL_CHANGES_JOB_ID], then for the main job [SAFETY_LABEL_CHANGES_JOB_ID].
+ */
+ override fun onStartJob(params: JobParameters): Boolean {
+ if (!KotlinUtils.isSafetyLabelChangeNotificationsEnabled()) {
+ return false
+ }
+ when (params.jobId) {
+ PERIODIC_SAFETY_LABEL_CHANGES_JOB_ID -> scheduleMainJobWithDelay()
+ SAFETY_LABEL_CHANGES_JOB_ID -> {
+ dispatchMainJobTask(params)
+ return true
+ }
+ else -> Log.w(LOG_TAG, "Unexpected job ID")
+ }
+ return false
+ }
+
+ private fun dispatchMainJobTask(params: JobParameters) {
+ GlobalScope.launch(Dispatchers.Default) {
+ try {
+ Log.i(LOG_TAG, "Job started")
+ runMainJob()
+ Log.i(LOG_TAG, "Job finished successfully")
+ } catch (e: Throwable) {
+ Log.e(LOG_TAG, "Job failed", e)
+ throw e
+ } finally {
+ jobFinished(params, false)
+ }
+ }
+ }
+
+ private fun runMainJob() {
+ postSafetyLabelChangedNotification()
+ }
+
+ private fun postSafetyLabelChangedNotification() {
+ if (hasDataSharingChanged()) {
+ Log.i(LOG_TAG, "Showing notification: data sharing has changed")
+ showNotification()
+ } else {
+ Log.i(LOG_TAG, "Not showing notification: data sharing has not changed")
+ }
+ }
+
+ override fun onStopJob(params: JobParameters?): Boolean = true
+
+ private fun hasDataSharingChanged(): Boolean {
+ // TODO(b/261663886): Check whether data sharing has changed
+ return true
+ }
+
+ private fun showNotification() {
+ val context = PermissionControllerApplication.get() as Context
+ val notificationManager = getSystemServiceSafe(context, NotificationManager::class.java)
+
+ createNotificationChannel(context, notificationManager)
+
+ val notification =
+ NotificationCompat.Builder(context, PERMISSION_REMINDER_CHANNEL_ID)
+ .setSmallIcon(R.drawable.ic_info)
+ .setContentTitle(
+ context.getString(R.string.safety_label_changes_notification_title))
+ .setContentText(context.getString(R.string.safety_label_changes_notification_desc))
+ .setPriority(NotificationCompat.PRIORITY_DEFAULT)
+ .setLocalOnly(true)
+ .setAutoCancel(true)
+ .setSilent(true)
+ .build()
+
+ notificationManager.notify(SAFETY_LABEL_CHANGES_NOTIFICATION_ID, notification)
+ }
+
+ private fun createNotificationChannel(
+ context: Context,
+ notificationManager: NotificationManager
+ ) {
+ val notificationChannel =
+ NotificationChannel(
+ PERMISSION_REMINDER_CHANNEL_ID,
+ context.getString(R.string.permission_reminders),
+ NotificationManager.IMPORTANCE_LOW)
+
+ notificationManager.createNotificationChannel(notificationChannel)
+ }
+
+ companion object {
+ private val LOG_TAG = SafetyLabelChangesJobService::class.java.simpleName
+
+ private fun schedulePeriodicJob(context: Context) {
+ try {
+ val jobScheduler = getSystemServiceSafe(context, JobScheduler::class.java)
+
+ if (jobScheduler.getPendingJob(PERIODIC_SAFETY_LABEL_CHANGES_JOB_ID) != null) {
+ Log.i(LOG_TAG, "Not scheduling periodic job: already scheduled")
+ return
+ }
+
+ Log.i(LOG_TAG, "Scheduling periodic job")
+ val job =
+ JobInfo.Builder(
+ PERIODIC_SAFETY_LABEL_CHANGES_JOB_ID,
+ ComponentName(context, SafetyLabelChangesJobService::class.java))
+ .setPersisted(true)
+ .setPeriodic(KotlinUtils.getSafetyLabelChangesJobIntervalMillis())
+ .build()
+ jobScheduler.schedule(job)
+ Log.i(LOG_TAG, "Periodic job scheduled successfully")
+ } catch (e: Throwable) {
+ Log.e(LOG_TAG, "Failed to schedule periodic job", e)
+ throw e
+ }
+ }
+
+ private fun scheduleMainJobWithDelay() {
+ try {
+ val context = PermissionControllerApplication.get() as Context
+ val jobScheduler = getSystemServiceSafe(context, JobScheduler::class.java)
+
+ if (jobScheduler.getPendingJob(SAFETY_LABEL_CHANGES_JOB_ID) != null) {
+ Log.i(LOG_TAG, "Not scheduling job: already scheduled")
+ return
+ }
+
+ Log.i(LOG_TAG, "Scheduling job")
+ val job =
+ JobInfo.Builder(
+ SAFETY_LABEL_CHANGES_JOB_ID,
+ ComponentName(context, SafetyLabelChangesJobService::class.java))
+ .setPersisted(true)
+ .setMinimumLatency(KotlinUtils.getSafetyLabelChangesJobDelayMillis())
+ .build()
+ jobScheduler.schedule(job)
+ Log.i(LOG_TAG, "Job scheduled successfully")
+ } catch (e: Throwable) {
+ Log.e(LOG_TAG, "Failed to schedule job", e)
+ throw e
+ }
+ }
+ }
+}
diff --git a/PermissionController/src/com/android/permissioncontroller/permission/service/v34/package-info.java b/PermissionController/src/com/android/permissioncontroller/permission/service/v34/package-info.java
new file mode 100644
index 0000000..0625654
--- /dev/null
+++ b/PermissionController/src/com/android/permissioncontroller/permission/service/v34/package-info.java
@@ -0,0 +1,18 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@androidx.annotation.RequiresApi(android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+package com.android.permissioncontroller.permission.service.v34;
diff --git a/PermissionController/src/com/android/permissioncontroller/permission/utils/KotlinUtils.kt b/PermissionController/src/com/android/permissioncontroller/permission/utils/KotlinUtils.kt
index 087fe3d..7dea71a 100644
--- a/PermissionController/src/com/android/permissioncontroller/permission/utils/KotlinUtils.kt
+++ b/PermissionController/src/com/android/permissioncontroller/permission/utils/KotlinUtils.kt
@@ -81,6 +81,7 @@
import com.android.permissioncontroller.permission.model.livedatatypes.PermState
import com.android.permissioncontroller.permission.service.LocationAccessCheck
import com.android.permissioncontroller.permission.ui.handheld.SettingsWithLargeHeader
+import java.time.Duration
import java.util.concurrent.atomic.AtomicReference
import kotlin.coroutines.Continuation
import kotlin.coroutines.CoroutineContext
@@ -150,6 +151,17 @@
private const val PROPERTY_PHOTO_PICKER_PROMPT_ENABLED = "photo_picker_prompt_enabled"
/**
+ * The minimum amount of time to wait, after scheduling the safety label changes job, before
+ * the job actually runs for the first time.
+ */
+ private const val PROPERTY_SAFETY_LABEL_CHANGES_JOB_DELAY_MILLIS =
+ "safety_label_changes_job_delay_millis"
+
+ /** How often the safety label changes job service will run its job. */
+ private const val PROPERTY_SAFETY_LABEL_CHANGES_JOB_INTERVAL_MILLIS =
+ "safety_label_changes_job_interval_millis"
+
+ /**
* Whether the Permissions Hub 2 flag is enabled
*
* @return whether the flag is enabled
@@ -287,6 +299,27 @@
}
/**
+ * The minimum amount of time to wait, after scheduling the safety label changes job, before
+ * the job actually runs for the first time.
+ */
+ @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.UPSIDE_DOWN_CAKE, codename = "UpsideDownCake")
+ fun getSafetyLabelChangesJobDelayMillis(): Long {
+ return DeviceConfig.getLong(
+ DeviceConfig.NAMESPACE_PRIVACY,
+ PROPERTY_SAFETY_LABEL_CHANGES_JOB_DELAY_MILLIS,
+ Duration.ofMinutes(30).toMillis())
+ }
+
+ /** How often the safety label changes job will run. */
+ @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.UPSIDE_DOWN_CAKE, codename = "UpsideDownCake")
+ fun getSafetyLabelChangesJobIntervalMillis(): Long {
+ return DeviceConfig.getLong(
+ DeviceConfig.NAMESPACE_PRIVACY,
+ PROPERTY_SAFETY_LABEL_CHANGES_JOB_INTERVAL_MILLIS,
+ Duration.ofDays(30).toMillis())
+ }
+
+ /**
* Given a Map, and a List, determines which elements are in the list, but not the map, and
* vice versa. Used primarily for determining which liveDatas are already being watched, and
* which need to be removed or added
diff --git a/PermissionController/tests/mocking/src/com/android/permissioncontroller/tests/mocking/safetylabel/SafetyLabelChangesJobServiceTest.kt b/PermissionController/tests/mocking/src/com/android/permissioncontroller/tests/mocking/safetylabel/SafetyLabelChangesJobServiceTest.kt
new file mode 100644
index 0000000..9297c68
--- /dev/null
+++ b/PermissionController/tests/mocking/src/com/android/permissioncontroller/tests/mocking/safetylabel/SafetyLabelChangesJobServiceTest.kt
@@ -0,0 +1,193 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.permissioncontroller.tests.mocking.safetylabel
+
+import android.app.Notification
+import android.app.NotificationManager
+import android.app.job.JobInfo
+import android.app.job.JobParameters
+import android.app.job.JobScheduler
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import android.provider.DeviceConfig
+import android.safetylabel.SafetyLabelConstants
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.filters.SdkSuppress
+import com.android.dx.mockito.inline.extended.ExtendedMockito
+import com.android.permissioncontroller.Constants
+import com.android.permissioncontroller.PermissionControllerApplication
+import com.android.permissioncontroller.permission.service.v34.SafetyLabelChangesJobService
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mock
+import org.mockito.Mockito.doNothing
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.timeout
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyZeroInteractions
+import org.mockito.Mockito.`when` as whenever
+import org.mockito.MockitoAnnotations
+import org.mockito.MockitoSession
+import org.mockito.Spy
+import org.mockito.quality.Strictness
+
+/** Tests for [SafetyLabelChangesJobService]. */
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE, codeName = "UpsideDownCake")
+class SafetyLabelChangesJobServiceTest {
+ @Spy private val service = SafetyLabelChangesJobService()
+ private val receiver = SafetyLabelChangesJobService.Receiver()
+ private val context: Context = ApplicationProvider.getApplicationContext()
+
+ private lateinit var mockitoSession: MockitoSession
+
+ @Mock private lateinit var application: PermissionControllerApplication
+
+ @Mock private lateinit var mockJobScheduler: JobScheduler
+
+ @Mock private lateinit var mockNotificationManager: NotificationManager
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+
+ mockitoSession =
+ ExtendedMockito.mockitoSession()
+ .mockStatic(DeviceConfig::class.java)
+ .mockStatic(PermissionControllerApplication::class.java)
+ .strictness(Strictness.LENIENT)
+ .startMocking()
+
+ // Mock flags
+ setSafetyLabelChangeNotificationsEnabled(true)
+ setPermissionRationaleEnabled(true)
+
+ // Mock application context
+ whenever(PermissionControllerApplication.get()).thenReturn(application)
+ whenever(application.resources).thenReturn(context.resources)
+ whenever(application.applicationInfo).thenReturn(context.applicationInfo)
+
+ // Mock services
+ whenever(application.getSystemService(eq(NotificationManager::class.java)))
+ .thenReturn(mockNotificationManager)
+ whenever(application.getSystemService(eq(JobScheduler::class.java)))
+ .thenReturn(mockJobScheduler)
+ doNothing().`when`(service).jobFinished(any(), anyBoolean())
+ }
+
+ @After
+ fun cleanup() {
+ mockitoSession.finishMocking()
+ }
+
+ @Test
+ fun flagsDisabled_onReceiveValidIntentAction_jobNotScheduled() {
+ setSafetyLabelChangeNotificationsEnabled(false)
+
+ receiver.onReceive(application, Intent(Intent.ACTION_BOOT_COMPLETED))
+
+ verifyZeroInteractions(mockJobScheduler)
+ }
+
+ @Test
+ fun flagsDisabled_onMainJobStart_notificationNotShown() {
+ setSafetyLabelChangeNotificationsEnabled(false)
+
+ val jobId = mockJobParamsForJobId(Constants.SAFETY_LABEL_CHANGES_JOB_ID)
+ val jobStillRunning = service.onStartJob(jobId)
+
+ assertThat(jobStillRunning).isEqualTo(false)
+
+ verifyZeroInteractions(mockNotificationManager)
+ }
+
+ @Test
+ fun onReceiveInvalidIntentAction_jobNotScheduled() {
+ receiver.onReceive(application, Intent(Intent.ACTION_DEFAULT))
+
+ verifyZeroInteractions(mockJobScheduler)
+ }
+
+ @Test
+ fun onReceiveValidIntentAction_periodicJobScheduled() {
+ receiver.onReceive(application, Intent(Intent.ACTION_BOOT_COMPLETED))
+
+ val captor = ArgumentCaptor.forClass(JobInfo::class.java)
+ verify(mockJobScheduler).schedule(captor.capture())
+ assertThat(captor.value.id).isEqualTo(Constants.PERIODIC_SAFETY_LABEL_CHANGES_JOB_ID)
+ }
+
+ @Test
+ fun onStartPeriodicJob_scheduleJob() {
+ val jobParams = mockJobParamsForJobId(Constants.PERIODIC_SAFETY_LABEL_CHANGES_JOB_ID)
+ val jobStillRunning = service.onStartJob(jobParams)
+
+ assertThat(jobStillRunning).isEqualTo(false)
+
+ val captor = ArgumentCaptor.forClass(JobInfo::class.java)
+ verify(mockJobScheduler, timeout(5000)).schedule(captor.capture())
+ assertThat(captor.value.id).isEqualTo(Constants.SAFETY_LABEL_CHANGES_JOB_ID)
+ }
+
+ @Test
+ fun onStartMainJob_notificationShown() {
+ val jobParams = mockJobParamsForJobId(Constants.SAFETY_LABEL_CHANGES_JOB_ID)
+ val jobStillRunning = service.onStartJob(jobParams)
+
+ assertThat(jobStillRunning).isEqualTo(true)
+ waitForJobFinished()
+
+ val captor = ArgumentCaptor.forClass(Notification::class.java)
+ verify(mockNotificationManager)
+ .notify(eq(Constants.SAFETY_LABEL_CHANGES_NOTIFICATION_ID), captor.capture())
+ // TODO(b/261662686): Assert notification title and content
+ }
+
+ private fun waitForJobFinished() {
+ verify(service, timeout(5000)).jobFinished(any(), anyBoolean())
+ }
+
+ private fun mockJobParamsForJobId(jobId: Int): JobParameters {
+ val jobParameters = mock(JobParameters::class.java)
+ whenever(jobParameters.jobId).thenReturn(jobId)
+ return jobParameters
+ }
+
+ private fun setSafetyLabelChangeNotificationsEnabled(flagValue: Boolean) {
+ whenever(
+ DeviceConfig.getBoolean(
+ eq(DeviceConfig.NAMESPACE_PRIVACY),
+ eq(SafetyLabelConstants.SAFETY_LABEL_CHANGE_NOTIFICATIONS_ENABLED),
+ anyBoolean()))
+ .thenReturn(flagValue)
+ }
+
+ private fun setPermissionRationaleEnabled(flagValue: Boolean) {
+ whenever(
+ DeviceConfig.getBoolean(
+ eq(DeviceConfig.NAMESPACE_PRIVACY),
+ eq(SafetyLabelConstants.PERMISSION_RATIONALE_ENABLED),
+ anyBoolean()))
+ .thenReturn(flagValue)
+ }
+}
\ No newline at end of file