1 /*
2  * Copyright (C) 2021 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.systemui.qs.external
18 
19 import android.content.ComponentName
20 import android.content.Context
21 import android.content.SharedPreferences
22 import android.service.quicksettings.Tile
23 import android.util.Log
24 import com.android.internal.annotations.VisibleForTesting
25 import javax.inject.Inject
26 import org.json.JSONException
27 import org.json.JSONObject
28 
29 data class TileServiceKey(val componentName: ComponentName, val user: Int) {
30     private val string = "${componentName.flattenToString()}:$user"
toStringnull31     override fun toString() = string
32 }
33 
34 private const val STATE = "state"
35 private const val LABEL = "label"
36 private const val SUBTITLE = "subtitle"
37 private const val CONTENT_DESCRIPTION = "content_description"
38 private const val STATE_DESCRIPTION = "state_description"
39 
40 /**
41  * Persists and retrieves state for [CustomTile].
42  *
43  * This class will persists to a fixed [SharedPreference] file a state for a pair of [ComponentName]
44  * and user id ([TileServiceKey]).
45  *
46  * It persists the state from a [Tile] necessary to present the view in the same state when
47  * retrieved, with the exception of the icon.
48  */
49 interface CustomTileStatePersister {
50 
51     /**
52      * Read the state from [SharedPreferences].
53      *
54      * Returns `null` if the tile has no saved state.
55      *
56      * Any fields that have not been saved will be set to `null`
57      */
58     fun readState(key: TileServiceKey): Tile?
59     /**
60      * Persists the state into [SharedPreferences].
61      *
62      * The implementation does not store fields that are `null` or icons.
63      */
64     fun persistState(key: TileServiceKey, tile: Tile)
65     /**
66      * Removes the state for a given tile, user pair.
67      *
68      * Used when the tile is removed by the user.
69      */
70     fun removeState(key: TileServiceKey)
71 }
72 
73 // TODO(b/299909989) Merge this class into into CustomTileRepository
74 class CustomTileStatePersisterImpl @Inject constructor(context: Context) :
75     CustomTileStatePersister {
76     companion object {
77         private const val FILE_NAME = "custom_tiles_state"
78     }
79 
80     private val sharedPreferences: SharedPreferences = context.getSharedPreferences(FILE_NAME, 0)
81 
readStatenull82     override fun readState(key: TileServiceKey): Tile? {
83         val state = sharedPreferences.getString(key.toString(), null) ?: return null
84         return try {
85             readTileFromString(state)
86         } catch (e: JSONException) {
87             Log.e("TileServicePersistence", "Bad saved state: $state", e)
88             null
89         }
90     }
91 
persistStatenull92     override fun persistState(key: TileServiceKey, tile: Tile) {
93         val state = writeToString(tile)
94 
95         sharedPreferences.edit().putString(key.toString(), state).apply()
96     }
97 
removeStatenull98     override fun removeState(key: TileServiceKey) {
99         sharedPreferences.edit().remove(key.toString()).apply()
100     }
101 }
102 
103 @VisibleForTesting
readTileFromStringnull104 internal fun readTileFromString(stateString: String): Tile {
105     val json = JSONObject(stateString)
106     return Tile().apply {
107         state = json.getInt(STATE)
108         label = json.getStringOrNull(LABEL)
109         subtitle = json.getStringOrNull(SUBTITLE)
110         contentDescription = json.getStringOrNull(CONTENT_DESCRIPTION)
111         stateDescription = json.getStringOrNull(STATE_DESCRIPTION)
112     }
113 }
114 
115 // Properties with null values will not be saved to the Json string in any way. This makes sure
116 // to properly retrieve a null in that case.
JSONObjectnull117 private fun JSONObject.getStringOrNull(name: String): String? {
118     return if (has(name)) getString(name) else null
119 }
120 
121 @VisibleForTesting
writeToStringnull122 internal fun writeToString(tile: Tile): String {
123     // Not storing the icon
124     return with(tile) {
125         JSONObject()
126             .put(STATE, state)
127             .put(LABEL, customLabel)
128             .put(SUBTITLE, subtitle)
129             .put(CONTENT_DESCRIPTION, contentDescription)
130             .put(STATE_DESCRIPTION, stateDescription)
131             .toString()
132     }
133 }
134