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" 30 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 93 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. 106 private fun JSONObject.getStringOrNull(name: String): String? { 107 return if (has(name)) getString(name) else null 108 } 109 110 @VisibleForTesting 111 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