1 /*
2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
5  * except in compliance with the License. You may obtain a copy of the License at
6  *
7  *      http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software distributed under the
10  * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
11  * KIND, either express or implied. See the License for the specific language governing
12  * permissions and limitations under the License.
13  */
14 package com.android.systemui.plugins
15 
16 import android.content.res.Resources
17 import android.graphics.Rect
18 import android.graphics.drawable.Drawable
19 import android.view.View
20 import com.android.internal.annotations.Keep
21 import com.android.systemui.log.core.MessageBuffer
22 import com.android.systemui.plugins.annotations.ProvidesInterface
23 import java.io.PrintWriter
24 import java.util.Locale
25 import java.util.TimeZone
26 import org.json.JSONObject
27 
28 /** Identifies a clock design */
29 typealias ClockId = String
30 
31 /** A Plugin which exposes the ClockProvider interface */
32 @ProvidesInterface(action = ClockProviderPlugin.ACTION, version = ClockProviderPlugin.VERSION)
33 interface ClockProviderPlugin : Plugin, ClockProvider {
34     companion object {
35         const val ACTION = "com.android.systemui.action.PLUGIN_CLOCK_PROVIDER"
36         const val VERSION = 1
37     }
38 }
39 
40 /** Interface for building clocks and providing information about those clocks */
41 interface ClockProvider {
42     /** Returns metadata for all clocks this provider knows about */
43     fun getClocks(): List<ClockMetadata>
44 
45     /** Initializes and returns the target clock design */
46     @Deprecated("Use overload with ClockSettings")
47     fun createClock(id: ClockId): ClockController {
48         return createClock(ClockSettings(id, null))
49     }
50 
51     /** Initializes and returns the target clock design */
52     fun createClock(settings: ClockSettings): ClockController
53 
54     /** A static thumbnail for rendering in some examples */
55     fun getClockThumbnail(id: ClockId): Drawable?
56 }
57 
58 /** Interface for controlling an active clock */
59 interface ClockController {
60     /** A small version of the clock, appropriate for smaller viewports */
61     val smallClock: ClockFaceController
62 
63     /** A large version of the clock, appropriate when a bigger viewport is available */
64     val largeClock: ClockFaceController
65 
66     /** Determines the way the hosting app should behave when rendering either clock face */
67     val config: ClockConfig
68 
69     /** Events that clocks may need to respond to */
70     val events: ClockEvents
71 
72     /** Initializes various rendering parameters. If never called, provides reasonable defaults. */
73     fun initialize(
74         resources: Resources,
75         dozeFraction: Float,
76         foldFraction: Float,
77     )
78 
79     /** Optional method for dumping debug information */
80     fun dump(pw: PrintWriter)
81 }
82 
83 /** Interface for a specific clock face version rendered by the clock */
84 interface ClockFaceController {
85     /** View that renders the clock face */
86     val view: View
87 
88     /** Determines the way the hosting app should behave when rendering this clock face */
89     val config: ClockFaceConfig
90 
91     /** Events specific to this clock face */
92     val events: ClockFaceEvents
93 
94     /** Triggers for various animations */
95     val animations: ClockAnimations
96 
97     /** Some clocks may log debug information */
98     var messageBuffer: MessageBuffer?
99 }
100 
101 /** Events that should call when various rendering parameters change */
102 interface ClockEvents {
103     /** Call whenever timezone changes */
104     fun onTimeZoneChanged(timeZone: TimeZone)
105 
106     /** Call whenever the text time format changes (12hr vs 24hr) */
107     fun onTimeFormatChanged(is24Hr: Boolean)
108 
109     /** Call whenever the locale changes */
110     fun onLocaleChanged(locale: Locale)
111 
112     /** Call whenever the color palette should update */
113     fun onColorPaletteChanged(resources: Resources)
114 
115     /** Call if the seed color has changed and should be updated */
116     fun onSeedColorChanged(seedColor: Int?)
117 
118     /** Call whenever the weather data should update */
119     fun onWeatherDataChanged(data: WeatherData)
120 }
121 
122 /** Methods which trigger various clock animations */
123 interface ClockAnimations {
124     /** Runs an enter animation (if any) */
125     fun enter()
126 
127     /** Sets how far into AOD the device currently is. */
128     fun doze(fraction: Float)
129 
130     /** Sets how far into the folding animation the device is. */
131     fun fold(fraction: Float)
132 
133     /** Runs the battery animation (if any). */
134     fun charge()
135 
136     /**
137      * Runs when the clock's position changed during the move animation.
138      *
139      * @param fromLeft the [View.getLeft] position of the clock, before it started moving.
140      * @param direction the direction in which it is moving. A positive number means right, and
141      *   negative means left.
142      * @param fraction fraction of the clock movement. 0 means it is at the beginning, and 1 means
143      *   it finished moving.
144      */
145     fun onPositionUpdated(fromLeft: Int, direction: Int, fraction: Float)
146 
147     /**
148      * Runs when swiping clock picker, swipingFraction: 1.0 -> clock is scaled up in the preview,
149      * 0.0 -> clock is scaled down in the shade; previewRatio is previewSize / screenSize
150      */
151     fun onPickerCarouselSwiping(swipingFraction: Float)
152 }
153 
154 /** Events that have specific data about the related face */
155 interface ClockFaceEvents {
156     /** Call every time tick */
157     fun onTimeTick()
158 
159     /**
160      * Region Darkness specific to the clock face.
161      * - isRegionDark = dark theme -> clock should be light
162      * - !isRegionDark = light theme -> clock should be dark
163      */
164     fun onRegionDarknessChanged(isRegionDark: Boolean)
165 
166     /**
167      * Call whenever font settings change. Pass in a target font size in pixels. The specific clock
168      * design is allowed to ignore this target size on a case-by-case basis.
169      */
170     fun onFontSettingChanged(fontSizePx: Float)
171 
172     /**
173      * Target region information for the clock face. For small clock, this will match the bounds of
174      * the parent view mostly, but have a target height based on the height of the default clock.
175      * For large clocks, the parent view is the entire device size, but most clocks will want to
176      * render within the centered targetRect to avoid obstructing other elements. The specified
177      * targetRegion is relative to the parent view.
178      */
179     fun onTargetRegionChanged(targetRegion: Rect?)
180 
181     /** Called to notify the clock about its display. */
182     fun onSecondaryDisplayChanged(onSecondaryDisplay: Boolean)
183 }
184 
185 /** Tick rates for clocks */
186 enum class ClockTickRate(val value: Int) {
187     PER_MINUTE(2), // Update the clock once per minute.
188     PER_SECOND(1), // Update the clock once per second.
189     PER_FRAME(0), // Update the clock every second.
190 }
191 
192 /** Some data about a clock design */
193 data class ClockMetadata(
194     val clockId: ClockId,
195     val name: String,
196 ) {
197     constructor(clockId: ClockId) : this(clockId, clockId) {}
198 }
199 
200 /** Render configuration for the full clock. Modifies the way systemUI behaves with this clock. */
201 data class ClockConfig(
202     val id: String,
203 
204     /** Transition to AOD should move smartspace like large clock instead of small clock */
205     val useAlternateSmartspaceAODTransition: Boolean = false,
206 
207     /** True if the clock will react to tone changes in the seed color. */
208     val isReactiveToTone: Boolean = true,
209 )
210 
211 /** Render configuration options for a clock face. Modifies the way SystemUI behaves. */
212 data class ClockFaceConfig(
213     /** Expected interval between calls to onTimeTick. Can always reduce to PER_MINUTE in AOD. */
214     val tickRate: ClockTickRate = ClockTickRate.PER_MINUTE,
215 
216     /** Call to check whether the clock consumes weather data */
217     val hasCustomWeatherDataDisplay: Boolean = false,
218 
219     /**
220      * Whether this clock has a custom position update animation. If true, the keyguard will call
221      * `onPositionUpdated` to notify the clock of a position update animation. If false, a default
222      * animation will be used (e.g. a simple translation).
223      */
224     val hasCustomPositionUpdatedAnimation: Boolean = false,
225 )
226 
227 /** Structure for keeping clock-specific settings */
228 @Keep
229 data class ClockSettings(
230     val clockId: ClockId? = null,
231     val seedColor: Int? = null,
232 ) {
233     // Exclude metadata from equality checks
234     var metadata: JSONObject = JSONObject()
235 
236     companion object {
237         private val KEY_CLOCK_ID = "clockId"
238         private val KEY_SEED_COLOR = "seedColor"
239         private val KEY_METADATA = "metadata"
240 
241         fun serialize(setting: ClockSettings?): String {
242             if (setting == null) {
243                 return ""
244             }
245 
246             return JSONObject()
247                 .put(KEY_CLOCK_ID, setting.clockId)
248                 .put(KEY_SEED_COLOR, setting.seedColor)
249                 .put(KEY_METADATA, setting.metadata)
250                 .toString()
251         }
252 
253         fun deserialize(jsonStr: String?): ClockSettings? {
254             if (jsonStr.isNullOrEmpty()) {
255                 return null
256             }
257 
258             val json = JSONObject(jsonStr)
259             val result =
260                 ClockSettings(
261                     if (!json.isNull(KEY_CLOCK_ID)) json.getString(KEY_CLOCK_ID) else null,
262                     if (!json.isNull(KEY_SEED_COLOR)) json.getInt(KEY_SEED_COLOR) else null
263                 )
264             if (!json.isNull(KEY_METADATA)) {
265                 result.metadata = json.getJSONObject(KEY_METADATA)
266             }
267             return result
268         }
269     }
270 }
271