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