Merge "Multiuser support in ClockRegistry" into tm-qpr-dev

This commit is contained in:
Hawkwood Glazier
2023-02-10 20:31:34 +00:00
committed by Android (Google) Code Review
5 changed files with 235 additions and 98 deletions

View File

@@ -13,11 +13,13 @@
*/
package com.android.systemui.shared.clocks
import android.app.ActivityManager
import android.app.UserSwitchObserver
import android.content.Context
import android.database.ContentObserver
import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Handler
import android.os.UserHandle
import android.provider.Settings
import android.util.Log
import androidx.annotation.OpenForTesting
@@ -29,17 +31,23 @@ import com.android.systemui.plugins.ClockProviderPlugin
import com.android.systemui.plugins.ClockSettings
import com.android.systemui.plugins.PluginListener
import com.android.systemui.plugins.PluginManager
import com.android.systemui.util.Assert
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
private val TAG = ClockRegistry::class.simpleName
private val TAG = ClockRegistry::class.simpleName!!
private const val DEBUG = true
/** ClockRegistry aggregates providers and plugins */
open class ClockRegistry(
val context: Context,
val pluginManager: PluginManager,
val handler: Handler,
val scope: CoroutineScope,
val mainDispatcher: CoroutineDispatcher,
val bgDispatcher: CoroutineDispatcher,
val isEnabled: Boolean,
userHandle: Int,
val handleAllUsers: Boolean,
defaultClockProvider: ClockProvider,
val fallbackClockId: ClockId = DEFAULT_CLOCK_ID,
) {
@@ -50,66 +58,132 @@ open class ClockRegistry(
private val availableClocks = mutableMapOf<ClockId, ClockInfo>()
private val clockChangeListeners = mutableListOf<ClockChangeListener>()
private val settingObserver = object : ContentObserver(handler) {
override fun onChange(selfChange: Boolean, uris: Collection<Uri>, flags: Int, userId: Int) =
clockChangeListeners.forEach { it.onClockChanged() }
}
private val settingObserver =
object : ContentObserver(null) {
override fun onChange(
selfChange: Boolean,
uris: Collection<Uri>,
flags: Int,
userId: Int
) {
scope.launch(bgDispatcher) { querySettings() }
}
}
private val pluginListener = object : PluginListener<ClockProviderPlugin> {
override fun onPluginConnected(plugin: ClockProviderPlugin, context: Context) =
connectClocks(plugin)
private val pluginListener =
object : PluginListener<ClockProviderPlugin> {
override fun onPluginConnected(plugin: ClockProviderPlugin, context: Context) =
connectClocks(plugin)
override fun onPluginDisconnected(plugin: ClockProviderPlugin) =
disconnectClocks(plugin)
}
override fun onPluginDisconnected(plugin: ClockProviderPlugin) =
disconnectClocks(plugin)
}
open var settings: ClockSettings?
get() {
private val userSwitchObserver =
object : UserSwitchObserver() {
override fun onUserSwitchComplete(newUserId: Int) {
scope.launch(bgDispatcher) { querySettings() }
}
}
// TODO(b/267372164): Migrate to flows
var settings: ClockSettings? = null
get() = field
protected set(value) {
if (field != value) {
field = value
scope.launch(mainDispatcher) { onClockChanged() }
}
}
var isRegistered: Boolean = false
private set
@OpenForTesting
open fun querySettings() {
assertNotMainThread()
val result =
try {
val json = Settings.Secure.getString(
context.contentResolver,
Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE
)
if (json == null || json.isEmpty()) {
return null
}
return ClockSettings.deserialize(json)
val json =
if (handleAllUsers) {
Settings.Secure.getStringForUser(
context.contentResolver,
Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE,
ActivityManager.getCurrentUser()
)
} else {
Settings.Secure.getString(
context.contentResolver,
Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE
)
}
ClockSettings.deserialize(json)
} catch (ex: Exception) {
Log.e(TAG, "Failed to parse clock settings", ex)
return null
null
}
}
protected set(value) {
try {
val json = if (value != null) {
value._applied_timestamp = System.currentTimeMillis()
ClockSettings.serialize(value)
} else {
""
}
settings = result
}
@OpenForTesting
open fun applySettings(value: ClockSettings?) {
assertNotMainThread()
try {
value?._applied_timestamp = System.currentTimeMillis()
val json = ClockSettings.serialize(value)
if (handleAllUsers) {
Settings.Secure.putStringForUser(
context.contentResolver,
Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE,
json,
ActivityManager.getCurrentUser()
)
} else {
Settings.Secure.putString(
context.contentResolver,
Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE, json
Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE,
json
)
} catch (ex: Exception) {
Log.e(TAG, "Failed to set clock settings", ex)
}
} catch (ex: Exception) {
Log.e(TAG, "Failed to set clock settings", ex)
}
settings = value
}
private fun mutateSetting(mutator: (ClockSettings) -> Unit) {
val settings = this.settings ?: ClockSettings()
mutator(settings)
this.settings = settings
@OpenForTesting
protected open fun assertMainThread() {
Assert.isMainThread()
}
@OpenForTesting
protected open fun assertNotMainThread() {
Assert.isNotMainThread()
}
private fun onClockChanged() {
assertMainThread()
clockChangeListeners.forEach { it.onClockChanged() }
}
private fun mutateSetting(mutator: (ClockSettings) -> ClockSettings) {
scope.launch(bgDispatcher) { applySettings(mutator(settings ?: ClockSettings())) }
}
var currentClockId: ClockId
get() = settings?.clockId ?: fallbackClockId
set(value) { mutateSetting { it.clockId = value } }
set(value) {
mutateSetting { it.copy(clockId = value) }
}
var seedColor: Int?
get() = settings?.seedColor
set(value) { mutateSetting { it.seedColor = value } }
set(value) {
mutateSetting { it.copy(seedColor = value) }
}
init {
connectClocks(defaultClockProvider)
@@ -118,19 +192,51 @@ open class ClockRegistry(
"$defaultClockProvider did not register clock at $DEFAULT_CLOCK_ID"
)
}
}
if (isEnabled) {
pluginManager.addPluginListener(
pluginListener,
ClockProviderPlugin::class.java,
/*allowMultiple=*/ true
)
fun registerListeners() {
if (!isEnabled || isRegistered) {
return
}
isRegistered = true
pluginManager.addPluginListener(
pluginListener,
ClockProviderPlugin::class.java,
/*allowMultiple=*/ true
)
scope.launch(bgDispatcher) { querySettings() }
if (handleAllUsers) {
context.contentResolver.registerContentObserver(
Settings.Secure.getUriFor(Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE),
/*notifyForDescendants=*/ false,
settingObserver,
userHandle
UserHandle.USER_ALL
)
ActivityManager.getService().registerUserSwitchObserver(userSwitchObserver, TAG)
} else {
context.contentResolver.registerContentObserver(
Settings.Secure.getUriFor(Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE),
/*notifyForDescendants=*/ false,
settingObserver
)
}
}
fun unregisterListeners() {
if (!isRegistered) {
return
}
isRegistered = false
pluginManager.removePluginListener(pluginListener)
context.contentResolver.unregisterContentObserver(settingObserver)
if (handleAllUsers) {
ActivityManager.getService().unregisterUserSwitchObserver(userSwitchObserver)
}
}
@@ -157,7 +263,7 @@ open class ClockRegistry(
if (DEBUG) {
Log.i(TAG, "Current clock ($currentId) was connected")
}
clockChangeListeners.forEach { it.onClockChanged() }
onClockChanged()
}
}
}
@@ -172,13 +278,12 @@ open class ClockRegistry(
if (currentId == clock.clockId) {
Log.w(TAG, "Current clock ($currentId) was disconnected")
clockChangeListeners.forEach { it.onClockChanged() }
onClockChanged()
}
}
}
@OpenForTesting
open fun getClocks(): List<ClockMetadata> {
fun getClocks(): List<ClockMetadata> {
if (!isEnabled) {
return listOf(availableClocks[DEFAULT_CLOCK_ID]!!.metadata)
}
@@ -213,16 +318,16 @@ open class ClockRegistry(
return createClock(DEFAULT_CLOCK_ID)!!
}
private fun createClock(clockId: ClockId): ClockController? {
val settings = this.settings ?: ClockSettings()
if (clockId != settings.clockId) {
settings.clockId = clockId
private fun createClock(targetClockId: ClockId): ClockController? {
var settings = this.settings ?: ClockSettings()
if (targetClockId != settings.clockId) {
settings = settings.copy(clockId = targetClockId)
}
return availableClocks[clockId]?.provider?.createClock(settings)
return availableClocks[targetClockId]?.provider?.createClock(settings)
}
private data class ClockInfo(
val metadata: ClockMetadata,
val provider: ClockProvider
val provider: ClockProvider,
)
}

View File

@@ -45,7 +45,7 @@ interface ClockProvider {
/** Initializes and returns the target clock design */
@Deprecated("Use overload with ClockSettings")
fun createClock(id: ClockId): ClockController {
return createClock(ClockSettings(id, null, null))
return createClock(ClockSettings(id, null))
}
/** Initializes and returns the target clock design */
@@ -186,16 +186,21 @@ data class ClockMetadata(
/** Structure for keeping clock-specific settings */
@Keep
data class ClockSettings(
var clockId: ClockId? = null,
var seedColor: Int? = null,
var _applied_timestamp: Long? = null,
val clockId: ClockId? = null,
val seedColor: Int? = null,
) {
var _applied_timestamp: Long? = null
companion object {
private val KEY_CLOCK_ID = "clockId"
private val KEY_SEED_COLOR = "seedColor"
private val KEY_TIMESTAMP = "_applied_timestamp"
fun serialize(setting: ClockSettings): String {
fun serialize(setting: ClockSettings?): String {
if (setting == null) {
return ""
}
return JSONObject()
.put(KEY_CLOCK_ID, setting.clockId)
.put(KEY_SEED_COLOR, setting.seedColor)
@@ -203,13 +208,21 @@ data class ClockSettings(
.toString()
}
fun deserialize(jsonStr: String): ClockSettings {
fun deserialize(jsonStr: String?): ClockSettings? {
if (jsonStr.isNullOrEmpty()) {
return null
}
val json = JSONObject(jsonStr)
return ClockSettings(
json.getString(KEY_CLOCK_ID),
if (!json.isNull(KEY_SEED_COLOR)) json.getInt(KEY_SEED_COLOR) else null,
if (!json.isNull(KEY_TIMESTAMP)) json.getLong(KEY_TIMESTAMP) else null
)
val result =
ClockSettings(
json.getString(KEY_CLOCK_ID),
if (!json.isNull(KEY_SEED_COLOR)) json.getInt(KEY_SEED_COLOR) else null
)
if (!json.isNull(KEY_TIMESTAMP)) {
result._applied_timestamp = json.getLong(KEY_TIMESTAMP)
}
return result
}
}
}

View File

@@ -18,13 +18,12 @@ package com.android.keyguard.dagger;
import android.content.Context;
import android.content.res.Resources;
import android.os.Handler;
import android.os.UserHandle;
import android.view.LayoutInflater;
import com.android.systemui.R;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Application;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.flags.Flags;
@@ -34,6 +33,8 @@ import com.android.systemui.shared.clocks.DefaultClockProvider;
import dagger.Module;
import dagger.Provides;
import kotlinx.coroutines.CoroutineDispatcher;
import kotlinx.coroutines.CoroutineScope;
/** Dagger Module for clocks. */
@Module
@@ -44,17 +45,23 @@ public abstract class ClockRegistryModule {
public static ClockRegistry getClockRegistry(
@Application Context context,
PluginManager pluginManager,
@Main Handler handler,
@Application CoroutineScope scope,
@Main CoroutineDispatcher mainDispatcher,
@Background CoroutineDispatcher bgDispatcher,
FeatureFlags featureFlags,
@Main Resources resources,
LayoutInflater layoutInflater) {
return new ClockRegistry(
ClockRegistry registry = new ClockRegistry(
context,
pluginManager,
handler,
scope,
mainDispatcher,
bgDispatcher,
featureFlags.isEnabled(Flags.LOCKSCREEN_CUSTOM_CLOCKS),
UserHandle.USER_ALL,
/* handleAllUsers= */ true,
new DefaultClockProvider(context, layoutInflater, resources),
context.getString(R.string.lockscreen_clock_id_fallback));
registry.registerListeners();
return registry;
}
}

View File

@@ -18,8 +18,6 @@ package com.android.systemui.shared.clocks
import android.content.ContentResolver
import android.content.Context
import android.graphics.drawable.Drawable
import android.os.Handler
import android.os.UserHandle
import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
@@ -34,6 +32,9 @@ import com.android.systemui.util.mockito.argumentCaptor
import com.android.systemui.util.mockito.eq
import junit.framework.Assert.assertEquals
import junit.framework.Assert.fail
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
import org.json.JSONException
import org.junit.Before
import org.junit.Rule
@@ -49,19 +50,19 @@ import org.mockito.junit.MockitoJUnit
class ClockRegistryTest : SysuiTestCase() {
@JvmField @Rule val mockito = MockitoJUnit.rule()
private lateinit var dispatcher: CoroutineDispatcher
private lateinit var scope: TestScope
@Mock private lateinit var mockContext: Context
@Mock private lateinit var mockPluginManager: PluginManager
@Mock private lateinit var mockClock: ClockController
@Mock private lateinit var mockDefaultClock: ClockController
@Mock private lateinit var mockThumbnail: Drawable
@Mock private lateinit var mockHandler: Handler
@Mock private lateinit var mockContentResolver: ContentResolver
private lateinit var fakeDefaultProvider: FakeClockPlugin
private lateinit var pluginListener: PluginListener<ClockProviderPlugin>
private lateinit var registry: ClockRegistry
private var settingValue: ClockSettings? = null
companion object {
private fun failFactory(): ClockController {
fail("Unexpected call to createClock")
@@ -99,6 +100,9 @@ class ClockRegistryTest : SysuiTestCase() {
@Before
fun setUp() {
dispatcher = StandardTestDispatcher()
scope = TestScope(dispatcher)
fakeDefaultProvider = FakeClockPlugin()
.addClock(DEFAULT_CLOCK_ID, DEFAULT_CLOCK_NAME, { mockDefaultClock }, { mockThumbnail })
whenever(mockContext.contentResolver).thenReturn(mockContentResolver)
@@ -107,15 +111,22 @@ class ClockRegistryTest : SysuiTestCase() {
registry = object : ClockRegistry(
mockContext,
mockPluginManager,
mockHandler,
scope = scope.backgroundScope,
mainDispatcher = dispatcher,
bgDispatcher = dispatcher,
isEnabled = true,
userHandle = UserHandle.USER_ALL,
defaultClockProvider = fakeDefaultProvider
handleAllUsers = true,
defaultClockProvider = fakeDefaultProvider,
) {
override var settings: ClockSettings?
get() = settingValue
set(value) { settingValue = value }
override fun querySettings() { }
override fun applySettings(value: ClockSettings?) {
settings = value
}
// Unit Test does not validate threading
override fun assertMainThread() {}
override fun assertNotMainThread() {}
}
registry.registerListeners()
verify(mockPluginManager)
.addPluginListener(captor.capture(), eq(ClockProviderPlugin::class.java), eq(true))
@@ -187,16 +198,16 @@ class ClockRegistryTest : SysuiTestCase() {
.addClock("clock_1", "clock 1")
.addClock("clock_2", "clock 2")
settingValue = ClockSettings("clock_3", null, null)
val plugin2 = FakeClockPlugin()
.addClock("clock_3", "clock 3", { mockClock })
.addClock("clock_4", "clock 4")
registry.applySettings(ClockSettings("clock_3", null))
pluginListener.onPluginConnected(plugin1, mockContext)
pluginListener.onPluginConnected(plugin2, mockContext)
val clock = registry.createCurrentClock()
assertEquals(clock, mockClock)
assertEquals(mockClock, clock)
}
@Test
@@ -205,11 +216,11 @@ class ClockRegistryTest : SysuiTestCase() {
.addClock("clock_1", "clock 1")
.addClock("clock_2", "clock 2")
settingValue = ClockSettings("clock_3", null, null)
val plugin2 = FakeClockPlugin()
.addClock("clock_3", "clock 3")
.addClock("clock_4", "clock 4")
registry.applySettings(ClockSettings("clock_3", null))
pluginListener.onPluginConnected(plugin1, mockContext)
pluginListener.onPluginConnected(plugin2, mockContext)
pluginListener.onPluginDisconnected(plugin2)
@@ -224,11 +235,11 @@ class ClockRegistryTest : SysuiTestCase() {
.addClock("clock_1", "clock 1")
.addClock("clock_2", "clock 2")
settingValue = ClockSettings("clock_3", null, null)
val plugin2 = FakeClockPlugin()
.addClock("clock_3", "clock 3", { mockClock })
.addClock("clock_4", "clock 4")
registry.applySettings(ClockSettings("clock_3", null))
pluginListener.onPluginConnected(plugin1, mockContext)
pluginListener.onPluginConnected(plugin2, mockContext)
@@ -244,7 +255,7 @@ class ClockRegistryTest : SysuiTestCase() {
@Test
fun jsonDeserialization_gotExpectedObject() {
val expected = ClockSettings("ID", null, 500)
val expected = ClockSettings("ID", null).apply { _applied_timestamp = 500 }
val actual = ClockSettings.deserialize("""{
"clockId":"ID",
"_applied_timestamp":500
@@ -254,14 +265,14 @@ class ClockRegistryTest : SysuiTestCase() {
@Test
fun jsonDeserialization_noTimestamp_gotExpectedObject() {
val expected = ClockSettings("ID", null, null)
val expected = ClockSettings("ID", null)
val actual = ClockSettings.deserialize("{\"clockId\":\"ID\"}")
assertEquals(expected, actual)
}
@Test
fun jsonDeserialization_nullTimestamp_gotExpectedObject() {
val expected = ClockSettings("ID", null, null)
val expected = ClockSettings("ID", null)
val actual = ClockSettings.deserialize("""{
"clockId":"ID",
"_applied_timestamp":null
@@ -271,7 +282,7 @@ class ClockRegistryTest : SysuiTestCase() {
@Test(expected = JSONException::class)
fun jsonDeserialization_noId_threwException() {
val expected = ClockSettings("ID", null, 500)
val expected = ClockSettings(null, null).apply { _applied_timestamp = 500 }
val actual = ClockSettings.deserialize("{\"_applied_timestamp\":500}")
assertEquals(expected, actual)
}
@@ -279,14 +290,15 @@ class ClockRegistryTest : SysuiTestCase() {
@Test
fun jsonSerialization_gotExpectedString() {
val expected = "{\"clockId\":\"ID\",\"_applied_timestamp\":500}"
val actual = ClockSettings.serialize(ClockSettings("ID", null, 500))
val actual = ClockSettings.serialize(ClockSettings("ID", null)
.apply { _applied_timestamp = 500 })
assertEquals(expected, actual)
}
@Test
fun jsonSerialization_noTimestamp_gotExpectedString() {
val expected = "{\"clockId\":\"ID\"}"
val actual = ClockSettings.serialize(ClockSettings("ID", null, null))
val actual = ClockSettings.serialize(ClockSettings("ID", null))
assertEquals(expected, actual)
}
}