1 /*
<lambda>null2  * Copyright (C) 2024 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 com.android.settings.fuelgauge.batteryusage
18 
19 import android.content.Context
20 import android.content.SharedPreferences
21 import android.util.ArrayMap
22 import android.util.Base64
23 import android.util.Log
24 import androidx.annotation.VisibleForTesting
25 import com.android.settings.fuelgauge.BatteryOptimizeHistoricalLogEntry.Action
26 import com.android.settings.fuelgauge.BatteryOptimizeUtils
27 import com.android.settings.fuelgauge.BatteryUtils
28 
29 /** A util to store and update app optimization mode expiration event data. */
30 object AppOptModeSharedPreferencesUtils {
31     private const val TAG: String = "AppOptModeSharedPreferencesUtils"
32     private const val SHARED_PREFS_FILE: String = "app_optimization_mode_shared_prefs"
33 
34     @VisibleForTesting const val UNLIMITED_EXPIRE_TIME: Long = -1L
35 
36     private val appOptimizationModeLock = Any()
37     private val defaultInstance = AppOptimizationModeEvent.getDefaultInstance()
38 
39     /** Returns all app optimization mode events for log. */
40     @JvmStatic
41     fun getAllEvents(context: Context): List<AppOptimizationModeEvent> =
42         synchronized(appOptimizationModeLock) { getAppOptModeEventsMap(context).values.toList() }
43 
44     /** Removes all app optimization mode events. */
45     @JvmStatic
46     fun clearAll(context: Context) =
47         synchronized(appOptimizationModeLock) {
48             getSharedPreferences(context).edit().clear().apply()
49         }
50 
51     /** Updates the app optimization mode event data. */
52     @JvmStatic
53     fun updateAppOptModeExpiration(
54         context: Context,
55         uids: List<Int>,
56         packageNames: List<String>,
57         optimizationModes: List<Int>,
58         expirationTimes: LongArray,
59     ) =
60         // The internal fun with an additional lambda parameter is used to
61         // 1) get true BatteryOptimizeUtils in production environment
62         // 2) get fake BatteryOptimizeUtils for testing environment
63         updateAppOptModeExpirationInternal(
64             context,
65             uids,
66             packageNames,
67             optimizationModes,
68             expirationTimes,
69         ) { uid: Int, packageName: String ->
70             BatteryOptimizeUtils(context, uid, packageName)
71         }
72 
73     /** Resets the app optimization mode event data since the query timestamp. */
74     @JvmStatic
75     fun resetExpiredAppOptModeBeforeTimestamp(context: Context, queryTimestampMs: Long) =
76         synchronized(appOptimizationModeLock) {
77             val eventsMap = getAppOptModeEventsMap(context)
78             val expirationUids = ArrayList<Int>(eventsMap.size)
79             for ((uid, event) in eventsMap) {
80                 if (event.expirationTime > queryTimestampMs) {
81                     continue
82                 }
83                 updateBatteryOptimizationMode(
84                     context,
85                     event.uid,
86                     event.packageName,
87                     event.resetOptimizationMode,
88                     Action.EXPIRATION_RESET,
89                 )
90                 expirationUids.add(uid)
91             }
92             // Remove the expired AppOptimizationModeEvent data from storage
93             clearSharedPreferences(context, expirationUids)
94         }
95 
96     /** Deletes all app optimization mode event data with a specific uid. */
97     @JvmStatic
98     fun deleteAppOptimizationModeEventByUid(context: Context, uid: Int) =
99         synchronized(appOptimizationModeLock) { clearSharedPreferences(context, listOf(uid)) }
100 
101     @VisibleForTesting
102     fun updateAppOptModeExpirationInternal(
103         context: Context,
104         uids: List<Int>,
105         packageNames: List<String>,
106         optimizationModes: List<Int>,
107         expirationTimes: LongArray,
108         getBatteryOptimizeUtils: (Int, String) -> BatteryOptimizeUtils,
109     ) =
110         synchronized(appOptimizationModeLock) {
111             val eventsMap = getAppOptModeEventsMap(context)
112             val expirationEvents: MutableMap<Int, AppOptimizationModeEvent> = ArrayMap()
113             for (i in uids.indices) {
114                 val uid = uids[i]
115                 val packageName = packageNames[i]
116                 val optimizationMode = optimizationModes[i]
117                 val originalOptMode: Int =
118                     updateBatteryOptimizationMode(
119                         context,
120                         uid,
121                         packageName,
122                         optimizationMode,
123                         Action.EXTERNAL_UPDATE,
124                         getBatteryOptimizeUtils(uid, packageName),
125                     )
126                 if (originalOptMode == BatteryOptimizeUtils.MODE_UNKNOWN) {
127                     continue
128                 }
129                 // Make sure the reset mode is consistent with the expiration event in storage.
130                 val resetOptMode = eventsMap[uid]?.resetOptimizationMode ?: originalOptMode
131                 val expireTimeMs: Long = expirationTimes[i]
132                 if (expireTimeMs != UNLIMITED_EXPIRE_TIME) {
133                     Log.d(
134                         TAG,
135                         "setOptimizationMode($packageName) from $originalOptMode " +
136                             "to $optimizationMode with expiration time $expireTimeMs",
137                     )
138                     expirationEvents[uid] =
139                         AppOptimizationModeEvent.newBuilder()
140                             .setUid(uid)
141                             .setPackageName(packageName)
142                             .setResetOptimizationMode(resetOptMode)
143                             .setExpirationTime(expireTimeMs)
144                             .build()
145                 }
146             }
147 
148             // Append and update the AppOptimizationModeEvent.
149             if (expirationEvents.isNotEmpty()) {
150                 updateSharedPreferences(context, expirationEvents)
151             }
152         }
153 
154     @VisibleForTesting
155     fun updateBatteryOptimizationMode(
156         context: Context,
157         uid: Int,
158         packageName: String,
159         optimizationMode: Int,
160         action: Action,
161         batteryOptimizeUtils: BatteryOptimizeUtils =
162             BatteryOptimizeUtils(context, uid, packageName),
163     ): Int {
164         if (!batteryOptimizeUtils.isOptimizeModeMutable) {
165             Log.w(TAG, "Fail to update immutable optimization mode for: $packageName")
166             return BatteryOptimizeUtils.MODE_UNKNOWN
167         }
168         val currentOptMode = batteryOptimizeUtils.appOptimizationMode
169         batteryOptimizeUtils.setAppUsageState(optimizationMode, action)
170         Log.d(
171             TAG,
172             "setAppUsageState($packageName) to $optimizationMode with action = ${action.name}",
173         )
174         return currentOptMode
175     }
176 
177     private fun getSharedPreferences(context: Context): SharedPreferences {
178         return context.applicationContext.getSharedPreferences(
179             SHARED_PREFS_FILE,
180             Context.MODE_PRIVATE,
181         )
182     }
183 
184     private fun getAppOptModeEventsMap(context: Context): ArrayMap<Int, AppOptimizationModeEvent> {
185         val sharedPreferences = getSharedPreferences(context)
186         val allKeys = sharedPreferences.all?.keys ?: emptySet()
187         if (allKeys.isEmpty()) {
188             return ArrayMap()
189         }
190         val eventsMap = ArrayMap<Int, AppOptimizationModeEvent>(allKeys.size)
191         for (key in allKeys) {
192             sharedPreferences.getString(key, null)?.let {
193                 eventsMap[key.toInt()] = deserializeAppOptimizationModeEvent(it)
194             }
195         }
196         return eventsMap
197     }
198 
199     private fun updateSharedPreferences(
200         context: Context,
201         eventsMap: Map<Int, AppOptimizationModeEvent>,
202     ) {
203         val sharedPreferences = getSharedPreferences(context)
204         sharedPreferences.edit().run {
205             for ((uid, event) in eventsMap) {
206                 putString(uid.toString(), serializeAppOptimizationModeEvent(event))
207             }
208             apply()
209         }
210     }
211 
212     private fun clearSharedPreferences(context: Context, uids: List<Int>) {
213         val sharedPreferences = getSharedPreferences(context)
214         sharedPreferences.edit().run {
215             for (uid in uids) {
216                 remove(uid.toString())
217             }
218             apply()
219         }
220     }
221 
222     private fun serializeAppOptimizationModeEvent(event: AppOptimizationModeEvent): String {
223         return Base64.encodeToString(event.toByteArray(), Base64.DEFAULT)
224     }
225 
226     private fun deserializeAppOptimizationModeEvent(
227         encodedProtoString: String,
228     ): AppOptimizationModeEvent {
229         return BatteryUtils.parseProtoFromString(encodedProtoString, defaultInstance)
230     }
231 }
232