Skip to content

Instantly share code, notes, and snippets.

@Dexterp37
Created November 9, 2018 11:49
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Dexterp37/d2e0a85db0ddae508979e37b1fd7b996 to your computer and use it in GitHub Desktop.
Save Dexterp37/d2e0a85db0ddae508979e37b1fd7b996 to your computer and use it in GitHub Desktop.
Generic Scalar Storage
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package mozilla.components.service.glean.storages
import android.annotation.SuppressLint
import android.content.Context
import android.content.SharedPreferences
import java.util.UUID
import android.support.annotation.VisibleForTesting
import mozilla.components.service.glean.Lifetime
import mozilla.components.support.base.log.logger.Logger
import org.json.JSONObject
/**
* This singleton handles the in-memory storage logic for uuids. It is meant to be used by
* the Specific UUID API and the ping assembling objects. No validation on the stored data
* is performed at this point: validation must be performed by the Specific Uuids API.
*
* This class contains a reference to the Android application Context. While the IDE warns
* us that this could leak, the application context lives as long as the application and this
* object. For this reason, we should be safe to suppress the IDE warning.
*/
@SuppressLint("StaticFieldLeak")
internal object UuidsStorageEngine : UuidsStorageEngineImplementation()
// Generic scalar implementation below
internal typealias GenericDataStorage<T> = MutableMap<String, T>
internal typealias GenericStorageMap<T> = MutableMap<String, GenericDataStorage<T>>
abstract class ScalarStorageImpl<ScalarType> : StorageEngine {
override lateinit var applicationContext: Context
protected val userLifetimeStorage: SharedPreferences by lazy { deserializeUserLifetime() }
// Let derived class define a logger so that they can provide a proper name,
// useful when debugging weird behaviours.
abstract val logger: Logger
// Use a multi-level map to store the data for the different lifetimes and
// stores: Map[Lifetime, Map[StorageName, ScalarType]].
protected val dataStores: Map<String, GenericStorageMap<ScalarType>> =
Lifetime.values().associateBy(
{ it.toString() },
{ mutableMapOf<String, GenericDataStorage<ScalarType>>() }
)
/**
* TODO
*/
abstract fun singleMetricInstantiator(value: Any?): ScalarType
/**
* Deserialize the metrics with a lifetime = User that are on disk.
* This will be called the first time a metric is used or before a snapshot is
* taken.
*
* @return A [SharedPreferences] reference that will be used to inititialize [userLifetimeStorage]
*/
@Suppress("TooGenericExceptionCaught")
open fun deserializeUserLifetime(): SharedPreferences {
val prefs =
applicationContext.getSharedPreferences(this.javaClass.simpleName, Context.MODE_PRIVATE)
return try {
for ((metricName, metricValue) in prefs.all.entries) {
if (metricValue !is String) {
continue
}
// Split the stored name in 2: we expect it to be in the format
// store#metric.name
val parts = metricName.split('#', limit = 2)
val storeData = dataStores[Lifetime.User.toString()]!!.getOrPut(parts[0]) { mutableMapOf() }
storeData[parts[1]] = singleMetricInstantiator(metricValue)
}
prefs
} catch (e: NullPointerException) {
// If we fail to deserialize, we can log the problem but keep on going.
logger.error("Failed to deserialize UUIDs with 'user' lifetime")
prefs
}
}
/**
* Retrieves the [recorded metric data][ScalarType] for the provided
* store name.
*
* @param storeName the name of the desired store
* @param clearStore whether or not to clearStore the requested store
*
* @return the [ScalarType] recorded in the requested store
*/
@Synchronized
fun getSnapshot(storeName: String, clearStore: Boolean): GenericDataStorage<ScalarType>? {
val allLifetimes: GenericDataStorage<ScalarType> = mutableMapOf()
// Make sure data with "user" lifetime is loaded before getting the snapshot.
userLifetimeStorage.all
// Get the metrics for all the supported lifetimes.
for ((_, store) in dataStores) {
store[storeName]?.let {
allLifetimes.putAll(it)
}
}
if (clearStore) {
// We only allow clearing metrics with the "ping" lifetime.
dataStores[Lifetime.Ping.toString()]!!.remove(storeName)
}
return if (allLifetimes.isNotEmpty()) allLifetimes else null
}
/**
* Get a snapshot of the stored data as a JSON object.
*
* @param storeName the name of the desired store
* @param clearStore whether or not to clearStore the requested store
*
* @return the [JSONObject] containing the recorded data.
*/
override fun getSnapshotAsJSON(storeName: String, clearStore: Boolean): Any? {
return getSnapshot(storeName, clearStore)?.let { dataMap ->
return JSONObject(dataMap)
}
}
@VisibleForTesting
internal open fun clearAllStores() {
for ((_, store) in dataStores) {
store.clear()
}
}
}
/**
* This class implements the behaviour for UuidsStorageEngine. This is separate
* from the object to make it easier to test it.
*/
open class UuidsStorageEngineImplementation(
override val logger: Logger = Logger("")
) : ScalarStorageImpl<UUID>() {
override fun singleMetricInstantiator(value: Any?): UUID {
if (value is String) {
return UUID.fromString(value)
}
return UUID.randomUUID()
}
/**
* Record a uuid in the desired stores.
*
* @param stores the list of stores to record the uuid into
* @param category the category of the uuid
* @param name the name of the uuid
* @param value the uuid value to record
*/
@Synchronized
fun record(
stores: List<String>,
category: String,
name: String,
lifetime: Lifetime,
value: UUID
) {
checkNotNull(applicationContext) { "No recording can take place without an application context" }
// Record a copy of the uuid in all the needed stores.
val userPrefs: SharedPreferences.Editor? =
if (lifetime == Lifetime.User) userLifetimeStorage.edit() else null
for (storeName in stores) {
val storeData = dataStores[lifetime.toString()]!!.getOrPut(storeName) { mutableMapOf() }
val entryName = "$category.$name"
storeData[entryName] = value
// Persist data with "user" lifetime
if (lifetime == Lifetime.User) {
userPrefs?.putString("$storeName#$entryName", value.toString())
}
}
userPrefs?.apply()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment