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.clocks
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 androidx.constraintlayout.widget.ConstraintSet
21 import com.android.internal.annotations.Keep
22 import com.android.systemui.log.core.MessageBuffer
23 import com.android.systemui.plugins.Plugin
24 import com.android.systemui.plugins.annotations.ProvidesInterface
25 import java.io.PrintWriter
26 import java.util.Locale
27 import java.util.TimeZone
28 import org.json.JSONObject
29 
30 /** Identifies a clock design */
31 typealias ClockId = String
32 
33 /** A Plugin which exposes the ClockProvider interface */
34 @ProvidesInterface(action = ClockProviderPlugin.ACTION, version = ClockProviderPlugin.VERSION)
35 interface ClockProviderPlugin : Plugin, ClockProvider {
36     companion object {
37         const val ACTION = "com.android.systemui.action.PLUGIN_CLOCK_PROVIDER"
38         const val VERSION = 1
39     }
40 }
41 
42 /** Interface for building clocks and providing information about those clocks */
43 interface ClockProvider {
44     /** Initializes the clock provider with debug log buffers */
initializenull45     fun initialize(buffers: ClockMessageBuffers?)
46 
47     /** Returns metadata for all clocks this provider knows about */
48     fun getClocks(): List<ClockMetadata>
49 
50     /** Initializes and returns the target clock design */
51     fun createClock(settings: ClockSettings): ClockController
52 
53     /** A static thumbnail for rendering in some examples */
54     fun getClockThumbnail(id: ClockId): Drawable?
55 }
56 
57 /** Interface for controlling an active clock */
58 interface ClockController {
59     /** A small version of the clock, appropriate for smaller viewports */
60     val smallClock: ClockFaceController
61 
62     /** A large version of the clock, appropriate when a bigger viewport is available */
63     val largeClock: ClockFaceController
64 
65     /** Determines the way the hosting app should behave when rendering either clock face */
66     val config: ClockConfig
67 
68     /** Events that clocks may need to respond to */
69     val events: ClockEvents
70 
71     /** Initializes various rendering parameters. If never called, provides reasonable defaults. */
72     fun initialize(
73         resources: Resources,
74         dozeFraction: Float,
75         foldFraction: Float,
76     )
77 
78     /** Optional method for dumping debug information */
79     fun dump(pw: PrintWriter)
80 }
81 
82 /** Interface for a specific clock face version rendered by the clock */
83 interface ClockFaceController {
84     /** View that renders the clock face */
85     val view: View
86 
87     /** Layout specification for this clock */
88     val layout: ClockFaceLayout
89 
90     /** Determines the way the hosting app should behave when rendering this clock face */
91     val config: ClockFaceConfig
92 
93     /** Events specific to this clock face */
94     val events: ClockFaceEvents
95 
96     /** Triggers for various animations */
97     val animations: ClockAnimations
98 }
99 
100 /** For clocks that want to report debug information */
101 data class ClockMessageBuffers(
102     /** Message buffer for general infra */
103     val infraMessageBuffer: MessageBuffer,
104 
105     /** Message buffer for small clock renering */
106     val smallClockMessageBuffer: MessageBuffer,
107 
108     /** Message buffer for large clock rendering */
109     val largeClockMessageBuffer: MessageBuffer,
110 )
111 
112 data class AodClockBurnInModel(
113     val scale: Float,
114     val translationX: Float,
115     val translationY: Float,
116 )
117 
118 /** Specifies layout information for the */
119 interface ClockFaceLayout {
120     /** All clock views to add to the root constraint layout before applying constraints. */
121     val views: List<View>
122 
123     /** Custom constraints to apply to Lockscreen ConstraintLayout. */
applyConstraintsnull124     fun applyConstraints(constraints: ConstraintSet): ConstraintSet
125 
126     fun applyPreviewConstraints(constraints: ConstraintSet): ConstraintSet
127 
128     fun applyAodBurnIn(aodBurnInModel: AodClockBurnInModel)
129 }
130 
131 /** A ClockFaceLayout that applies the default lockscreen layout to a single view */
132 class DefaultClockFaceLayout(val view: View) : ClockFaceLayout {
133     // both small and large clock should have a container (RelativeLayout in
134     // SimpleClockFaceController)
135     override val views = listOf(view)
136     override fun applyConstraints(constraints: ConstraintSet): ConstraintSet {
137         if (views.size != 1) {
138             throw IllegalArgumentException(
139                 "Should have only one container view when using DefaultClockFaceLayout"
140             )
141         }
142         return constraints
143     }
144 
145     override fun applyPreviewConstraints(constraints: ConstraintSet): ConstraintSet {
146         return constraints
147     }
148 
149     override fun applyAodBurnIn(aodBurnInModel: AodClockBurnInModel) {
150         // Default clock doesn't need detailed control of view
151     }
152 }
153 
154 /** Events that should call when various rendering parameters change */
155 interface ClockEvents {
156     /** Set to enable or disable swipe interaction */
157     var isReactiveTouchInteractionEnabled: Boolean
158 
159     /** Call whenever timezone changes */
onTimeZoneChangednull160     fun onTimeZoneChanged(timeZone: TimeZone)
161 
162     /** Call whenever the text time format changes (12hr vs 24hr) */
163     fun onTimeFormatChanged(is24Hr: Boolean)
164 
165     /** Call whenever the locale changes */
166     fun onLocaleChanged(locale: Locale)
167 
168     /** Call whenever the color palette should update */
169     fun onColorPaletteChanged(resources: Resources)
170 
171     /** Call if the seed color has changed and should be updated */
172     fun onSeedColorChanged(seedColor: Int?)
173 
174     /** Call whenever the weather data should update */
175     fun onWeatherDataChanged(data: WeatherData)
176 
177     /** Call with alarm information */
178     fun onAlarmDataChanged(data: AlarmData)
179 
180     /** Call with zen/dnd information */
181     fun onZenDataChanged(data: ZenData)
182 }
183 
184 /** Methods which trigger various clock animations */
185 interface ClockAnimations {
186     /** Runs an enter animation (if any) */
187     fun enter()
188 
189     /** Sets how far into AOD the device currently is. */
190     fun doze(fraction: Float)
191 
192     /** Sets how far into the folding animation the device is. */
193     fun fold(fraction: Float)
194 
195     /** Runs the battery animation (if any). */
196     fun charge()
197 
198     /**
199      * Runs when the clock's position changed during the move animation.
200      *
201      * @param fromLeft the [View.getLeft] position of the clock, before it started moving.
202      * @param direction the direction in which it is moving. A positive number means right, and
203      *   negative means left.
204      * @param fraction fraction of the clock movement. 0 means it is at the beginning, and 1 means
205      *   it finished moving.
206      * @deprecated use {@link #onPositionUpdated(float, float)} instead.
207      */
208     fun onPositionUpdated(fromLeft: Int, direction: Int, fraction: Float)
209 
210     /**
211      * Runs when the clock's position changed during the move animation.
212      *
213      * @param distance is the total distance in pixels to offset the glyphs when animation
214      *   completes. Negative distance means we are animating the position towards the center.
215      * @param fraction fraction of the clock movement. 0 means it is at the beginning, and 1 means
216      *   it finished moving.
217      */
218     fun onPositionUpdated(distance: Float, fraction: Float)
219 
220     /**
221      * Runs when swiping clock picker, swipingFraction: 1.0 -> clock is scaled up in the preview,
222      * 0.0 -> clock is scaled down in the shade; previewRatio is previewSize / screenSize
223      */
224     fun onPickerCarouselSwiping(swipingFraction: Float)
225 }
226 
227 /** Events that have specific data about the related face */
228 interface ClockFaceEvents {
229     /** Call every time tick */
onTimeTicknull230     fun onTimeTick()
231 
232     /**
233      * Region Darkness specific to the clock face.
234      * - isRegionDark = dark theme -> clock should be light
235      * - !isRegionDark = light theme -> clock should be dark
236      */
237     fun onRegionDarknessChanged(isRegionDark: Boolean)
238 
239     /**
240      * Call whenever font settings change. Pass in a target font size in pixels. The specific clock
241      * design is allowed to ignore this target size on a case-by-case basis.
242      */
243     fun onFontSettingChanged(fontSizePx: Float)
244 
245     /**
246      * Target region information for the clock face. For small clock, this will match the bounds of
247      * the parent view mostly, but have a target height based on the height of the default clock.
248      * For large clocks, the parent view is the entire device size, but most clocks will want to
249      * render within the centered targetRect to avoid obstructing other elements. The specified
250      * targetRegion is relative to the parent view.
251      */
252     fun onTargetRegionChanged(targetRegion: Rect?)
253 
254     /** Called to notify the clock about its display. */
255     fun onSecondaryDisplayChanged(onSecondaryDisplay: Boolean)
256 }
257 
258 /** Tick rates for clocks */
259 enum class ClockTickRate(val value: Int) {
260     PER_MINUTE(2), // Update the clock once per minute.
261     PER_SECOND(1), // Update the clock once per second.
262     PER_FRAME(0), // Update the clock every second.
263 }
264 
265 /** Some data about a clock design */
266 data class ClockMetadata(
267     val clockId: ClockId,
268 )
269 
270 /** Render configuration for the full clock. Modifies the way systemUI behaves with this clock. */
271 data class ClockConfig(
272     val id: String,
273 
274     /** Localized name of the clock */
275     val name: String,
276 
277     /** Localized accessibility description for the clock */
278     val description: String,
279 
280     /** Transition to AOD should move smartspace like large clock instead of small clock */
281     val useAlternateSmartspaceAODTransition: Boolean = false,
282 
283     /** True if the clock will react to tone changes in the seed color. */
284     val isReactiveToTone: Boolean = true,
285 
286     /** True if the clock is large frame clock, which will use weather in compose. */
287     val useCustomClockScene: Boolean = false,
288 )
289 
290 /** Render configuration options for a clock face. Modifies the way SystemUI behaves. */
291 data class ClockFaceConfig(
292     /** Expected interval between calls to onTimeTick. Can always reduce to PER_MINUTE in AOD. */
293     val tickRate: ClockTickRate = ClockTickRate.PER_MINUTE,
294 
295     /** Call to check whether the clock consumes weather data */
296     val hasCustomWeatherDataDisplay: Boolean = false,
297 
298     /**
299      * Whether this clock has a custom position update animation. If true, the keyguard will call
300      * `onPositionUpdated` to notify the clock of a position update animation. If false, a default
301      * animation will be used (e.g. a simple translation).
302      */
303     val hasCustomPositionUpdatedAnimation: Boolean = false,
304 
305     /** True if the clock is large frame clock, which will use weatherBlueprint in compose. */
306     val useCustomClockScene: Boolean = false,
307 )
308 
309 /** Structure for keeping clock-specific settings */
310 @Keep
311 data class ClockSettings(
312     val clockId: ClockId? = null,
313     val seedColor: Int? = null,
314 ) {
315     // Exclude metadata from equality checks
316     var metadata: JSONObject = JSONObject()
317 
318     companion object {
319         private val KEY_CLOCK_ID = "clockId"
320         private val KEY_SEED_COLOR = "seedColor"
321         private val KEY_METADATA = "metadata"
322 
serializenull323         fun serialize(setting: ClockSettings?): String {
324             if (setting == null) {
325                 return ""
326             }
327 
328             return JSONObject()
329                 .put(KEY_CLOCK_ID, setting.clockId)
330                 .put(KEY_SEED_COLOR, setting.seedColor)
331                 .put(KEY_METADATA, setting.metadata)
332                 .toString()
333         }
334 
deserializenull335         fun deserialize(jsonStr: String?): ClockSettings? {
336             if (jsonStr.isNullOrEmpty()) {
337                 return null
338             }
339 
340             val json = JSONObject(jsonStr)
341             val result =
342                 ClockSettings(
343                     if (!json.isNull(KEY_CLOCK_ID)) json.getString(KEY_CLOCK_ID) else null,
344                     if (!json.isNull(KEY_SEED_COLOR)) json.getInt(KEY_SEED_COLOR) else null
345                 )
346             if (!json.isNull(KEY_METADATA)) {
347                 result.metadata = json.getJSONObject(KEY_METADATA)
348             }
349             return result
350         }
351     }
352 }
353