1 /*
2 * Copyright (C) 2021 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package com.android.systemui.statusbar.policy
18
19 import android.content.BroadcastReceiver
20 import android.content.Context
21 import android.content.Intent
22 import android.content.IntentFilter
23 import android.icu.text.DateFormat
24 import android.icu.text.DisplayContext
25 import android.icu.util.Calendar
26 import android.os.Handler
27 import android.os.HandlerExecutor
28 import android.os.UserHandle
29 import android.text.TextUtils
30 import android.util.Log
31 import androidx.annotation.VisibleForTesting
32 import com.android.systemui.Dependency
33 import com.android.systemui.broadcast.BroadcastDispatcher
34 import com.android.systemui.util.ViewController
35 import com.android.systemui.util.time.SystemClock
36 import java.text.FieldPosition
37 import java.text.ParsePosition
38 import java.util.Date
39 import java.util.Locale
40 import javax.inject.Inject
41 import javax.inject.Named
42
43 @VisibleForTesting
getTextForFormatnull44 internal fun getTextForFormat(date: Date?, format: DateFormat): String {
45 return if (format === EMPTY_FORMAT) { // Check if same object
46 ""
47 } else format.format(date)
48 }
49
50 @VisibleForTesting
getFormatFromPatternnull51 internal fun getFormatFromPattern(pattern: String?): DateFormat {
52 if (TextUtils.equals(pattern, "")) {
53 return EMPTY_FORMAT
54 }
55 val l = Locale.getDefault()
56 val format = DateFormat.getInstanceForSkeleton(pattern, l)
57 format.setContext(DisplayContext.CAPITALIZATION_FOR_STANDALONE)
58 return format
59 }
60
61 private val EMPTY_FORMAT: DateFormat = object : DateFormat() {
formatnull62 override fun format(
63 cal: Calendar,
64 toAppendTo: StringBuffer,
65 fieldPosition: FieldPosition
66 ): StringBuffer? {
67 return null
68 }
69
parsenull70 override fun parse(text: String, cal: Calendar, pos: ParsePosition) {}
71 }
72
73 private const val DEBUG = false
74 private const val TAG = "VariableDateViewController"
75
76 class VariableDateViewController(
77 private val systemClock: SystemClock,
78 private val broadcastDispatcher: BroadcastDispatcher,
79 private val timeTickHandler: Handler,
80 view: VariableDateView
81 ) : ViewController<VariableDateView>(view) {
82
83 private var dateFormat: DateFormat? = null
84 private var datePattern = view.longerPattern
85 set(value) {
86 if (field == value) return
87 field = value
88 dateFormat = null
89 if (isAttachedToWindow) {
90 post(::updateClock)
91 }
92 }
93 private var lastWidth = Integer.MAX_VALUE
94 private var lastText = ""
95 private var currentTime = Date()
96
97 // View class easy accessors
98 private val longerPattern: String
99 get() = mView.longerPattern
100 private val shorterPattern: String
101 get() = mView.shorterPattern
postnull102 private fun post(block: () -> Unit) = mView.handler?.post(block)
103
104 private val intentReceiver: BroadcastReceiver = object : BroadcastReceiver() {
105 override fun onReceive(context: Context, intent: Intent) {
106 // If the handler is null, it means we received a broadcast while the view has not
107 // finished being attached or in the process of being detached.
108 // In that case, do not post anything.
109 val handler = mView.handler ?: return
110 val action = intent.action
111 if (
112 Intent.ACTION_TIME_TICK == action ||
113 Intent.ACTION_TIME_CHANGED == action ||
114 Intent.ACTION_TIMEZONE_CHANGED == action ||
115 Intent.ACTION_LOCALE_CHANGED == action
116 ) {
117 if (
118 Intent.ACTION_LOCALE_CHANGED == action ||
119 Intent.ACTION_TIMEZONE_CHANGED == action
120 ) {
121 // need to get a fresh date format
122 handler.post { dateFormat = null }
123 }
124 handler.post(::updateClock)
125 }
126 }
127 }
128
129 private val onMeasureListener = object : VariableDateView.OnMeasureListener {
onMeasureActionnull130 override fun onMeasureAction(availableWidth: Int) {
131 if (availableWidth != lastWidth) {
132 // maybeChangeFormat will post if the pattern needs to change.
133 maybeChangeFormat(availableWidth)
134 lastWidth = availableWidth
135 }
136 }
137 }
138
onViewAttachednull139 override fun onViewAttached() {
140 val filter = IntentFilter().apply {
141 addAction(Intent.ACTION_TIME_TICK)
142 addAction(Intent.ACTION_TIME_CHANGED)
143 addAction(Intent.ACTION_TIMEZONE_CHANGED)
144 addAction(Intent.ACTION_LOCALE_CHANGED)
145 }
146
147 broadcastDispatcher.registerReceiver(intentReceiver, filter,
148 HandlerExecutor(timeTickHandler), UserHandle.SYSTEM)
149
150 post(::updateClock)
151 mView.onAttach(onMeasureListener)
152 }
153
onViewDetachednull154 override fun onViewDetached() {
155 dateFormat = null
156 mView.onAttach(null)
157 broadcastDispatcher.unregisterReceiver(intentReceiver)
158 }
159
updateClocknull160 private fun updateClock() {
161 if (dateFormat == null) {
162 dateFormat = getFormatFromPattern(datePattern)
163 }
164
165 currentTime.time = systemClock.currentTimeMillis()
166
167 val text = getTextForFormat(currentTime, dateFormat!!)
168 if (text != lastText) {
169 mView.setText(text)
170 lastText = text
171 }
172 }
173
maybeChangeFormatnull174 private fun maybeChangeFormat(availableWidth: Int) {
175 if (mView.freezeSwitching ||
176 availableWidth > lastWidth && datePattern == longerPattern ||
177 availableWidth < lastWidth && datePattern == ""
178 ) {
179 // Nothing to do
180 return
181 }
182 if (DEBUG) Log.d(TAG, "Width changed. Maybe changing pattern")
183 // Start with longer pattern and see what fits
184 var text = getTextForFormat(currentTime, getFormatFromPattern(longerPattern))
185 var length = mView.getDesiredWidthForText(text)
186 if (length <= availableWidth) {
187 changePattern(longerPattern)
188 return
189 }
190
191 text = getTextForFormat(currentTime, getFormatFromPattern(shorterPattern))
192 length = mView.getDesiredWidthForText(text)
193 if (length <= availableWidth) {
194 changePattern(shorterPattern)
195 return
196 }
197
198 changePattern("")
199 }
200
changePatternnull201 private fun changePattern(newPattern: String) {
202 if (newPattern.equals(datePattern)) return
203 if (DEBUG) Log.d(TAG, "Changing pattern to $newPattern")
204 datePattern = newPattern
205 }
206
207 class Factory @Inject constructor(
208 private val systemClock: SystemClock,
209 private val broadcastDispatcher: BroadcastDispatcher,
210 @Named(Dependency.TIME_TICK_HANDLER_NAME) private val handler: Handler
211 ) {
createnull212 fun create(view: VariableDateView): VariableDateViewController {
213 return VariableDateViewController(
214 systemClock,
215 broadcastDispatcher,
216 handler,
217 view
218 )
219 }
220 }
221 }