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

Commit c067f64b authored by Hawkwood Glazier's avatar Hawkwood Glazier Committed by Automerger Merge Worker
Browse files

Merge "Multiuser support in ClockRegistry" into tm-qpr-dev am: c38b9fdb

parents 45ca59f1 c38b9fdb
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)
    }
}