Add AppOptimizationModeEventsUtils to save & update app optimization mode expiration events.

- [Update] Save app optimizaiton mode set & expire events from turbo.
- [Reset ] Restore optimization mode for expired events in Periodic job.
- [Delete] Cancel expiration event if user updates mode in app usage page.

Bug: 338965652
Test: atest + manual
Change-Id: I3fb7311207da1bdb1146ea1ff041aca6adb66052
diff --git a/Android.bp b/Android.bp
index 06ce8ab..b96d0dc 100644
--- a/Android.bp
+++ b/Android.bp
@@ -95,15 +95,11 @@
         "SettingsLibActivityEmbedding",
         "aconfig_settings_flags_lib",
         "accessibility_settings_flags_lib",
-        "app-usage-event-protos-lite",
-        "battery-event-protos-lite",
-        "battery-usage-slot-protos-lite",
         "contextualcards",
         "development_settings_flag_lib",
         "factory_reset_flags_lib",
         "fuelgauge-log-protos-lite",
-        "fuelgauge-usage-state-protos-lite",
-        "power-anomaly-event-protos-lite",
+        "fuelgauge-protos-lite",
         "settings-contextual-card-protos-lite",
         "settings-log-bridge-protos-lite",
         "settings-logtags",
diff --git a/protos/fuelgauge_log.proto b/protos/fuelgauge_log.proto
index b16958d..3be173e 100644
--- a/protos/fuelgauge_log.proto
+++ b/protos/fuelgauge_log.proto
@@ -21,6 +21,7 @@
     BACKUP = 5;
     FORCE_RESET = 6;
     EXTERNAL_UPDATE = 7;
+    EXPIRATION_RESET = 8;
   }
 
   optional string package_name = 1;
diff --git a/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetail.java b/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetail.java
index 42e6d9c..005c073 100644
--- a/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetail.java
+++ b/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetail.java
@@ -43,6 +43,7 @@
 import com.android.settings.core.SubSettingLauncher;
 import com.android.settings.dashboard.DashboardFragment;
 import com.android.settings.fuelgauge.BatteryOptimizeHistoricalLogEntry.Action;
+import com.android.settings.fuelgauge.batteryusage.AppOptModeSharedPreferencesUtils;
 import com.android.settings.fuelgauge.batteryusage.BatteryDiffEntry;
 import com.android.settings.fuelgauge.batteryusage.BatteryEntry;
 import com.android.settings.overlay.FeatureFactory;
@@ -274,9 +275,12 @@
         final int currentOptimizeMode = mBatteryOptimizeUtils.getAppOptimizationMode();
         mLogStringBuilder.append(", onPause mode = ").append(currentOptimizeMode);
         logMetricCategory(currentOptimizeMode);
-
         mExecutor.execute(
                 () -> {
+                    if (currentOptimizeMode != mOptimizationMode) {
+                        AppOptModeSharedPreferencesUtils.deleteAppOptimizationModeEventByUid(
+                                getContext(), mBatteryOptimizeUtils.getUid());
+                    }
                     BatteryOptimizeLogUtils.writeLog(
                             getContext().getApplicationContext(),
                             Action.LEAVE,
diff --git a/src/com/android/settings/fuelgauge/BatteryOptimizeUtils.java b/src/com/android/settings/fuelgauge/BatteryOptimizeUtils.java
index 9c7f007..3e37618 100644
--- a/src/com/android/settings/fuelgauge/BatteryOptimizeUtils.java
+++ b/src/com/android/settings/fuelgauge/BatteryOptimizeUtils.java
@@ -182,6 +182,14 @@
                 && getAppOptimizationMode() != BatteryOptimizeUtils.MODE_RESTRICTED;
     }
 
+    String getPackageName() {
+        return mPackageName == null ? UNKNOWN_PACKAGE : mPackageName;
+    }
+
+    int getUid() {
+        return mUid;
+    }
+
     /** Gets the list of installed applications. */
     public static ArraySet<ApplicationInfo> getInstalledApplications(
             Context context, IPackageManager ipm) {
@@ -257,10 +265,6 @@
         }
     }
 
-    String getPackageName() {
-        return mPackageName == null ? UNKNOWN_PACKAGE : mPackageName;
-    }
-
     static int getMode(AppOpsManager appOpsManager, int uid, String packageName) {
         return appOpsManager.checkOpNoThrow(
                 AppOpsManager.OP_RUN_ANY_IN_BACKGROUND, uid, packageName);
diff --git a/src/com/android/settings/fuelgauge/PowerBackgroundUsageDetail.java b/src/com/android/settings/fuelgauge/PowerBackgroundUsageDetail.java
index b662d3e..2d2c838 100644
--- a/src/com/android/settings/fuelgauge/PowerBackgroundUsageDetail.java
+++ b/src/com/android/settings/fuelgauge/PowerBackgroundUsageDetail.java
@@ -35,6 +35,7 @@
 import com.android.settings.R;
 import com.android.settings.core.SubSettingLauncher;
 import com.android.settings.dashboard.DashboardFragment;
+import com.android.settings.fuelgauge.batteryusage.AppOptModeSharedPreferencesUtils;
 import com.android.settings.overlay.FeatureFactory;
 import com.android.settings.widget.EntityHeaderController;
 import com.android.settingslib.HelpUtils;
@@ -121,6 +122,10 @@
 
         mExecutor.execute(
                 () -> {
+                    if (currentOptimizeMode != mOptimizationMode) {
+                        AppOptModeSharedPreferencesUtils.deleteAppOptimizationModeEventByUid(
+                                getContext(), mBatteryOptimizeUtils.getUid());
+                    }
                     BatteryOptimizeLogUtils.writeLog(
                             getContext().getApplicationContext(),
                             Action.LEAVE,
diff --git a/src/com/android/settings/fuelgauge/batteryusage/AppOptModeSharedPreferencesUtils.kt b/src/com/android/settings/fuelgauge/batteryusage/AppOptModeSharedPreferencesUtils.kt
new file mode 100644
index 0000000..60db031
--- /dev/null
+++ b/src/com/android/settings/fuelgauge/batteryusage/AppOptModeSharedPreferencesUtils.kt
@@ -0,0 +1,223 @@
+/*
+ * Copyright (C) 2024 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.settings.fuelgauge.batteryusage
+
+import android.content.Context
+import android.content.SharedPreferences
+import android.util.ArrayMap
+import android.util.Base64
+import android.util.Log
+import androidx.annotation.VisibleForTesting
+import com.android.settings.fuelgauge.BatteryOptimizeHistoricalLogEntry.Action
+import com.android.settings.fuelgauge.BatteryOptimizeUtils
+import com.android.settings.fuelgauge.BatteryUtils
+
+/** A util to store and update app optimization mode expiration event data. */
+object AppOptModeSharedPreferencesUtils {
+    private const val TAG: String = "AppOptModeSharedPreferencesUtils"
+    private const val SHARED_PREFS_FILE: String = "app_optimization_mode_shared_prefs"
+
+    @VisibleForTesting const val UNLIMITED_EXPIRE_TIME: Long = -1L
+
+    private val appOptimizationModeLock = Any()
+    private val defaultInstance = AppOptimizationModeEvent.getDefaultInstance()
+
+    /** Returns all app optimization mode events for log. */
+    @JvmStatic
+    fun getAllEvents(context: Context): List<AppOptimizationModeEvent> =
+        synchronized(appOptimizationModeLock) { getAppOptModeEventsMap(context).values.toList() }
+
+    /** Updates the app optimization mode event data. */
+    @JvmStatic
+    fun updateAppOptModeExpiration(
+        context: Context,
+        uids: List<Int>,
+        packageNames: List<String>,
+        optimizationModes: List<Int>,
+        expirationTimes: LongArray,
+    ) =
+        // The internal fun with an additional lambda parameter is used to
+        // 1) get true BatteryOptimizeUtils in production environment
+        // 2) get fake BatteryOptimizeUtils for testing environment
+        updateAppOptModeExpirationInternal(
+            context,
+            uids,
+            packageNames,
+            optimizationModes,
+            expirationTimes
+        ) { uid: Int, packageName: String ->
+            BatteryOptimizeUtils(context, uid, packageName)
+        }
+
+    /** Resets the app optimization mode event data since the query timestamp. */
+    @JvmStatic
+    fun resetExpiredAppOptModeBeforeTimestamp(context: Context, queryTimestamp: Long) =
+        synchronized(appOptimizationModeLock) {
+            val eventsMap = getAppOptModeEventsMap(context)
+            val expirationUids = ArrayList<Int>(eventsMap.size)
+            for ((uid, event) in eventsMap) {
+                if (event.expirationTime > queryTimestamp) {
+                    continue
+                }
+                updateBatteryOptimizationMode(
+                    context,
+                    event.uid,
+                    event.packageName,
+                    event.resetOptimizationMode,
+                    Action.EXPIRATION_RESET,
+                )
+                expirationUids.add(uid)
+            }
+            // Remove the expired AppOptimizationModeEvent data from storage
+            clearSharedPreferences(context, expirationUids)
+        }
+
+    /** Deletes all app optimization mode event data with a specific uid. */
+    @JvmStatic
+    fun deleteAppOptimizationModeEventByUid(context: Context, uid: Int) =
+        synchronized(appOptimizationModeLock) { clearSharedPreferences(context, listOf(uid)) }
+
+    @VisibleForTesting
+    fun updateAppOptModeExpirationInternal(
+        context: Context,
+        uids: List<Int>,
+        packageNames: List<String>,
+        optimizationModes: List<Int>,
+        expirationTimes: LongArray,
+        getBatteryOptimizeUtils: (Int, String) -> BatteryOptimizeUtils
+    ) =
+        synchronized(appOptimizationModeLock) {
+            val eventsMap = getAppOptModeEventsMap(context)
+            val expirationEvents: MutableMap<Int, AppOptimizationModeEvent> = ArrayMap()
+            for (i in uids.indices) {
+                val uid = uids[i]
+                val packageName = packageNames[i]
+                val optimizationMode = optimizationModes[i]
+                val originalOptMode: Int =
+                    updateBatteryOptimizationMode(
+                        context,
+                        uid,
+                        packageName,
+                        optimizationMode,
+                        Action.EXTERNAL_UPDATE,
+                        getBatteryOptimizeUtils(uid, packageName)
+                    )
+                if (originalOptMode == BatteryOptimizeUtils.MODE_UNKNOWN) {
+                    continue
+                }
+                // Make sure the reset mode is consistent with the expiration event in storage.
+                val resetOptMode = eventsMap[uid]?.resetOptimizationMode ?: originalOptMode
+                val expireTimeMs: Long = expirationTimes[i]
+                if (expireTimeMs != UNLIMITED_EXPIRE_TIME) {
+                    Log.d(
+                        TAG,
+                        "setOptimizationMode($packageName) from $originalOptMode " +
+                            "to $optimizationMode with expiration time $expireTimeMs",
+                    )
+                    expirationEvents[uid] =
+                        AppOptimizationModeEvent.newBuilder()
+                            .setUid(uid)
+                            .setPackageName(packageName)
+                            .setResetOptimizationMode(resetOptMode)
+                            .setExpirationTime(expireTimeMs)
+                            .build()
+                }
+            }
+
+            // Append and update the AppOptimizationModeEvent.
+            if (expirationEvents.isNotEmpty()) {
+                updateSharedPreferences(context, expirationEvents)
+            }
+        }
+
+    @VisibleForTesting
+    fun updateBatteryOptimizationMode(
+        context: Context,
+        uid: Int,
+        packageName: String,
+        optimizationMode: Int,
+        action: Action,
+        batteryOptimizeUtils: BatteryOptimizeUtils = BatteryOptimizeUtils(context, uid, packageName)
+    ): Int {
+        if (!batteryOptimizeUtils.isOptimizeModeMutable) {
+            Log.w(TAG, "Fail to update immutable optimization mode for: $packageName")
+            return BatteryOptimizeUtils.MODE_UNKNOWN
+        }
+        val currentOptMode = batteryOptimizeUtils.appOptimizationMode
+        batteryOptimizeUtils.setAppUsageState(optimizationMode, action)
+        Log.d(
+            TAG,
+            "setAppUsageState($packageName) to $optimizationMode with action = ${action.name}",
+        )
+        return currentOptMode
+    }
+
+    private fun getSharedPreferences(context: Context): SharedPreferences {
+        return context.applicationContext.getSharedPreferences(
+            SHARED_PREFS_FILE,
+            Context.MODE_PRIVATE,
+        )
+    }
+
+    private fun getAppOptModeEventsMap(context: Context): ArrayMap<Int, AppOptimizationModeEvent> {
+        val sharedPreferences = getSharedPreferences(context)
+        val allKeys = sharedPreferences.all?.keys ?: emptySet()
+        if (allKeys.isEmpty()) {
+            return ArrayMap()
+        }
+        val eventsMap = ArrayMap<Int, AppOptimizationModeEvent>(allKeys.size)
+        for (key in allKeys) {
+            sharedPreferences.getString(key, null)?.let {
+                eventsMap[key.toInt()] = deserializeAppOptimizationModeEvent(it)
+            }
+        }
+        return eventsMap
+    }
+
+    private fun updateSharedPreferences(
+        context: Context,
+        eventsMap: Map<Int, AppOptimizationModeEvent>
+    ) {
+        val sharedPreferences = getSharedPreferences(context)
+        sharedPreferences.edit().run {
+            for ((uid, event) in eventsMap) {
+                putString(uid.toString(), serializeAppOptimizationModeEvent(event))
+            }
+            apply()
+        }
+    }
+
+    private fun clearSharedPreferences(context: Context, uids: List<Int>) {
+        val sharedPreferences = getSharedPreferences(context)
+        sharedPreferences.edit().run {
+            for (uid in uids) {
+                remove(uid.toString())
+            }
+            apply()
+        }
+    }
+
+    private fun serializeAppOptimizationModeEvent(event: AppOptimizationModeEvent): String {
+        return Base64.encodeToString(event.toByteArray(), Base64.DEFAULT)
+    }
+
+    private fun deserializeAppOptimizationModeEvent(
+        encodedProtoString: String
+    ): AppOptimizationModeEvent {
+        return BatteryUtils.parseProtoFromString(encodedProtoString, defaultInstance)
+    }
+}
diff --git a/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageDataLoader.java b/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageDataLoader.java
index 26bb6dd..0836912 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageDataLoader.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageDataLoader.java
@@ -167,6 +167,8 @@
         try {
             final long start = System.currentTimeMillis();
             loadBatteryStatsData(context, isFullChargeStart);
+            AppOptModeSharedPreferencesUtils.resetExpiredAppOptModeBeforeTimestamp(
+                    context, System.currentTimeMillis());
             if (!isFullChargeStart) {
                 // No app usage data or battery diff data at this time.
                 final UserIdsSeries userIdsSeries =
diff --git a/src/com/android/settings/fuelgauge/protos/Android.bp b/src/com/android/settings/fuelgauge/protos/Android.bp
index 462962b..40fb987 100644
--- a/src/com/android/settings/fuelgauge/protos/Android.bp
+++ b/src/com/android/settings/fuelgauge/protos/Android.bp
@@ -9,41 +9,9 @@
 }
 
 java_library {
-    name: "app-usage-event-protos-lite",
+    name: "fuelgauge-protos-lite",
     proto: {
         type: "lite",
     },
-    srcs: ["app_usage_event.proto"],
-}
-
-java_library {
-    name: "battery-event-protos-lite",
-    proto: {
-        type: "lite",
-    },
-    srcs: ["battery_event.proto"],
-}
-
-java_library {
-    name: "battery-usage-slot-protos-lite",
-    proto: {
-        type: "lite",
-    },
-    srcs: ["battery_usage_slot.proto"],
-}
-
-java_library {
-    name: "fuelgauge-usage-state-protos-lite",
-    proto: {
-        type: "lite",
-    },
-    srcs: ["fuelgauge_usage_state.proto"],
-}
-
-java_library {
-    name: "power-anomaly-event-protos-lite",
-    proto: {
-        type: "lite",
-    },
-    srcs: ["power_anomaly_event.proto"],
+    srcs: ["*.proto"],
 }
diff --git a/src/com/android/settings/fuelgauge/protos/app_optimization_mode_event.proto b/src/com/android/settings/fuelgauge/protos/app_optimization_mode_event.proto
new file mode 100644
index 0000000..81d51bc
--- /dev/null
+++ b/src/com/android/settings/fuelgauge/protos/app_optimization_mode_event.proto
@@ -0,0 +1,18 @@
+syntax = "proto3";
+
+option java_multiple_files = true;
+option java_package = "com.android.settings.fuelgauge.batteryusage";
+option java_outer_classname = "AppOptimizationModeEventProto";
+
+message AppOptimizationModeEvents {
+  // Map of uid to AppOptimizationModeEvent
+  map<int32, AppOptimizationModeEvent> events = 1;
+}
+
+message AppOptimizationModeEvent {
+  optional int32 uid = 1;
+  optional string package_name = 2;
+  // Value of BatteryUsageSlot.BatteryOptimizationMode, range = [0,3]
+  optional int32 reset_optimization_mode = 3;
+  optional int64 expiration_time = 4;
+}
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/AppOptModeSharedPreferencesUtilsTest.kt b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/AppOptModeSharedPreferencesUtilsTest.kt
new file mode 100644
index 0000000..02d3739
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/AppOptModeSharedPreferencesUtilsTest.kt
@@ -0,0 +1,212 @@
+/*
+ * Copyright (C) 2024 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.settings.fuelgauge.batteryusage
+
+import android.content.Context
+import androidx.test.core.app.ApplicationProvider
+import com.android.settings.fuelgauge.BatteryOptimizeHistoricalLogEntry.Action
+import com.android.settings.fuelgauge.BatteryOptimizeUtils
+import com.android.settings.fuelgauge.BatteryOptimizeUtils.MODE_OPTIMIZED
+import com.android.settings.fuelgauge.BatteryOptimizeUtils.MODE_RESTRICTED
+import com.android.settings.fuelgauge.BatteryOptimizeUtils.MODE_UNKNOWN
+import com.android.settings.fuelgauge.BatteryOptimizeUtils.MODE_UNRESTRICTED
+import com.android.settings.fuelgauge.batteryusage.AppOptModeSharedPreferencesUtils.UNLIMITED_EXPIRE_TIME
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when` as whenever
+import org.mockito.Spy
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+import org.robolectric.RobolectricTestRunner
+
+@RunWith(RobolectricTestRunner::class)
+class AppOptModeSharedPreferencesUtilsTest {
+    @JvmField @Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()
+
+    @Spy private var context: Context = ApplicationProvider.getApplicationContext()
+
+    @Spy
+    private var testBatteryOptimizeUtils = spy(BatteryOptimizeUtils(context, UID, PACKAGE_NAME))
+
+    @Before
+    fun setup() {
+        AppOptModeSharedPreferencesUtils.deleteAppOptimizationModeEventByUid(context, UID)
+    }
+
+    @Test
+    fun getAllEvents_emptyData_verifyEmptyList() {
+        assertThat(AppOptModeSharedPreferencesUtils.getAllEvents(context)).isEmpty()
+    }
+
+    @Test
+    fun updateAppOptModeExpirationInternal_withExpirationTime_verifyData() {
+        insertAppOptModeEventForTest(/* expirationTime= */ 1000L)
+
+        val events = AppOptModeSharedPreferencesUtils.getAllEvents(context)
+
+        assertThat(events.size).isEqualTo(1)
+        assertAppOptimizationModeEventInfo(events.get(0), UID, PACKAGE_NAME, MODE_OPTIMIZED, 1000L)
+    }
+
+    @Test
+    fun updateAppOptModeExpirationInternal_withoutExpirationTime_verifyEmptyList() {
+        insertAppOptModeEventForTest(/* expirationTime= */ UNLIMITED_EXPIRE_TIME)
+
+        assertThat(AppOptModeSharedPreferencesUtils.getAllEvents(context)).isEmpty()
+    }
+
+    @Test
+    fun deleteAppOptimizationModeEventByUid_uidNotContained_verifyData() {
+        insertAppOptModeEventForTest(/* expirationTime= */ 1000L)
+        assertThat(AppOptModeSharedPreferencesUtils.getAllEvents(context).size).isEqualTo(1)
+
+        AppOptModeSharedPreferencesUtils.deleteAppOptimizationModeEventByUid(context, UNSET_UID)
+        val events = AppOptModeSharedPreferencesUtils.getAllEvents(context)
+
+        assertThat(events.size).isEqualTo(1)
+        assertAppOptimizationModeEventInfo(events.get(0), UID, PACKAGE_NAME, MODE_OPTIMIZED, 1000L)
+    }
+
+    @Test
+    fun deleteAppOptimizationModeEventByUid_uidExisting_verifyData() {
+        insertAppOptModeEventForTest(/* expirationTime= */ 1000L)
+
+        AppOptModeSharedPreferencesUtils.deleteAppOptimizationModeEventByUid(context, UID)
+
+        assertThat(AppOptModeSharedPreferencesUtils.getAllEvents(context)).isEmpty()
+    }
+
+    @Test
+    fun resetExpiredAppOptModeBeforeTimestamp_noExpiredData_verifyData() {
+        insertAppOptModeEventForTest(/* expirationTime= */ 1000L)
+
+        AppOptModeSharedPreferencesUtils.resetExpiredAppOptModeBeforeTimestamp(context, 999L)
+        val events = AppOptModeSharedPreferencesUtils.getAllEvents(context)
+
+        assertThat(events.size).isEqualTo(1)
+        assertAppOptimizationModeEventInfo(events.get(0), UID, PACKAGE_NAME, MODE_OPTIMIZED, 1000L)
+    }
+
+    @Test
+    fun resetExpiredAppOptModeBeforeTimestamp_hasExpiredData_verifyEmptyList() {
+        insertAppOptModeEventForTest(/* expirationTime= */ 1000L)
+
+        AppOptModeSharedPreferencesUtils.resetExpiredAppOptModeBeforeTimestamp(context, 1001L)
+
+        assertThat(AppOptModeSharedPreferencesUtils.getAllEvents(context)).isEmpty()
+    }
+
+    @Test
+    fun updateBatteryOptimizationMode_updateToOptimizedMode_verifyAction() {
+        whenever(testBatteryOptimizeUtils?.isOptimizeModeMutable).thenReturn(true)
+        whenever(testBatteryOptimizeUtils?.getAppOptimizationMode(true))
+            .thenReturn(MODE_UNRESTRICTED)
+
+        val currentOptMode =
+            AppOptModeSharedPreferencesUtils.updateBatteryOptimizationMode(
+                context,
+                UID,
+                PACKAGE_NAME,
+                MODE_OPTIMIZED,
+                Action.EXTERNAL_UPDATE,
+                testBatteryOptimizeUtils
+            )
+
+        verify(testBatteryOptimizeUtils)?.setAppUsageState(MODE_OPTIMIZED, Action.EXTERNAL_UPDATE)
+        assertThat(currentOptMode).isEqualTo(MODE_UNRESTRICTED)
+    }
+
+    @Test
+    fun updateBatteryOptimizationMode_optimizationModeNotChanged_verifyAction() {
+        whenever(testBatteryOptimizeUtils?.isOptimizeModeMutable).thenReturn(false)
+        whenever(testBatteryOptimizeUtils?.getAppOptimizationMode(true))
+            .thenReturn(MODE_UNRESTRICTED)
+
+        val currentOptMode =
+            AppOptModeSharedPreferencesUtils.updateBatteryOptimizationMode(
+                context,
+                UID,
+                PACKAGE_NAME,
+                MODE_OPTIMIZED,
+                Action.EXTERNAL_UPDATE,
+                testBatteryOptimizeUtils
+            )
+
+        verify(testBatteryOptimizeUtils, never())?.setAppUsageState(anyInt(), any())
+        assertThat(currentOptMode).isEqualTo(MODE_UNKNOWN)
+    }
+
+    @Test
+    fun updateBatteryOptimizationMode_updateToSameOptimizationMode_verifyAction() {
+        whenever(testBatteryOptimizeUtils?.isOptimizeModeMutable).thenReturn(true)
+        whenever(testBatteryOptimizeUtils?.getAppOptimizationMode(true)).thenReturn(MODE_RESTRICTED)
+
+        val currentOptMode =
+            AppOptModeSharedPreferencesUtils.updateBatteryOptimizationMode(
+                context,
+                UID,
+                PACKAGE_NAME,
+                MODE_RESTRICTED,
+                Action.EXTERNAL_UPDATE,
+                testBatteryOptimizeUtils
+            )
+
+        verify(testBatteryOptimizeUtils)?.setAppUsageState(MODE_RESTRICTED, Action.EXTERNAL_UPDATE)
+        assertThat(currentOptMode).isEqualTo(MODE_RESTRICTED)
+    }
+
+    private fun insertAppOptModeEventForTest(expirationTime: Long) {
+        whenever(testBatteryOptimizeUtils?.isOptimizeModeMutable).thenReturn(true)
+        whenever(testBatteryOptimizeUtils?.getAppOptimizationMode(true)).thenReturn(MODE_OPTIMIZED)
+        AppOptModeSharedPreferencesUtils.updateAppOptModeExpirationInternal(
+            context,
+            mutableListOf(UID),
+            mutableListOf(PACKAGE_NAME),
+            mutableListOf(MODE_OPTIMIZED),
+            longArrayOf(expirationTime)
+        ) { _: Int, _: String ->
+            testBatteryOptimizeUtils
+        }
+    }
+
+    companion object {
+        const val UID: Int = 12345
+        const val UNSET_UID: Int = 15432
+        const val PACKAGE_NAME: String = "com.android.app"
+
+        private fun assertAppOptimizationModeEventInfo(
+            event: AppOptimizationModeEvent,
+            uid: Int,
+            packageName: String,
+            resetOptimizationMode: Int,
+            expirationTime: Long
+        ) {
+            assertThat(event.uid).isEqualTo(uid)
+            assertThat(event.packageName).isEqualTo(packageName)
+            assertThat(event.resetOptimizationMode).isEqualTo(resetOptimizationMode)
+            assertThat(event.expirationTime).isEqualTo(expirationTime)
+        }
+    }
+}