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