diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt index a523cf10cf3b2..ed6e6198f139d 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt @@ -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() private val clockChangeListeners = mutableListOf() - private val settingObserver = object : ContentObserver(handler) { - override fun onChange(selfChange: Boolean, uris: Collection, flags: Int, userId: Int) = - clockChangeListeners.forEach { it.onClockChanged() } - } + private val settingObserver = + object : ContentObserver(null) { + override fun onChange( + selfChange: Boolean, + uris: Collection, + flags: Int, + userId: Int + ) { + scope.launch(bgDispatcher) { querySettings() } + } + } - private val pluginListener = object : PluginListener { - override fun onPluginConnected(plugin: ClockProviderPlugin, context: Context) = - connectClocks(plugin) + private val pluginListener = + object : PluginListener { + 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 { + fun getClocks(): List { 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, ) } diff --git a/packages/SystemUI/src/com/android/systemui/util/Assert.java b/packages/SystemUI/customization/src/com/android/systemui/util/Assert.java similarity index 100% rename from packages/SystemUI/src/com/android/systemui/util/Assert.java rename to packages/SystemUI/customization/src/com/android/systemui/util/Assert.java diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt index 590015df37aae..1c2f38beb867a 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt @@ -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 } } } diff --git a/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java b/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java index 676979cd39317..b1a83fbda7de6 100644 --- a/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java +++ b/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java @@ -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; } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt index 78bebb92dc709..d01edccb6a82e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt @@ -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 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) } }