Initial ClockPlugin interfaces and ClockRegistry functions

Bug: 229771520
Test: Automated
Change-Id: I2bc8be56f9fb91734aa7d430270c3e9ad7328ed0
(cherry picked from commit c225417205b8f44086556681a10457ae4c7db917)
Merged-In: I2bc8be56f9fb91734aa7d430270c3e9ad7328ed0
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockPlugin.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockPlugin.java
index 3058d94..bef61b8 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockPlugin.java
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockPlugin.java
@@ -23,7 +23,9 @@
 
 /**
  * Plugin used to replace main clock in keyguard.
+ * @deprecated Migrating to ClockProviderPlugin
  */
+@Deprecated
 @ProvidesInterface(action = ClockPlugin.ACTION, version = ClockPlugin.VERSION)
 public interface ClockPlugin extends Plugin {
 
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt
new file mode 100644
index 0000000..032f946
--- /dev/null
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+package com.android.systemui.plugins
+
+import com.android.systemui.plugins.annotations.ProvidesInterface
+import android.annotation.FloatRange
+import android.graphics.drawable.Drawable
+import android.view.View
+
+/** Identifies a clock design */
+typealias ClockId = String
+
+/** A Plugin which exposes the ClockProvider interface */
+@ProvidesInterface(action = ClockProviderPlugin.ACTION, version = ClockProviderPlugin.VERSION)
+interface ClockProviderPlugin : Plugin, ClockProvider {
+    companion object {
+        const val ACTION = "com.android.systemui.action.PLUGIN_CLOCK_PROVIDER"
+        const val VERSION = 1
+    }
+}
+
+/** Interface for building clocks and providing information about those clocks */
+interface ClockProvider {
+    /** Returns metadata for all clocks this provider knows about */
+    fun getClocks(): List<ClockMetadata>
+
+    /** Initializes and returns the target clock design */
+    fun createClock(id: ClockId): Clock
+
+    /** A static thumbnail for rendering in some examples */
+    fun getClockThumbnail(id: ClockId): Drawable?
+}
+
+/** Interface for controlling an active clock */
+interface Clock {
+    /** A small version of the clock, appropriate for smaller viewports */
+    val smallClock: View
+
+    /** A large version of the clock, appropriate when a bigger viewport is available */
+    val largeClock: View
+
+    /** Callback to update the clock view to the current time */
+    fun onTimeTick()
+
+    /** Sets the level of the AOD transition */
+    fun setAodFraction(@FloatRange(from = 0.0, to = 1.0) fraction: Float)
+}
+
+/** Some data about a clock design */
+data class ClockMetadata(
+    val clockId: ClockId,
+    val name: String
+)
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/keyguard/clock/ClockManager.java b/packages/SystemUI/src/com/android/keyguard/clock/ClockManager.java
index 013cdac..9a0bfc1 100644
--- a/packages/SystemUI/src/com/android/keyguard/clock/ClockManager.java
+++ b/packages/SystemUI/src/com/android/keyguard/clock/ClockManager.java
@@ -53,8 +53,11 @@
 
 /**
  * Manages custom clock faces for AOD and lock screen.
+ *
+ * @deprecated Migrate to ClockRegistry
  */
 @SysUISingleton
+@Deprecated
 public final class ClockManager {
 
     private static final String TAG = "ClockOptsProvider";
diff --git a/packages/SystemUI/src/com/android/keyguard/clock/ClockRegistry.kt b/packages/SystemUI/src/com/android/keyguard/clock/ClockRegistry.kt
new file mode 100644
index 0000000..c168a46
--- /dev/null
+++ b/packages/SystemUI/src/com/android/keyguard/clock/ClockRegistry.kt
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+package com.android.keyguard.clock
+
+import android.content.Context
+import android.graphics.drawable.Drawable
+import android.util.Log
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.plugins.Clock
+import com.android.systemui.plugins.ClockId
+import com.android.systemui.plugins.ClockMetadata
+import com.android.systemui.plugins.ClockProvider
+import com.android.systemui.plugins.ClockProviderPlugin
+import com.android.systemui.plugins.PluginListener
+import com.android.systemui.shared.plugins.PluginManager
+import javax.inject.Inject
+
+private val TAG = ClockRegistry::class.simpleName
+private const val DEFAULT_CLOCK_ID = "DEFAULT"
+
+typealias ClockChangeListener = () -> Unit
+
+/** ClockRegistry aggregates providers and plugins */
+// TODO: Is this the right place for this?
+@SysUISingleton
+class ClockRegistry @Inject constructor(
+    val context: Context,
+    val pluginManager: PluginManager
+) {
+    val pluginListener = object : PluginListener<ClockProviderPlugin> {
+        override fun onPluginConnected(plugin: ClockProviderPlugin, context: Context) {
+            for (clock in plugin.getClocks()) {
+                val id = clock.clockId
+                val current = availableClocks[id]
+                if (current != null) {
+                    Log.e(TAG, "Clock Id conflict: $id is registered by both " +
+                            "${plugin::class.simpleName} and ${current.provider::class.simpleName}")
+                    return
+                }
+
+                availableClocks[id] = ClockInfo(clock, plugin)
+            }
+        }
+
+        override fun onPluginDisconnected(plugin: ClockProviderPlugin) {
+            for (clock in plugin.getClocks()) {
+                availableClocks.remove(clock.clockId)
+            }
+        }
+    }
+
+    private val availableClocks = mutableMapOf<ClockId, ClockInfo>()
+
+    init {
+        pluginManager.addPluginListener(pluginListener, ClockProviderPlugin::class.java)
+        // TODO: Register Settings ContentObserver
+    }
+
+    fun getClocks(): List<ClockMetadata> = availableClocks.map { (_, clock) -> clock.metadata }
+
+    fun getClockThumbnail(clockId: ClockId): Drawable? =
+        availableClocks[clockId]?.provider?.getClockThumbnail(clockId)
+
+    fun createExampleClock(clockId: ClockId): Clock? = createClock(clockId)
+
+    fun getCurrentClock(): Clock {
+        val clockId = "" // TODO: Load setting
+        if (!clockId.isNullOrEmpty()) {
+            val clock = createClock(clockId)
+            if (clock != null) {
+                return clock
+            } else {
+                Log.e(TAG, "Clock $clockId not found; using default")
+            }
+        }
+
+        return createClock(DEFAULT_CLOCK_ID)!!
+    }
+
+    private fun createClock(clockId: ClockId): Clock? =
+        availableClocks[clockId]?.provider?.createClock(clockId)
+
+    fun setCurrentClock(clockId: ClockId) {
+        // TODO: Write Setting
+    }
+
+    private data class ClockInfo(
+        val metadata: ClockMetadata,
+        val provider: ClockProvider
+    )
+}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/clock/ClockRegistryTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/clock/ClockRegistryTest.kt
new file mode 100644
index 0000000..dc28e4d
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/keyguard/clock/ClockRegistryTest.kt
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.keyguard.clock
+
+import android.content.Context
+import android.graphics.drawable.Drawable
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.plugins.Clock
+import com.android.systemui.plugins.ClockId
+import com.android.systemui.plugins.ClockMetadata
+import com.android.systemui.plugins.ClockProviderPlugin
+import com.android.systemui.plugins.PluginListener
+import com.android.systemui.shared.plugins.PluginManager
+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 org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.junit.MockitoJUnit
+
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+class ClockRegistryTest : SysuiTestCase() {
+
+    @JvmField @Rule val mockito = MockitoJUnit.rule()
+    @Mock private lateinit var mockContext: Context
+    @Mock private lateinit var mockPluginManager: PluginManager
+    @Mock private lateinit var mockClock: Clock
+    @Mock private lateinit var mockThumbnail: Drawable
+    private lateinit var pluginListener: PluginListener<ClockProviderPlugin>
+    private lateinit var registry: ClockRegistry
+
+    private fun failFactory(): Clock {
+        fail("Unexpected call to createClock")
+        return null!!
+    }
+
+    private fun failThumbnail(): Drawable? {
+        fail("Unexpected call to getThumbnail")
+        return null
+    }
+
+    private class FakeClockPlugin : ClockProviderPlugin {
+        private val metadata = mutableListOf<ClockMetadata>()
+        private val createCallbacks = mutableMapOf<ClockId, () -> Clock>()
+        private val thumbnailCallbacks = mutableMapOf<ClockId, () -> Drawable?>()
+
+        override fun getClocks() = metadata
+        override fun createClock(id: ClockId): Clock = createCallbacks[id]!!()
+        override fun getClockThumbnail(id: ClockId): Drawable? = thumbnailCallbacks[id]!!()
+
+        fun addClock(
+            id: ClockId,
+            name: String,
+            create: () -> Clock,
+            getThumbnail: () -> Drawable?
+        ) {
+            metadata.add(ClockMetadata(id, name))
+            createCallbacks[id] = create
+            thumbnailCallbacks[id] = getThumbnail
+        }
+    }
+
+    @Before
+    fun setUp() {
+        val captor = argumentCaptor<PluginListener<ClockProviderPlugin>>()
+        registry = ClockRegistry(mockContext, mockPluginManager)
+        verify(mockPluginManager).addPluginListener(captor.capture(),
+            eq(ClockProviderPlugin::class.java))
+        pluginListener = captor.value
+    }
+
+    @Test
+    fun pluginRegistration_CorrectState() {
+        val plugin1 = FakeClockPlugin()
+        plugin1.addClock("clock_1", "clock 1", ::failFactory, ::failThumbnail)
+        plugin1.addClock("clock_2", "clock 2", ::failFactory, ::failThumbnail)
+
+        val plugin2 = FakeClockPlugin()
+        plugin2.addClock("clock_3", "clock 3", ::failFactory, ::failThumbnail)
+        plugin2.addClock("clock_4", "clock 4", ::failFactory, ::failThumbnail)
+
+        pluginListener.onPluginConnected(plugin1, mockContext)
+        pluginListener.onPluginConnected(plugin2, mockContext)
+        val list = registry.getClocks()
+        assertEquals(list, listOf(
+            ClockMetadata("clock_1", "clock 1"),
+            ClockMetadata("clock_2", "clock 2"),
+            ClockMetadata("clock_3", "clock 3"),
+            ClockMetadata("clock_4", "clock 4")
+        ))
+    }
+
+    @Test
+    fun clockIdConflict_ErrorWithoutCrash() {
+        val plugin1 = FakeClockPlugin()
+        plugin1.addClock("clock_1", "clock 1", { mockClock }, { mockThumbnail })
+        plugin1.addClock("clock_2", "clock 2", { mockClock }, { mockThumbnail })
+
+        val plugin2 = FakeClockPlugin()
+        plugin2.addClock("clock_1", "clock 1", ::failFactory, ::failThumbnail)
+        plugin2.addClock("clock_2", "clock 2", ::failFactory, ::failThumbnail)
+
+        pluginListener.onPluginConnected(plugin1, mockContext)
+        pluginListener.onPluginConnected(plugin2, mockContext)
+        val list = registry.getClocks()
+        assertEquals(list, listOf(
+            ClockMetadata("clock_1", "clock 1"),
+            ClockMetadata("clock_2", "clock 2")
+        ))
+
+        assertEquals(registry.createExampleClock("clock_1"), mockClock)
+        assertEquals(registry.createExampleClock("clock_2"), mockClock)
+        assertEquals(registry.getClockThumbnail("clock_1"), mockThumbnail)
+        assertEquals(registry.getClockThumbnail("clock_2"), mockThumbnail)
+    }
+}