/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.util.settings
import android.annotation.UserIdInt
import android.content.ContentResolver
import android.database.ContentObserver
import android.net.Uri
import android.provider.Settings
import android.provider.Settings.SettingNotFoundException
import androidx.annotation.AnyThread
import androidx.annotation.WorkerThread
import com.android.app.tracing.TraceUtils.trace
import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.app.tracing.coroutines.withContextTraced as withContext
import kotlin.coroutines.coroutineContext
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
/**
* Used to interact with mainly with Settings.Global, but can also be used for Settings.System and
* Settings.Secure. To use the per-user System and Secure settings, [UserSettingsProxy] must be used
* instead.
*
* This interface can be implemented to give instance method (instead of static method) versions of
* Settings.Global. It can be injected into class constructors and then faked or mocked as needed in
* tests.
*
* You can ask for [GlobalSettings] to be injected as needed.
*
* This class also provides [.registerContentObserver] methods, normally found on [ContentResolver]
* instances, unifying setting related actions in one place.
*/
public interface SettingsProxy {
/** Returns the [ContentResolver] this instance was constructed with. */
public fun getContentResolver(): ContentResolver
/** Returns the [CoroutineScope] that the async APIs will use. */
public val settingsScope: CoroutineScope
@OptIn(ExperimentalStdlibApi::class)
public suspend fun executeOnSettingsScopeDispatcher(name: String, block: () -> Unit) {
val settingsDispatcher = settingsScope.coroutineContext[CoroutineDispatcher]
if (
settingsDispatcher != null &&
settingsDispatcher != coroutineContext[CoroutineDispatcher]
) {
withContext(name, settingsDispatcher) { block() }
} else {
trace(name) { block() }
}
}
/**
* Construct the content URI for a particular name/value pair, useful for monitoring changes
* with a ContentObserver.
*
* @param name to look up in the table
* @return the corresponding content URI, or null if not present
*/
@AnyThread public fun getUriFor(name: String): Uri
/**
* Registers listener for a given content observer while blocking the current thread.
* Implicitly calls [getUriFor] on the passed in name.
*
* This should not be called from the main thread, use [registerContentObserver] or
* [registerContentObserverAsync] instead.
*/
@WorkerThread
public fun registerContentObserverSync(name: String, settingsObserver: ContentObserver) {
registerContentObserverSync(getUriFor(name), settingsObserver)
}
/**
* Convenience wrapper around [ContentResolver.registerContentObserver].'
*
* suspend API corresponding to [registerContentObserver] to ensure that [ContentObserver]
* registration happens on a worker thread. Caller may wrap the API in an async block if they
* wish to synchronize execution.
*/
public suspend fun registerContentObserver(name: String, settingsObserver: ContentObserver) {
executeOnSettingsScopeDispatcher("registerContentObserver-A") {
registerContentObserverSync(getUriFor(name), settingsObserver)
}
}
/**
* Convenience wrapper around [ContentResolver.registerContentObserver].'
*
* API corresponding to [registerContentObserver] for Java usage.
*/
@AnyThread
public fun registerContentObserverAsync(name: String, settingsObserver: ContentObserver): Job =
settingsScope.launch("registerContentObserverAsync-A") {
registerContentObserverSync(getUriFor(name), settingsObserver)
}
/**
* Convenience wrapper around [ContentResolver.registerContentObserver].'
*
* API corresponding to [registerContentObserver] for Java usage. After registration is
* complete, the callback block is called on the background thread to allow for update of
* value.
*/
@AnyThread
public fun registerContentObserverAsync(
name: String,
settingsObserver: ContentObserver,
@WorkerThread registered: Runnable,
): Job =
settingsScope.launch("registerContentObserverAsync-B") {
registerContentObserverSync(getUriFor(name), settingsObserver)
registered.run()
}
/**
* Registers listener for a given content observer while blocking the current thread.
*
* This should not be called from the main thread, use [registerContentObserver] or
* [registerContentObserverAsync] instead.
*/
@WorkerThread
public fun registerContentObserverSync(uri: Uri, settingsObserver: ContentObserver) {
registerContentObserverSync(uri, false, settingsObserver)
}
/**
* Convenience wrapper around [ContentResolver.registerContentObserver].'
*
* suspend API corresponding to [registerContentObserver] to ensure that [ContentObserver]
* registration happens on a worker thread. Caller may wrap the API in an async block if they
* wish to synchronize execution.
*/
public suspend fun registerContentObserver(uri: Uri, settingsObserver: ContentObserver) {
executeOnSettingsScopeDispatcher("registerContentObserver-B") {
registerContentObserverSync(uri, settingsObserver)
}
}
/**
* Convenience wrapper around [ContentResolver.registerContentObserver].'
*
* API corresponding to [registerContentObserver] for Java usage.
*/
@AnyThread
public fun registerContentObserverAsync(uri: Uri, settingsObserver: ContentObserver): Job =
settingsScope.launch("registerContentObserverAsync-C") {
registerContentObserverSync(uri, settingsObserver)
}
/**
* Convenience wrapper around [ContentResolver.registerContentObserver].'
*
* API corresponding to [registerContentObserver] for Java usage. After registration is
* complete, the callback block is called on the background thread to allow for update of
* value.
*/
@AnyThread
public fun registerContentObserverAsync(
uri: Uri,
settingsObserver: ContentObserver,
@WorkerThread registered: Runnable,
): Job =
settingsScope.launch("registerContentObserverAsync-D") {
registerContentObserverSync(uri, settingsObserver)
registered.run()
}
/**
* Convenience wrapper around [ContentResolver.registerContentObserver].'
*
* Implicitly calls [getUriFor] on the passed in name.
*/
@WorkerThread
public fun registerContentObserverSync(
name: String,
notifyForDescendants: Boolean,
settingsObserver: ContentObserver,
) {
registerContentObserverSync(getUriFor(name), notifyForDescendants, settingsObserver)
}
/**
* Convenience wrapper around [ContentResolver.registerContentObserver].'
*
* suspend API corresponding to [registerContentObserver] to ensure that [ContentObserver]
* registration happens on a worker thread. Caller may wrap the API in an async block if they
* wish to synchronize execution.
*/
public suspend fun registerContentObserver(
name: String,
notifyForDescendants: Boolean,
settingsObserver: ContentObserver,
) {
executeOnSettingsScopeDispatcher("registerContentObserver-C") {
registerContentObserverSync(getUriFor(name), notifyForDescendants, settingsObserver)
}
}
/**
* Convenience wrapper around [ContentResolver.registerContentObserver].'
*
* API corresponding to [registerContentObserver] for Java usage.
*/
@AnyThread
public fun registerContentObserverAsync(
name: String,
notifyForDescendants: Boolean,
settingsObserver: ContentObserver,
): Job =
settingsScope.launch("registerContentObserverAsync-E") {
registerContentObserverSync(getUriFor(name), notifyForDescendants, settingsObserver)
}
/**
* Convenience wrapper around [ContentResolver.registerContentObserver].'
*
* API corresponding to [registerContentObserver] for Java usage. After registration is
* complete, the callback block is called on the background thread to allow for update of
* value.
*/
@AnyThread
public fun registerContentObserverAsync(
name: String,
notifyForDescendants: Boolean,
settingsObserver: ContentObserver,
@WorkerThread registered: Runnable,
): Job =
settingsScope.launch("registerContentObserverAsync-F") {
registerContentObserverSync(getUriFor(name), notifyForDescendants, settingsObserver)
registered.run()
}
/**
* Registers listener for a given content observer while blocking the current thread.
*
* This should not be called from the main thread, use [registerContentObserver] or
* [registerContentObserverAsync] instead.
*/
@WorkerThread
public fun registerContentObserverSync(
uri: Uri,
notifyForDescendants: Boolean,
settingsObserver: ContentObserver,
) {
trace({ "SP#registerObserver#[$uri]" }) {
getContentResolver()
.registerContentObserver(uri, notifyForDescendants, settingsObserver)
}
}
/**
* Convenience wrapper around [ContentResolver.registerContentObserver].'
*
* suspend API corresponding to [registerContentObserver] to ensure that [ContentObserver]
* registration happens on a worker thread. Caller may wrap the API in an async block if they
* wish to synchronize execution.
*/
public suspend fun registerContentObserver(
uri: Uri,
notifyForDescendants: Boolean,
settingsObserver: ContentObserver,
) {
executeOnSettingsScopeDispatcher("registerContentObserver-D") {
registerContentObserverSync(uri, notifyForDescendants, settingsObserver)
}
}
/**
* Convenience wrapper around [ContentResolver.registerContentObserver].'
*
* API corresponding to [registerContentObserver] for Java usage.
*/
@AnyThread
public fun registerContentObserverAsync(
uri: Uri,
notifyForDescendants: Boolean,
settingsObserver: ContentObserver,
): Job =
settingsScope.launch("registerContentObserverAsync-G") {
registerContentObserverSync(uri, notifyForDescendants, settingsObserver)
}
/**
* Convenience wrapper around [ContentResolver.registerContentObserver].'
*
* API corresponding to [registerContentObserver] for Java usage. After registration is
* complete, the callback block is called on the background thread to allow for update of
* value.
*/
@AnyThread
public fun registerContentObserverAsync(
uri: Uri,
notifyForDescendants: Boolean,
settingsObserver: ContentObserver,
@WorkerThread registered: Runnable,
): Job =
settingsScope.launch("registerContentObserverAsync-H") {
registerContentObserverSync(uri, notifyForDescendants, settingsObserver)
registered.run()
}
/**
* Unregisters the given content observer while blocking the current thread.
*
* This should not be called from the main thread, use [unregisterContentObserver] or
* [unregisterContentObserverAsync] instead.
*/
@WorkerThread
public fun unregisterContentObserverSync(settingsObserver: ContentObserver) {
trace({ "SP#unregisterObserver" }) {
getContentResolver().unregisterContentObserver(settingsObserver)
}
}
/**
* Convenience wrapper around [ContentResolver.unregisterContentObserver].'
*
* API corresponding to [unregisterContentObserver] for Java usage to ensure that
* [ContentObserver] un-registration happens on a worker thread. Caller may wrap the API in an
* async block if they wish to synchronize execution.
*/
public suspend fun unregisterContentObserver(settingsObserver: ContentObserver) {
executeOnSettingsScopeDispatcher("unregisterContentObserver") {
unregisterContentObserverSync(settingsObserver)
}
}
/**
* Convenience wrapper around [ContentResolver.unregisterContentObserver].'
*
* API corresponding to [unregisterContentObserver] for Java usage to ensure that
* [ContentObserver] registration happens on a worker thread.
*/
@AnyThread
public fun unregisterContentObserverAsync(settingsObserver: ContentObserver): Job =
settingsScope.launch("unregisterContentObserverAsync") {
unregisterContentObserver(settingsObserver)
}
/**
* Look up a name in the database.
*
* @param name to look up in the table
* @return the corresponding value, or null if not present
*/
public fun getString(name: String): String?
/**
* Store a name/value pair into the database.
*
* @param name to store
* @param value to associate with the name
* @return true if the value was set, false on database errors
*/
public fun putString(name: String, value: String?): Boolean
/**
* Store a name/value pair into the database.
*
* The method takes an optional tag to associate with the setting which can be used to clear
* only settings made by your package and associated with this tag by passing the tag to
* [ ][.resetToDefaults]. Anyone can override the current tag. Also if another package changes
* the setting then the tag will be set to the one specified in the set call which can be null.
* Also any of the settings setters that do not take a tag as an argument effectively clears the
* tag.
*
* For example, if you set settings A and B with tags T1 and T2 and another app changes setting
* A (potentially to the same value), it can assign to it a tag T3 (note that now the package
* that changed the setting is not yours). Now if you reset your changes for T1 and T2 only
* setting B will be reset and A not (as it was changed by another package) but since A did not
* change you are in the desired initial state. Now if the other app changes the value of A
* (assuming you registered an observer in the beginning) you would detect that the setting was
* changed by another app and handle this appropriately (ignore, set back to some value, etc).
*
* Also the method takes an argument whether to make the value the default for this setting. If
* the system already specified a default value, then the one passed in here will **not** be set
* as the default.
*
* @param name to store.
* @param value to associate with the name.
* @param tag to associate with the setting.
* @param makeDefault whether to make the value the default one.
* @return true if the value was set, false on database errors.
* @see .resetToDefaults
*/
public fun putString(name: String, value: String?, tag: String?, makeDefault: Boolean): Boolean
/**
* Convenience function for retrieving a single secure settings value as an integer. Note that
* internally setting values are always stored as strings; this function converts the string to
* an integer for you. The default value will be returned if the setting is not defined or not
* an integer.
*
* @param name The name of the setting to retrieve.
* @param default Value to return if the setting is not defined.
* @return The setting's current value, or default if it is not defined or not a valid integer.
*/
public fun getInt(name: String, default: Int): Int {
val v = getString(name)
return try {
v?.toInt() ?: default
} catch (e: NumberFormatException) {
default
}
}
/**
* Convenience function for retrieving a single secure settings value as an integer. Note that
* internally setting values are always stored as strings; this function converts the string to
* an integer for you.
*
* This version does not take a default value. If the setting has not been set, or the string
* value is not a number, it throws [Settings.SettingNotFoundException].
*
* @param name The name of the setting to retrieve.
* @return The setting's current value.
* @throws Settings.SettingNotFoundException Thrown if a setting by the given name can't be
* found or the setting value is not an integer.
*/
@Throws(SettingNotFoundException::class)
public fun getInt(name: String): Int {
val v = getString(name) ?: throw SettingNotFoundException(name)
return try {
v.toInt()
} catch (e: NumberFormatException) {
throw SettingNotFoundException(name)
}
}
/**
* Convenience function for updating a single settings value as an integer. This will either
* create a new entry in the table if the given name does not exist, or modify the value of the
* existing row with that name. Note that internally setting values are always stored as
* strings, so this function converts the given value to a string before storing it.
*
* @param name The name of the setting to modify.
* @param value The new value for the setting.
* @return true if the value was set, false on database errors
*/
public fun putInt(name: String, value: Int): Boolean {
return putString(name, value.toString())
}
/**
* Convenience function for retrieving a single secure settings value as a boolean. Note that
* internally setting values are always stored as strings; this function converts the string to
* a boolean for you. The default value will be returned if the setting is not defined or not a
* boolean.
*
* @param name The name of the setting to retrieve.
* @param default Value to return if the setting is not defined.
* @return The setting's current value, or default if it is not defined or not a valid boolean.
*/
public fun getBool(name: String, default: Boolean): Boolean {
return getInt(name, if (default) 1 else 0) != 0
}
/**
* Convenience function for retrieving a single secure settings value as a boolean. Note that
* internally setting values are always stored as strings; this function converts the string to
* a boolean for you.
*
* This version does not take a default value. If the setting has not been set, or the string
* value is not a number, it throws [Settings.SettingNotFoundException].
*
* @param name The name of the setting to retrieve.
* @return The setting's current value.
* @throws Settings.SettingNotFoundException Thrown if a setting by the given name can't be
* found or the setting value is not a boolean.
*/
@Throws(SettingNotFoundException::class)
public fun getBool(name: String): Boolean {
return getInt(name) != 0
}
/**
* Convenience function for updating a single settings value as a boolean. This will either
* create a new entry in the table if the given name does not exist, or modify the value of the
* existing row with that name. Note that internally setting values are always stored as
* strings, so this function converts the given value to a string before storing it.
*
* @param name The name of the setting to modify.
* @param value The new value for the setting.
* @return true if the value was set, false on database errors
*/
public fun putBool(name: String, value: Boolean): Boolean {
return putInt(name, if (value) 1 else 0)
}
/**
* Convenience function for retrieving a single secure settings value as a `long`. Note that
* internally setting values are always stored as strings; this function converts the string to
* a `long` for you. The default value will be returned if the setting is not defined or not a
* `long`.
*
* @param name The name of the setting to retrieve.
* @param def Value to return if the setting is not defined.
* @return The setting's current value, or 'def' if it is not defined or not a valid `long`.
*/
public fun getLong(name: String, def: Long): Long {
val valString = getString(name)
return parseLongOrUseDefault(valString, def)
}
/**
* Convenience function for retrieving a single secure settings value as a `long`. Note that
* internally setting values are always stored as strings; this function converts the string to
* a `long` for you.
*
* This version does not take a default value. If the setting has not been set, or the string
* value is not a number, it throws [Settings.SettingNotFoundException].
*
* @param name The name of the setting to retrieve.
* @return The setting's current value.
* @throws Settings.SettingNotFoundException Thrown if a setting by the given name can't be
* found or the setting value is not an integer.
*/
@Throws(SettingNotFoundException::class)
public fun getLong(name: String): Long {
val valString = getString(name)
return parseLongOrThrow(name, valString)
}
/**
* Convenience function for updating a secure settings value as a long integer. This will either
* create a new entry in the table if the given name does not exist, or modify the value of the
* existing row with that name. Note that internally setting values are always stored as
* strings, so this function converts the given value to a string before storing it.
*
* @param name The name of the setting to modify.
* @param value The new value for the setting.
* @return true if the value was set, false on database errors
*/
public fun putLong(name: String, value: Long): Boolean {
return putString(name, value.toString())
}
/**
* Convenience function for retrieving a single secure settings value as a floating point
* number. Note that internally setting values are always stored as strings; this function
* converts the string to an float for you. The default value will be returned if the setting is
* not defined or not a valid float.
*
* @param name The name of the setting to retrieve.
* @param def Value to return if the setting is not defined.
* @return The setting's current value, or 'def' if it is not defined or not a valid float.
*/
public fun getFloat(name: String, def: Float): Float {
val v = getString(name)
return parseFloat(v, def)
}
/**
* Convenience function for retrieving a single secure settings value as a float. Note that
* internally setting values are always stored as strings; this function converts the string to
* a float for you.
*
* This version does not take a default value. If the setting has not been set, or the string
* value is not a number, it throws [Settings.SettingNotFoundException].
*
* @param name The name of the setting to retrieve.
* @return The setting's current value.
* @throws Settings.SettingNotFoundException Thrown if a setting by the given name can't be
* found or the setting value is not a float.
*/
@Throws(SettingNotFoundException::class)
public fun getFloat(name: String): Float {
val v = getString(name)
return parseFloatOrThrow(name, v)
}
/**
* Convenience function for updating a single settings value as a floating point number. This
* will either create a new entry in the table if the given name does not exist, or modify the
* value of the existing row with that name. Note that internally setting values are always
* stored as strings, so this function converts the given value to a string before storing it.
*
* @param name The name of the setting to modify.
* @param value The new value for the setting.
* @return true if the value was set, false on database errors
*/
public fun putFloat(name: String, value: Float): Boolean {
return putString(name, value.toString())
}
public companion object {
/** Convert a string to a long, or uses a default if the string is malformed or null */
@JvmStatic
public fun parseLongOrUseDefault(valString: String?, default: Long): Long {
val value: Long =
try {
valString?.toLong() ?: default
} catch (e: NumberFormatException) {
default
}
return value
}
/** Convert a string to a long, or throws an exception if the string is malformed or null */
@JvmStatic
@Throws(SettingNotFoundException::class)
public fun parseLongOrThrow(name: String, valString: String?): Long {
if (valString == null) {
throw SettingNotFoundException(name)
}
return try {
valString.toLong()
} catch (e: NumberFormatException) {
throw SettingNotFoundException(name)
}
}
/** Convert a string to a float, or uses a default if the string is malformed or null */
@JvmStatic
public fun parseFloat(v: String?, def: Float): Float {
return try {
v?.toFloat() ?: def
} catch (e: NumberFormatException) {
def
}
}
/**
* Convert a string to a float, or throws an exception if the string is malformed or null
*/
@JvmStatic
@Throws(SettingNotFoundException::class)
public fun parseFloatOrThrow(name: String, v: String?): Float {
if (v == null) {
throw SettingNotFoundException(name)
}
return try {
v.toFloat()
} catch (e: NumberFormatException) {
throw SettingNotFoundException(name)
}
}
}
public fun interface CurrentUserIdProvider {
@UserIdInt public fun getUserId(): Int
}
}