fix: 首次提交
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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.settingslib.datastore
|
||||
|
||||
import android.app.backup.BackupAgent
|
||||
import android.app.backup.BackupDataOutput
|
||||
import android.app.backup.BackupHelper
|
||||
import android.os.Build
|
||||
import android.os.ParcelFileDescriptor
|
||||
import androidx.annotation.RequiresApi
|
||||
|
||||
/**
|
||||
* Context for backup.
|
||||
*
|
||||
* @see BackupHelper.performBackup
|
||||
* @see BackupDataOutput
|
||||
*/
|
||||
class BackupContext
|
||||
internal constructor(
|
||||
/**
|
||||
* An open, read-only file descriptor pointing to the last backup state provided by the
|
||||
* application. May be null, in which case no prior state is being provided and the application
|
||||
* should perform a full backup.
|
||||
*
|
||||
* TODO: the state should support marshall/unmarshall for incremental back up.
|
||||
*/
|
||||
val oldState: ParcelFileDescriptor?,
|
||||
|
||||
/** An open, read/write BackupDataOutput pointing to the backup data destination. */
|
||||
private val data: BackupDataOutput,
|
||||
|
||||
/**
|
||||
* An open, read/write file descriptor pointing to an empty file. The application should record
|
||||
* the final backup.
|
||||
*/
|
||||
val newState: ParcelFileDescriptor,
|
||||
) {
|
||||
/**
|
||||
* The quota in bytes for the application's current backup operation.
|
||||
*
|
||||
* @see [BackupDataOutput.getQuota]
|
||||
*/
|
||||
val quota: Long
|
||||
@RequiresApi(Build.VERSION_CODES.O) get() = data.quota
|
||||
|
||||
/**
|
||||
* Additional information about the backup transport.
|
||||
*
|
||||
* See [BackupAgent] for supported flags.
|
||||
*
|
||||
* @see [BackupDataOutput.getTransportFlags]
|
||||
*/
|
||||
val transportFlags: Int
|
||||
@RequiresApi(Build.VERSION_CODES.P) get() = data.transportFlags
|
||||
}
|
||||
|
||||
/** Context for restore. */
|
||||
class RestoreContext(val key: String)
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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.settingslib.datastore
|
||||
|
||||
import android.app.backup.BackupDataOutput
|
||||
import android.app.backup.BackupHelper
|
||||
import androidx.annotation.BinderThread
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
|
||||
/** Entity for back up and restore. */
|
||||
interface BackupRestoreEntity {
|
||||
/**
|
||||
* Key of the entity.
|
||||
*
|
||||
* The key string must be unique within the data set. Note that it is invalid if the first
|
||||
* character is \uFF00 or higher.
|
||||
*
|
||||
* @see BackupDataOutput.writeEntityHeader
|
||||
*/
|
||||
val key: String
|
||||
|
||||
/**
|
||||
* Backs up the entity.
|
||||
*
|
||||
* @param backupContext context for backup
|
||||
* @param outputStream output stream to back up data
|
||||
* @return false if backup file is deleted, otherwise true
|
||||
*/
|
||||
@BinderThread
|
||||
@Throws(IOException::class)
|
||||
fun backup(backupContext: BackupContext, outputStream: OutputStream): EntityBackupResult
|
||||
|
||||
/**
|
||||
* Restores the entity.
|
||||
*
|
||||
* @param restoreContext context for restore
|
||||
* @param inputStream An open input stream from which the backup data can be read.
|
||||
* @see BackupHelper.restoreEntity
|
||||
*/
|
||||
@BinderThread
|
||||
@Throws(IOException::class)
|
||||
fun restore(restoreContext: RestoreContext, inputStream: InputStream)
|
||||
}
|
||||
|
||||
/** Result of the backup operation. */
|
||||
enum class EntityBackupResult {
|
||||
/** Update the entity. */
|
||||
UPDATE,
|
||||
/** Leave the entity intact. */
|
||||
INTACT,
|
||||
/** Delete the entity. */
|
||||
DELETE,
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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.settingslib.datastore
|
||||
|
||||
import android.app.backup.BackupAgentHelper
|
||||
import android.app.backup.BackupDataInputStream
|
||||
import android.app.backup.BackupDataOutput
|
||||
import android.app.backup.BackupHelper
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.util.Log
|
||||
import com.google.common.io.ByteStreams
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.FilterInputStream
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
|
||||
internal const val LOG_TAG = "BackupRestoreStorage"
|
||||
|
||||
/**
|
||||
* Storage with backup and restore support. Subclass must implement either [Observable] or
|
||||
* [KeyedObservable] interface.
|
||||
*
|
||||
* The storage is identified by a unique string [name] and data set is split into entities
|
||||
* ([BackupRestoreEntity]).
|
||||
*/
|
||||
abstract class BackupRestoreStorage : BackupHelper {
|
||||
/**
|
||||
* A unique string used to disambiguate the various storages within backup agent.
|
||||
*
|
||||
* It will be used as the `keyPrefix` of [BackupAgentHelper.addHelper].
|
||||
*/
|
||||
abstract val name: String
|
||||
|
||||
private val entities: List<BackupRestoreEntity> by lazy { createBackupRestoreEntities() }
|
||||
|
||||
/** Entities to back up and restore. */
|
||||
abstract fun createBackupRestoreEntities(): List<BackupRestoreEntity>
|
||||
|
||||
override fun performBackup(
|
||||
oldState: ParcelFileDescriptor?,
|
||||
data: BackupDataOutput,
|
||||
newState: ParcelFileDescriptor,
|
||||
) {
|
||||
val backupContext = BackupContext(oldState, data, newState)
|
||||
if (!enableBackup(backupContext)) {
|
||||
Log.i(LOG_TAG, "[$name] Backup disabled")
|
||||
return
|
||||
}
|
||||
Log.i(LOG_TAG, "[$name] Backup start")
|
||||
for (entity in entities) {
|
||||
val key = entity.key
|
||||
val outputStream = ByteArrayOutputStream()
|
||||
val result =
|
||||
try {
|
||||
entity.backup(backupContext, wrapBackupOutputStream(outputStream))
|
||||
} catch (exception: Exception) {
|
||||
Log.e(LOG_TAG, "[$name] Fail to backup entity $key", exception)
|
||||
continue
|
||||
}
|
||||
when (result) {
|
||||
EntityBackupResult.UPDATE -> {
|
||||
val payload = outputStream.toByteArray()
|
||||
val size = payload.size
|
||||
data.writeEntityHeader(key, size)
|
||||
data.writeEntityData(payload, size)
|
||||
Log.i(LOG_TAG, "[$name] Backup entity $key: $size bytes")
|
||||
}
|
||||
EntityBackupResult.INTACT -> {
|
||||
Log.i(LOG_TAG, "[$name] Backup entity $key intact")
|
||||
}
|
||||
EntityBackupResult.DELETE -> {
|
||||
data.writeEntityHeader(key, -1)
|
||||
Log.i(LOG_TAG, "[$name] Backup entity $key deleted")
|
||||
}
|
||||
}
|
||||
}
|
||||
Log.i(LOG_TAG, "[$name] Backup end")
|
||||
}
|
||||
|
||||
/** Returns if backup is enabled. */
|
||||
open fun enableBackup(backupContext: BackupContext): Boolean = true
|
||||
|
||||
fun wrapBackupOutputStream(outputStream: OutputStream): OutputStream {
|
||||
return outputStream
|
||||
}
|
||||
|
||||
override fun restoreEntity(data: BackupDataInputStream) {
|
||||
val key = data.key
|
||||
if (!enableRestore()) {
|
||||
Log.i(LOG_TAG, "[$name] Restore disabled, ignore entity $key")
|
||||
return
|
||||
}
|
||||
val entity = entities.firstOrNull { it.key == key }
|
||||
if (entity == null) {
|
||||
Log.w(LOG_TAG, "[$name] Cannot find handler for entity $key")
|
||||
return
|
||||
}
|
||||
Log.i(LOG_TAG, "[$name] Restore $key: ${data.size()} bytes")
|
||||
val restoreContext = RestoreContext(key)
|
||||
try {
|
||||
entity.restore(restoreContext, wrapRestoreInputStream(data))
|
||||
} catch (exception: Exception) {
|
||||
Log.e(LOG_TAG, "[$name] Fail to restore entity $key", exception)
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns if restore is enabled. */
|
||||
open fun enableRestore(): Boolean = true
|
||||
|
||||
fun wrapRestoreInputStream(inputStream: BackupDataInputStream): InputStream {
|
||||
return LimitedNoCloseInputStream(inputStream)
|
||||
}
|
||||
|
||||
override fun writeNewStateDescription(newState: ParcelFileDescriptor) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper of [BackupDataInputStream], limiting the number of bytes that can be read and make
|
||||
* [close] no-op.
|
||||
*/
|
||||
class LimitedNoCloseInputStream(inputStream: BackupDataInputStream) :
|
||||
FilterInputStream(ByteStreams.limit(inputStream, inputStream.size().toLong())) {
|
||||
override fun close() {
|
||||
// do not close original input stream
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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.settingslib.datastore
|
||||
|
||||
import android.app.Application
|
||||
import android.app.backup.BackupAgentHelper
|
||||
import android.app.backup.BackupManager
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import com.google.common.util.concurrent.MoreExecutors
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
/** Manager of [BackupRestoreStorage]. */
|
||||
class BackupRestoreStorageManager private constructor(private val application: Application) {
|
||||
private val storages = ConcurrentHashMap<String, BackupRestoreStorage>()
|
||||
|
||||
private val executor = MoreExecutors.directExecutor()
|
||||
|
||||
private val observer = Observer { reason -> notifyBackupManager(null, reason) }
|
||||
|
||||
private val keyedObserver =
|
||||
KeyedObserver<Any?> { key, reason -> notifyBackupManager(key, reason) }
|
||||
|
||||
private fun notifyBackupManager(key: Any?, reason: Int) {
|
||||
// prefer not triggering backup immediately after restore
|
||||
if (reason == ChangeReason.RESTORE) return
|
||||
// TODO: log storage name
|
||||
Log.d(LOG_TAG, "Notify BackupManager data changed for change: key=$key")
|
||||
BackupManager.dataChanged(application.packageName)
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds all the registered [BackupRestoreStorage] as the helpers of given [BackupAgentHelper].
|
||||
*
|
||||
* @see BackupAgentHelper.addHelper
|
||||
*/
|
||||
fun addBackupAgentHelpers(backupAgentHelper: BackupAgentHelper) {
|
||||
for ((keyPrefix, storage) in storages) {
|
||||
backupAgentHelper.addHelper(keyPrefix, storage)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback when restore finished.
|
||||
*
|
||||
* The observers of the storages will be notified.
|
||||
*/
|
||||
fun onRestoreFinished() {
|
||||
for (storage in storages.values) {
|
||||
storage.notifyRestoreFinished()
|
||||
}
|
||||
}
|
||||
|
||||
private fun BackupRestoreStorage.notifyRestoreFinished() {
|
||||
when (this) {
|
||||
is KeyedObservable<*> -> notifyChange(ChangeReason.RESTORE)
|
||||
is Observable -> notifyChange(ChangeReason.RESTORE)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a list of storages.
|
||||
*
|
||||
* The storage MUST implement [KeyedObservable] or [Observable].
|
||||
*/
|
||||
fun add(vararg storages: BackupRestoreStorage) {
|
||||
for (storage in storages) add(storage)
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a storage.
|
||||
*
|
||||
* The storage MUST implement [KeyedObservable] or [Observable].
|
||||
*/
|
||||
fun add(storage: BackupRestoreStorage) {
|
||||
val name = storage.name
|
||||
val oldStorage = storages.put(name, storage)
|
||||
if (oldStorage != null) {
|
||||
throw IllegalStateException(
|
||||
"Storage name '$name' conflicts:\n\told: $oldStorage\n\tnew: $storage"
|
||||
)
|
||||
}
|
||||
storage.addObserver()
|
||||
}
|
||||
|
||||
private fun BackupRestoreStorage.addObserver() {
|
||||
when (this) {
|
||||
is KeyedObservable<*> -> addObserver(keyedObserver, executor)
|
||||
is Observable -> addObserver(observer, executor)
|
||||
else ->
|
||||
throw IllegalArgumentException(
|
||||
"$this does not implement either KeyedObservable or Observable"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** Removes all the storages. */
|
||||
fun removeAll() {
|
||||
for ((name, _) in storages) remove(name)
|
||||
}
|
||||
|
||||
/** Removes storage with given name. */
|
||||
fun remove(name: String): BackupRestoreStorage? {
|
||||
val storage = storages.remove(name)
|
||||
storage?.removeObserver()
|
||||
return storage
|
||||
}
|
||||
|
||||
private fun BackupRestoreStorage.removeObserver() {
|
||||
when (this) {
|
||||
is KeyedObservable<*> -> removeObserver(keyedObserver)
|
||||
is Observable -> removeObserver(observer)
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns storage with given name. */
|
||||
fun get(name: String): BackupRestoreStorage? = storages[name]
|
||||
|
||||
/** Returns storage with given name, exception is raised if not found. */
|
||||
fun getOrThrow(name: String): BackupRestoreStorage = storages[name]!!
|
||||
|
||||
companion object {
|
||||
@Volatile private var instance: BackupRestoreStorageManager? = null
|
||||
|
||||
/** Returns the singleton of manager. */
|
||||
@JvmStatic
|
||||
fun getInstance(context: Context): BackupRestoreStorageManager {
|
||||
val result = instance
|
||||
if (result != null) return result
|
||||
synchronized(this) {
|
||||
if (instance == null) {
|
||||
instance =
|
||||
BackupRestoreStorageManager(context.applicationContext as Application)
|
||||
}
|
||||
}
|
||||
return instance!!
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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.settingslib.datastore
|
||||
|
||||
import androidx.annotation.AnyThread
|
||||
import androidx.annotation.GuardedBy
|
||||
import androidx.collection.MutableScatterMap
|
||||
import java.util.WeakHashMap
|
||||
import java.util.concurrent.Executor
|
||||
|
||||
/**
|
||||
* Callback to be informed of changes in [KeyedObservable] object.
|
||||
*
|
||||
* The observer is weakly referenced, a strong reference must be kept.
|
||||
*/
|
||||
fun interface KeyedObserver<in K> {
|
||||
/**
|
||||
* Called by [KeyedObservable] in the event of changes.
|
||||
*
|
||||
* This callback will run in the given [Executor] when observer is added.
|
||||
*
|
||||
* @param key key that has been changed
|
||||
* @param reason the reason of change
|
||||
* @see KeyedObservable.addObserver
|
||||
*/
|
||||
fun onKeyChanged(key: K, @ChangeReason reason: Int)
|
||||
}
|
||||
|
||||
/**
|
||||
* A key-value observable object allows to observe change with [KeyedObserver].
|
||||
*
|
||||
* Notes:
|
||||
* - The order in which observers will be notified is unspecified.
|
||||
* - The observer is weakly referenced to avoid memory leaking, the call site must keep a strong
|
||||
* reference of the observer.
|
||||
* - It is possible that the callback may be triggered even there is no real data change. For
|
||||
* example, when data restore/clear happens, it might be too complex to check if data is really
|
||||
* changed, thus all the registered observers are notified directly.
|
||||
*/
|
||||
@AnyThread
|
||||
interface KeyedObservable<K> {
|
||||
/**
|
||||
* Adds an observer for any key.
|
||||
*
|
||||
* The observer will be notified whenever a change happens. The [KeyedObserver.onKeyChanged]
|
||||
* callback will be invoked with specific key that is modified. However, `null` key is passed in
|
||||
* the cases that a bunch of keys are changed simultaneously (e.g. clear data, restore happens).
|
||||
*
|
||||
* @param observer observer to be notified
|
||||
* @param executor executor to run the callback
|
||||
*/
|
||||
fun addObserver(observer: KeyedObserver<K?>, executor: Executor)
|
||||
|
||||
/**
|
||||
* Adds an observer on given key.
|
||||
*
|
||||
* The observer will be notified only when the given key is changed.
|
||||
*
|
||||
* @param key key to observe
|
||||
* @param observer observer to be notified
|
||||
* @param executor executor to run the callback
|
||||
*/
|
||||
fun addObserver(key: K, observer: KeyedObserver<K>, executor: Executor)
|
||||
|
||||
/** Removes observer. */
|
||||
fun removeObserver(observer: KeyedObserver<K?>)
|
||||
|
||||
/** Removes observer on given key. */
|
||||
fun removeObserver(key: K, observer: KeyedObserver<K>)
|
||||
|
||||
/**
|
||||
* Notifies all observers that a change occurs.
|
||||
*
|
||||
* All the any key and keyed observers are notified.
|
||||
*
|
||||
* @param reason reason of the change
|
||||
*/
|
||||
fun notifyChange(@ChangeReason reason: Int)
|
||||
|
||||
/**
|
||||
* Notifies observers that a change occurs on given key.
|
||||
*
|
||||
* The any key and specific key observers are notified.
|
||||
*
|
||||
* @param key key of the change
|
||||
* @param reason reason of the change
|
||||
*/
|
||||
fun notifyChange(key: K, @ChangeReason reason: Int)
|
||||
}
|
||||
|
||||
/** A thread safe implementation of [KeyedObservable]. */
|
||||
class KeyedDataObservable<K> : KeyedObservable<K> {
|
||||
// Instead of @GuardedBy("this"), guarded by itself because KeyedDataObservable object could be
|
||||
// synchronized outside by the holder
|
||||
@GuardedBy("itself") private val observers = WeakHashMap<KeyedObserver<K?>, Executor>()
|
||||
|
||||
@GuardedBy("itself")
|
||||
private val keyedObservers = MutableScatterMap<K, WeakHashMap<KeyedObserver<K>, Executor>>()
|
||||
|
||||
override fun addObserver(observer: KeyedObserver<K?>, executor: Executor) {
|
||||
val oldExecutor = synchronized(observers) { observers.put(observer, executor) }
|
||||
if (oldExecutor != null && oldExecutor != executor) {
|
||||
throw IllegalStateException("Add $observer twice, old=$oldExecutor, new=$executor")
|
||||
}
|
||||
}
|
||||
|
||||
override fun addObserver(key: K, observer: KeyedObserver<K>, executor: Executor) {
|
||||
val oldExecutor =
|
||||
synchronized(keyedObservers) {
|
||||
keyedObservers.getOrPut(key) { WeakHashMap() }.put(observer, executor)
|
||||
}
|
||||
if (oldExecutor != null && oldExecutor != executor) {
|
||||
throw IllegalStateException("Add $observer twice, old=$oldExecutor, new=$executor")
|
||||
}
|
||||
}
|
||||
|
||||
override fun removeObserver(observer: KeyedObserver<K?>) {
|
||||
synchronized(observers) { observers.remove(observer) }
|
||||
}
|
||||
|
||||
override fun removeObserver(key: K, observer: KeyedObserver<K>) {
|
||||
synchronized(keyedObservers) {
|
||||
val observers = keyedObservers[key]
|
||||
if (observers?.remove(observer) != null && observers.isEmpty()) {
|
||||
keyedObservers.remove(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun notifyChange(@ChangeReason reason: Int) {
|
||||
// make a copy to avoid potential ConcurrentModificationException
|
||||
val observers = synchronized(observers) { observers.entries.toTypedArray() }
|
||||
val keyedObservers = synchronized(keyedObservers) { keyedObservers.copy() }
|
||||
for (entry in observers) {
|
||||
val observer = entry.key // avoid reference "entry"
|
||||
entry.value.execute { observer.onKeyChanged(null, reason) }
|
||||
}
|
||||
for (pair in keyedObservers) {
|
||||
val key = pair.first
|
||||
for (entry in pair.second) {
|
||||
val observer = entry.key // avoid reference "entry"
|
||||
entry.value.execute { observer.onKeyChanged(key, reason) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableScatterMap<K, WeakHashMap<KeyedObserver<K>, Executor>>.copy():
|
||||
List<Pair<K, Array<Map.Entry<KeyedObserver<K>, Executor>>>> {
|
||||
val result = ArrayList<Pair<K, Array<Map.Entry<KeyedObserver<K>, Executor>>>>(size)
|
||||
forEach { key, value -> result.add(Pair(key, value.entries.toTypedArray())) }
|
||||
return result
|
||||
}
|
||||
|
||||
override fun notifyChange(key: K, @ChangeReason reason: Int) {
|
||||
// make a copy to avoid potential ConcurrentModificationException
|
||||
val observers = synchronized(observers) { observers.entries.toTypedArray() }
|
||||
val keyedObservers =
|
||||
synchronized(keyedObservers) { keyedObservers[key]?.entries?.toTypedArray() }
|
||||
?: arrayOf()
|
||||
for (entry in observers) {
|
||||
val observer = entry.key // avoid reference "entry"
|
||||
entry.value.execute { observer.onKeyChanged(key, reason) }
|
||||
}
|
||||
for (entry in keyedObservers) {
|
||||
val observer = entry.key // avoid reference "entry"
|
||||
entry.value.execute { observer.onKeyChanged(key, reason) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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.settingslib.datastore
|
||||
|
||||
/**
|
||||
* A [BackupRestoreStorage] that implements [Observable].
|
||||
*
|
||||
* This class provides the [Observable] implementations on top of [DataObservable] by delegation.
|
||||
*/
|
||||
abstract class ObservableBackupRestoreStorage :
|
||||
BackupRestoreStorage(), Observable by DataObservable()
|
||||
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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.settingslib.datastore
|
||||
|
||||
import androidx.annotation.AnyThread
|
||||
import androidx.annotation.GuardedBy
|
||||
import androidx.annotation.IntDef
|
||||
import java.util.WeakHashMap
|
||||
import java.util.concurrent.Executor
|
||||
|
||||
/** The reason of a change. */
|
||||
@IntDef(
|
||||
ChangeReason.UNKNOWN,
|
||||
ChangeReason.UPDATE,
|
||||
ChangeReason.DELETE,
|
||||
ChangeReason.RESTORE,
|
||||
ChangeReason.SYNC_ACROSS_PROFILES,
|
||||
)
|
||||
@Retention(AnnotationRetention.SOURCE)
|
||||
annotation class ChangeReason {
|
||||
companion object {
|
||||
/** Unknown reason of the change. */
|
||||
const val UNKNOWN = 0
|
||||
/** Data is updated. */
|
||||
const val UPDATE = 1
|
||||
/** Data is deleted. */
|
||||
const val DELETE = 2
|
||||
/** Data is restored from backup/restore framework. */
|
||||
const val RESTORE = 3
|
||||
/** Data is synced from another profile (e.g. personal profile to work profile). */
|
||||
const val SYNC_ACROSS_PROFILES = 4
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback to be informed of changes in [Observable] object.
|
||||
*
|
||||
* The observer is weakly referenced, a strong reference must be kept.
|
||||
*/
|
||||
fun interface Observer {
|
||||
/**
|
||||
* Called by [Observable] in the event of changes.
|
||||
*
|
||||
* This callback will run in the given [Executor] when observer is added.
|
||||
*
|
||||
* @param reason the reason of change
|
||||
* @see [Observable.addObserver] for the notices.
|
||||
*/
|
||||
fun onChanged(@ChangeReason reason: Int)
|
||||
}
|
||||
|
||||
/** An observable object allows to observe change with [Observer]. */
|
||||
@AnyThread
|
||||
interface Observable {
|
||||
/**
|
||||
* Adds an observer.
|
||||
*
|
||||
* Notes:
|
||||
* - The order in which observers will be notified is unspecified.
|
||||
* - The observer is weakly referenced to avoid memory leaking, the call site must keep a strong
|
||||
* reference of the observer.
|
||||
* - It is possible that the callback may be triggered even there is no real data change. For
|
||||
* example, when data restore/clear happens, it might be too complex to check if data is
|
||||
* really changed, thus all the registered observers are notified directly.
|
||||
*
|
||||
* @param observer observer to be notified
|
||||
* @param executor executor to run the [Observer.onChanged] callback
|
||||
*/
|
||||
fun addObserver(observer: Observer, executor: Executor)
|
||||
|
||||
/** Removes given observer. */
|
||||
fun removeObserver(observer: Observer)
|
||||
|
||||
/**
|
||||
* Notifies observers that a change occurs.
|
||||
*
|
||||
* @param reason reason of the change
|
||||
*/
|
||||
fun notifyChange(@ChangeReason reason: Int)
|
||||
}
|
||||
|
||||
/** A thread safe implementation of [Observable]. */
|
||||
class DataObservable : Observable {
|
||||
// Instead of @GuardedBy("this"), guarded by itself because DataObservable object could be
|
||||
// synchronized outside by the holder
|
||||
@GuardedBy("itself") private val observers = WeakHashMap<Observer, Executor>()
|
||||
|
||||
override fun addObserver(observer: Observer, executor: Executor) {
|
||||
val oldExecutor = synchronized(observers) { observers.put(observer, executor) }
|
||||
if (oldExecutor != null && oldExecutor != executor) {
|
||||
throw IllegalStateException("Add $observer twice, old=$oldExecutor, new=$executor")
|
||||
}
|
||||
}
|
||||
|
||||
override fun removeObserver(observer: Observer) {
|
||||
synchronized(observers) { observers.remove(observer) }
|
||||
}
|
||||
|
||||
override fun notifyChange(@ChangeReason reason: Int) {
|
||||
// make a copy to avoid potential ConcurrentModificationException
|
||||
val entries = synchronized(observers) { observers.entries.toTypedArray() }
|
||||
for (entry in entries) {
|
||||
val observer = entry.key // avoid reference "entry"
|
||||
entry.value.execute { observer.onChanged(reason) }
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user