summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author TreeHugger Robot <treehugger-gerrit@google.com> 2022-10-05 15:13:05 +0000
committer Android (Google) Code Review <android-gerrit@google.com> 2022-10-05 15:13:05 +0000
commitce44e5f5b63939f34961e21b09669a27f9da9e03 (patch)
tree5166d6fd7c72e0a805669a4d966814ee4c648fea
parent4317ffc6c5a96adabbd9d3fa6063c72f0615ba30 (diff)
parent0ee0f9272a4286cf784d00fac93f44aa90c0f64d (diff)
Merge "Fix start time of unused app tracking." into tm-mainline-prod
-rw-r--r--PermissionController/AndroidManifest.xml4
-rw-r--r--PermissionController/src/com/android/permissioncontroller/hibernation/HibernationPolicy.kt195
-rw-r--r--PermissionController/tests/mocking/src/com/android/permissioncontroller/tests/mocking/hibernation/HibernationPolicyTest.kt167
3 files changed, 315 insertions, 51 deletions
diff --git a/PermissionController/AndroidManifest.xml b/PermissionController/AndroidManifest.xml
index 0256f5f98..500f522ba 100644
--- a/PermissionController/AndroidManifest.xml
+++ b/PermissionController/AndroidManifest.xml
@@ -93,11 +93,13 @@
</intent-filter>
</receiver>
- <receiver android:name="com.android.permissioncontroller.hibernation.HibernationOnBootReceiver"
+ <receiver android:name="com.android.permissioncontroller.hibernation.HibernationBroadcastReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="com.android.permissioncontroller.action.SET_UP_HIBERNATION" />
+ <action android:name="android.intent.action.TIME_SET" />
+ <action android:name="android.intent.action.TIMEZONE_CHANGED" />
</intent-filter>
</receiver>
diff --git a/PermissionController/src/com/android/permissioncontroller/hibernation/HibernationPolicy.kt b/PermissionController/src/com/android/permissioncontroller/hibernation/HibernationPolicy.kt
index 66c85f913..22c3353ff 100644
--- a/PermissionController/src/com/android/permissioncontroller/hibernation/HibernationPolicy.kt
+++ b/PermissionController/src/com/android/permissioncontroller/hibernation/HibernationPolicy.kt
@@ -52,6 +52,7 @@ import android.content.pm.PackageManager.PERMISSION_GRANTED
import android.os.Build
import android.os.Bundle
import android.os.Process
+import android.os.SystemClock
import android.os.UserHandle
import android.os.UserManager
import android.printservice.PrintService
@@ -133,9 +134,18 @@ private fun getCheckFrequencyMs() = DeviceConfig.getLong(
Utils.PROPERTY_HIBERNATION_CHECK_FREQUENCY_MILLIS,
DEFAULT_CHECK_FREQUENCY_MS)
-private const val PREF_KEY_FIRST_BOOT_TIME = "first_boot_time"
+// Intentionally kept value of the key same as before because we want to continue reading value of
+// this shared preference stored by previous versions of PermissionController
+const val PREF_KEY_START_TIME_OF_UNUSED_APP_TRACKING = "first_boot_time"
+const val PREF_KEY_BOOT_TIME_SNAPSHOT = "ah_boot_time_snapshot"
+const val PREF_KEY_ELAPSED_REALTIME_SNAPSHOT = "ah_elapsed_realtime_snapshot"
+
private const val PREFS_FILE_NAME = "unused_apps_prefs"
private const val PREF_KEY_UNUSED_APPS_REVIEW = "unused_apps_need_review"
+const val SNAPSHOT_UNINITIALIZED = -1L
+private const val ACTION_SET_UP_HIBERNATION =
+ "com.android.permissioncontroller.action.SET_UP_HIBERNATION"
+val ONE_DAY_MS = TimeUnit.DAYS.toMillis(1)
fun isHibernationEnabled(): Boolean {
return SdkLevel.isAtLeastS() &&
@@ -226,49 +236,63 @@ private fun getUnusedAppsReviewNeeded(context: Context): Boolean {
}
/**
- * Receiver of the onBoot event.
+ * Receiver of the following broadcasts:
+ * <ul>
+ * <li> {@link Intent.ACTION_BOOT_COMPLETED}
+ * <li> {@link #ACTION_SET_UP_HIBERNATION}
+ * <li> {@link Intent.ACTION_TIME_CHANGED}
+ * <li> {@link Intent.ACTION_TIMEZONE_CHANGED}
+ * </ul>
*/
-class HibernationOnBootReceiver : BroadcastReceiver() {
+class HibernationBroadcastReceiver : BroadcastReceiver() {
- override fun onReceive(context: Context, intent: Intent?) {
- if (DEBUG_HIBERNATION_POLICY) {
- DumpableLog.i(LOG_TAG, "scheduleHibernationJob " +
- "with frequency ${getCheckFrequencyMs()}ms " +
- "and threshold ${getUnusedThresholdMs()}ms")
- }
+ override fun onReceive(context: Context, intent: Intent) {
+ val action = intent.action
+ if (action == Intent.ACTION_BOOT_COMPLETED || action == ACTION_SET_UP_HIBERNATION) {
+ if (DEBUG_HIBERNATION_POLICY) {
+ DumpableLog.i(LOG_TAG, "scheduleHibernationJob " +
+ "with frequency ${getCheckFrequencyMs()}ms " +
+ "and threshold ${getUnusedThresholdMs()}ms")
+ }
- // Write first boot time if first boot
- context.firstBootTime
+ initStartTimeOfUnusedAppTracking(context.sharedPreferences)
- // If this user is a profile, then its hibernation/auto-revoke will be handled by the
- // primary user
- if (isProfile(context)) {
- if (DEBUG_HIBERNATION_POLICY) {
- DumpableLog.i(LOG_TAG, "user ${Process.myUserHandle().identifier} is a profile." +
- " Not running hibernation job.")
+ // If this user is a profile, then its hibernation/auto-revoke will be handled by the
+ // primary user
+ if (isProfile(context)) {
+ if (DEBUG_HIBERNATION_POLICY) {
+ DumpableLog.i(LOG_TAG,
+ "user ${Process.myUserHandle().identifier} is a profile." +
+ " Not running hibernation job.")
+ }
+ return
+ } else if (DEBUG_HIBERNATION_POLICY) {
+ DumpableLog.i(LOG_TAG,
+ "user ${Process.myUserHandle().identifier} is a profile" +
+ "owner. Running hibernation job.")
}
- return
- } else if (DEBUG_HIBERNATION_POLICY) {
- DumpableLog.i(LOG_TAG, "user ${Process.myUserHandle().identifier} is a profile" +
- "owner. Running hibernation job.")
- }
- if (isNewJobScheduleRequired(context)) {
- // periodic jobs normally run immediately, which is unnecessarily premature
- SKIP_NEXT_RUN = true
- val jobInfo = JobInfo.Builder(
- Constants.HIBERNATION_JOB_ID,
- ComponentName(context, HibernationJobService::class.java))
- .setPeriodic(getCheckFrequencyMs())
- // persist this job across boots
- .setPersisted(true)
- .build()
- val status = context.getSystemService(JobScheduler::class.java)!!.schedule(jobInfo)
- if (status != JobScheduler.RESULT_SUCCESS) {
- DumpableLog.e(LOG_TAG,
- "Could not schedule ${HibernationJobService::class.java.simpleName}: $status")
+ if (isNewJobScheduleRequired(context)) {
+ // periodic jobs normally run immediately, which is unnecessarily premature
+ SKIP_NEXT_RUN = true
+ val jobInfo = JobInfo.Builder(
+ Constants.HIBERNATION_JOB_ID,
+ ComponentName(context, HibernationJobService::class.java))
+ .setPeriodic(getCheckFrequencyMs())
+ // persist this job across boots
+ .setPersisted(true)
+ .build()
+ val status =
+ context.getSystemService(JobScheduler::class.java)!!.schedule(jobInfo)
+ if (status != JobScheduler.RESULT_SUCCESS) {
+ DumpableLog.e(LOG_TAG, "Could not schedule " +
+ "${HibernationJobService::class.java.simpleName}: $status")
+ }
}
}
+ if (action == Intent.ACTION_TIME_CHANGED || action == Intent.ACTION_TIMEZONE_CHANGED) {
+ adjustStartTimeOfUnusedAppTracking(context.sharedPreferences)
+ }
}
// UserManager#isProfile was already a systemAPI, linter started complaining after it
@@ -316,7 +340,7 @@ private suspend fun getAppsToHibernate(
context: Context
): Map<UserHandle, List<LightPackageInfo>> {
val now = System.currentTimeMillis()
- val firstBootTime = context.firstBootTime
+ val startTimeOfUnusedAppTracking = getStartTimeOfUnusedAppTracking(context.sharedPreferences)
val allPackagesByUser = AllPackageInfosLiveData.getInitializedValue(forceUpdate = true)
val allPackagesByUserByUid = allPackagesByUser.mapValues { (_, pkgs) ->
@@ -361,7 +385,7 @@ private suspend fun getAppsToHibernate(
lastTimePkgUsed = Math.max(lastTimePkgUsed, packageInfo.firstInstallTime)
// Limit by first boot time
- lastTimePkgUsed = Math.max(lastTimePkgUsed, firstBootTime)
+ lastTimePkgUsed = Math.max(lastTimePkgUsed, startTimeOfUnusedAppTracking)
// Handle cross-profile apps
if (context.isPackageCrossProfile(pkgName)) {
@@ -623,15 +647,86 @@ val Context.sharedPreferences: SharedPreferences
return PreferenceManager.getDefaultSharedPreferences(this)
}
-private val Context.firstBootTime: Long get() {
- var time = sharedPreferences.getLong(PREF_KEY_FIRST_BOOT_TIME, -1L)
- if (time > 0) {
- return time
+internal class SystemTime {
+ var actualSystemTime: Long = SNAPSHOT_UNINITIALIZED
+ var actualRealtime: Long = SNAPSHOT_UNINITIALIZED
+ var diffSystemTime: Long = SNAPSHOT_UNINITIALIZED
+}
+
+private fun getSystemTime(sharedPreferences: SharedPreferences): SystemTime {
+ val systemTime = SystemTime()
+ val systemTimeSnapshot = sharedPreferences.getLong(PREF_KEY_BOOT_TIME_SNAPSHOT,
+ SNAPSHOT_UNINITIALIZED)
+ if (systemTimeSnapshot == SNAPSHOT_UNINITIALIZED) {
+ DumpableLog.e(LOG_TAG, "PREF_KEY_BOOT_TIME_SNAPSHOT is not initialized")
+ return systemTime
+ }
+
+ val realtimeSnapshot = sharedPreferences.getLong(PREF_KEY_ELAPSED_REALTIME_SNAPSHOT,
+ SNAPSHOT_UNINITIALIZED)
+ if (realtimeSnapshot == SNAPSHOT_UNINITIALIZED) {
+ DumpableLog.e(LOG_TAG, "PREF_KEY_ELAPSED_REALTIME_SNAPSHOT is not initialized")
+ return systemTime
+ }
+ systemTime.actualSystemTime = System.currentTimeMillis()
+ systemTime.actualRealtime = SystemClock.elapsedRealtime()
+ val expectedSystemTime = systemTime.actualRealtime - realtimeSnapshot + systemTimeSnapshot
+ systemTime.diffSystemTime = systemTime.actualSystemTime - expectedSystemTime
+ return systemTime
+}
+
+fun getStartTimeOfUnusedAppTracking(sharedPreferences: SharedPreferences): Long {
+ val startTimeOfUnusedAppTracking = sharedPreferences.getLong(
+ PREF_KEY_START_TIME_OF_UNUSED_APP_TRACKING, SNAPSHOT_UNINITIALIZED)
+
+ // If the preference is not initialized then use the current system time.
+ if (startTimeOfUnusedAppTracking == SNAPSHOT_UNINITIALIZED) {
+ val actualSystemTime = System.currentTimeMillis()
+ sharedPreferences.edit()
+ .putLong(PREF_KEY_START_TIME_OF_UNUSED_APP_TRACKING, actualSystemTime).apply()
+ return actualSystemTime
+ }
+
+ val diffSystemTime = getSystemTime(sharedPreferences).diffSystemTime
+ // If the value stored is older than a day adjust start time.
+ if (diffSystemTime > ONE_DAY_MS) {
+ adjustStartTimeOfUnusedAppTracking(sharedPreferences)
+ }
+ return sharedPreferences.getLong(PREF_KEY_START_TIME_OF_UNUSED_APP_TRACKING,
+ SNAPSHOT_UNINITIALIZED)
+}
+
+private fun initStartTimeOfUnusedAppTracking(sharedPreferences: SharedPreferences) {
+ val systemTimeSnapshot = System.currentTimeMillis()
+ if (sharedPreferences
+ .getLong(PREF_KEY_START_TIME_OF_UNUSED_APP_TRACKING, SNAPSHOT_UNINITIALIZED)
+ == SNAPSHOT_UNINITIALIZED) {
+ sharedPreferences.edit()
+ .putLong(PREF_KEY_START_TIME_OF_UNUSED_APP_TRACKING, systemTimeSnapshot).apply()
+ }
+ val realtimeSnapshot = SystemClock.elapsedRealtime()
+ sharedPreferences.edit()
+ .putLong(PREF_KEY_BOOT_TIME_SNAPSHOT, systemTimeSnapshot)
+ .putLong(PREF_KEY_ELAPSED_REALTIME_SNAPSHOT, realtimeSnapshot)
+ .apply()
+}
+
+private fun adjustStartTimeOfUnusedAppTracking(sharedPreferences: SharedPreferences) {
+ val systemTime = getSystemTime(sharedPreferences)
+ val startTimeOfUnusedAppTracking =
+ sharedPreferences.getLong(PREF_KEY_START_TIME_OF_UNUSED_APP_TRACKING,
+ SNAPSHOT_UNINITIALIZED)
+ if (startTimeOfUnusedAppTracking == SNAPSHOT_UNINITIALIZED) {
+ DumpableLog.e(LOG_TAG, "PREF_KEY_START_TIME_OF_UNUSED_APP_TRACKING is not initialized")
+ return
}
- // This is the first boot
- time = System.currentTimeMillis()
- sharedPreferences.edit().putLong(PREF_KEY_FIRST_BOOT_TIME, time).apply()
- return time
+ val adjustedStartTimeOfUnusedAppTracking =
+ startTimeOfUnusedAppTracking + systemTime.diffSystemTime
+ sharedPreferences.edit()
+ .putLong(PREF_KEY_START_TIME_OF_UNUSED_APP_TRACKING, adjustedStartTimeOfUnusedAppTracking)
+ .putLong(PREF_KEY_BOOT_TIME_SNAPSHOT, systemTime.actualSystemTime)
+ .putLong(PREF_KEY_ELAPSED_REALTIME_SNAPSHOT, systemTime.actualRealtime)
+ .apply()
}
/**
@@ -802,8 +897,8 @@ class HibernationJobService : JobService() {
* Packages using exempt services for the current user (package-name -> list<service-interfaces>
* implemented by the package)
*/
-class ExemptServicesLiveData(private val user: UserHandle)
- : SmartUpdateMediatorLiveData<Map<String, List<String>>>() {
+class ExemptServicesLiveData(private val user: UserHandle) :
+ SmartUpdateMediatorLiveData<Map<String, List<String>>>() {
private val serviceLiveDatas: List<SmartUpdateMediatorLiveData<Set<String>>> = listOf(
ServiceLiveData[InputMethod.SERVICE_INTERFACE,
Manifest.permission.BIND_INPUT_METHOD,
@@ -880,8 +975,8 @@ class ExemptServicesLiveData(private val user: UserHandle)
/**
* Live data for whether the hibernation feature is enabled or not.
*/
-object HibernationEnabledLiveData
- : MutableLiveData<Boolean>() {
+object HibernationEnabledLiveData :
+ MutableLiveData<Boolean>() {
init {
postValue(SdkLevel.isAtLeastS() &&
DeviceConfig.getBoolean(NAMESPACE_APP_HIBERNATION,
diff --git a/PermissionController/tests/mocking/src/com/android/permissioncontroller/tests/mocking/hibernation/HibernationPolicyTest.kt b/PermissionController/tests/mocking/src/com/android/permissioncontroller/tests/mocking/hibernation/HibernationPolicyTest.kt
new file mode 100644
index 000000000..fadd35f82
--- /dev/null
+++ b/PermissionController/tests/mocking/src/com/android/permissioncontroller/tests/mocking/hibernation/HibernationPolicyTest.kt
@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2022 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.hibernation
+
+import android.app.job.JobScheduler
+import android.content.Context
+import android.content.Intent
+import android.content.SharedPreferences
+import android.os.Build
+import android.os.SystemClock
+import android.os.UserManager
+import android.preference.PreferenceManager
+import android.provider.DeviceConfig
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+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.hibernation.HibernationBroadcastReceiver
+import com.android.permissioncontroller.hibernation.ONE_DAY_MS
+import com.android.permissioncontroller.hibernation.PREF_KEY_BOOT_TIME_SNAPSHOT
+import com.android.permissioncontroller.hibernation.PREF_KEY_ELAPSED_REALTIME_SNAPSHOT
+import com.android.permissioncontroller.hibernation.PREF_KEY_START_TIME_OF_UNUSED_APP_TRACKING
+import com.android.permissioncontroller.hibernation.SNAPSHOT_UNINITIALIZED
+import com.android.permissioncontroller.hibernation.getStartTimeOfUnusedAppTracking
+import com.google.common.truth.Truth.assertThat
+import java.io.File
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations
+import org.mockito.MockitoSession
+import org.mockito.quality.Strictness
+
+/**
+ * Unit tests for [HibernationPolicy].
+ */
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S, codeName = "S")
+class HibernationPolicyTest {
+
+ companion object {
+ private val application = Mockito.mock(PermissionControllerApplication::class.java)
+ }
+
+ @Mock lateinit var jobScheduler: JobScheduler
+ @Mock lateinit var context: Context
+ @Mock lateinit var userManager: UserManager
+
+ private lateinit var realContext: Context
+ private lateinit var receiver: HibernationBroadcastReceiver
+ private lateinit var sharedPreferences: SharedPreferences
+ private lateinit var mockitoSession: MockitoSession
+ private lateinit var filesDir: File
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+ mockitoSession = ExtendedMockito.mockitoSession()
+ .mockStatic(PermissionControllerApplication::class.java)
+ .mockStatic(DeviceConfig::class.java)
+ .strictness(Strictness.LENIENT).startMocking()
+ `when`(PermissionControllerApplication.get()).thenReturn(application)
+
+ realContext = ApplicationProvider.getApplicationContext()
+ sharedPreferences =
+ PreferenceManager.getDefaultSharedPreferences(realContext.applicationContext)
+
+ `when`(context.getSharedPreferences(anyString(), anyInt())).thenReturn(sharedPreferences)
+ `when`(context.getSystemService(UserManager::class.java)).thenReturn(userManager)
+
+ filesDir = realContext.cacheDir
+ `when`(application.filesDir).thenReturn(filesDir)
+ `when`(context.getSystemService(JobScheduler::class.java)).thenReturn(jobScheduler)
+ `when`(jobScheduler.schedule(Mockito.any())).thenReturn(JobScheduler.RESULT_SUCCESS)
+
+ receiver = HibernationBroadcastReceiver()
+ }
+
+ @After
+ fun cleanup() {
+ mockitoSession.finishMocking()
+ val logFile = File(filesDir, Constants.LOGS_TO_DUMP_FILE)
+ logFile.delete()
+ }
+
+ @Test
+ fun onReceive_shouldInitializeAndAdjustStartTimeOfUnusedAppTracking() {
+ receiver.onReceive(context, Intent(Intent.ACTION_BOOT_COMPLETED))
+ val startTimeOfUnusedAppTracking =
+ sharedPreferences.getLong(PREF_KEY_START_TIME_OF_UNUSED_APP_TRACKING,
+ SNAPSHOT_UNINITIALIZED)
+ val systemTimeSnapshot =
+ sharedPreferences.getLong(PREF_KEY_BOOT_TIME_SNAPSHOT, SNAPSHOT_UNINITIALIZED)
+ val realtimeSnapshot = sharedPreferences.getLong(PREF_KEY_ELAPSED_REALTIME_SNAPSHOT,
+ SNAPSHOT_UNINITIALIZED)
+ val currentTimeMillis = System.currentTimeMillis()
+ val currentRealTime = SystemClock.elapsedRealtime()
+ assertThat(startTimeOfUnusedAppTracking).isNotEqualTo(SNAPSHOT_UNINITIALIZED)
+ assertThat(startTimeOfUnusedAppTracking).isNotEqualTo(currentTimeMillis)
+ assertThat(systemTimeSnapshot).isNotEqualTo(SNAPSHOT_UNINITIALIZED)
+ assertThat(realtimeSnapshot).isNotEqualTo(SNAPSHOT_UNINITIALIZED)
+ assertThat(systemTimeSnapshot).isLessThan(currentTimeMillis)
+ assertThat(realtimeSnapshot).isLessThan(currentRealTime)
+
+ receiver.onReceive(context, Intent(Intent.ACTION_TIME_CHANGED))
+ assertAdjustedTime(systemTimeSnapshot, realtimeSnapshot)
+
+ receiver.onReceive(context, Intent(Intent.ACTION_TIMEZONE_CHANGED))
+ assertAdjustedTime(systemTimeSnapshot, realtimeSnapshot)
+ }
+
+ @Test
+ fun getStartTimeOfUnusedAppTracking_shouldReturnExpectedValue() {
+ assertThat(getStartTimeOfUnusedAppTracking(sharedPreferences))
+ .isNotEqualTo(SNAPSHOT_UNINITIALIZED)
+ receiver.onReceive(context, Intent(Intent.ACTION_BOOT_COMPLETED))
+ val systemTimeSnapshot = sharedPreferences.getLong(PREF_KEY_BOOT_TIME_SNAPSHOT,
+ SNAPSHOT_UNINITIALIZED)
+ sharedPreferences
+ .edit()
+ .putLong(PREF_KEY_BOOT_TIME_SNAPSHOT, systemTimeSnapshot - ONE_DAY_MS)
+ .apply()
+ assertThat(getStartTimeOfUnusedAppTracking(sharedPreferences))
+ .isNotEqualTo(systemTimeSnapshot)
+ }
+
+ private fun assertAdjustedTime(
+ systemTimeSnapshot: Long,
+ realtimeSnapshot: Long
+ ) {
+ val newStartTimeOfUnusedAppTracking =
+ sharedPreferences.getLong(PREF_KEY_START_TIME_OF_UNUSED_APP_TRACKING,
+ SNAPSHOT_UNINITIALIZED)
+ val newSystemTimeSnapshot =
+ sharedPreferences.getLong(PREF_KEY_BOOT_TIME_SNAPSHOT, SNAPSHOT_UNINITIALIZED)
+ val newRealtimeSnapshot =
+ sharedPreferences.getLong(PREF_KEY_ELAPSED_REALTIME_SNAPSHOT,
+ SNAPSHOT_UNINITIALIZED)
+ assertThat(newStartTimeOfUnusedAppTracking).isNotEqualTo(SNAPSHOT_UNINITIALIZED)
+ assertThat(newSystemTimeSnapshot).isNotEqualTo(SNAPSHOT_UNINITIALIZED)
+ assertThat(newSystemTimeSnapshot).isGreaterThan(systemTimeSnapshot)
+ assertThat(newRealtimeSnapshot).isNotEqualTo(SNAPSHOT_UNINITIALIZED)
+ assertThat(newRealtimeSnapshot).isAtLeast(realtimeSnapshot)
+ }
+}