Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit ad48b6dd authored by Hawkwood Glazier's avatar Hawkwood Glazier
Browse files

Multiuser support in ClockRegistry

This change correctly supports settings for the currently active
user, and properly switches when a user switch occurs. It also takes
the Settings.Secure binder calls off the main thread.

Bug: 267535577
Bug: 265061202
Bug: 267372164
Test: Checked multiuser scenario on device
Change-Id: I467212aa452ec3f7fcfb74f4bf60be5805803672
parent 6c1198d7
Loading
Loading
Loading
Loading
+165 −60
Original line number Diff line number Diff line
@@ -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,12 +58,20 @@ 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> {
    private val pluginListener =
        object : PluginListener<ClockProviderPlugin> {
            override fun onPluginConnected(plugin: ClockProviderPlugin, context: Context) =
                connectClocks(plugin)

@@ -63,53 +79,111 @@ open class ClockRegistry(
                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(
                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
                        )
                if (json == null || json.isEmpty()) {
                    return null
                    }
                return ClockSettings.deserialize(json)

                ClockSettings.deserialize(json)
            } catch (ex: Exception) {
                Log.e(TAG, "Failed to parse clock settings", ex)
                return null
                null
            }
        settings = result
    }
        protected set(value) {

    @OpenForTesting
    open fun applySettings(value: ClockSettings?) {
        assertNotMainThread()

        try {
                val json = if (value != null) {
                    value._applied_timestamp = System.currentTimeMillis()
                    ClockSettings.serialize(value)
                } else {
                    ""
                }
            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)
        }
        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"
            )
        }
    }

    fun registerListeners() {
        if (!isEnabled || isRegistered) {
            return
        }

        isRegistered = true

        if (isEnabled) {
        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,
    )
}
+24 −11
Original line number Diff line number Diff line
@@ -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(
            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)) json.getLong(KEY_TIMESTAMP) else null
                    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
        }
    }
}
+13 −6
Original line number Diff line number Diff line
@@ -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;
    }
}
+33 −21
Original line number Diff line number Diff line
@@ -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)
    }
}