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.shade.ShadeLogger
35 import com.android.systemui.util.ViewController
36 import com.android.systemui.util.time.SystemClock
37 import java.text.FieldPosition
38 import java.text.ParsePosition
39 import java.util.Date
40 import java.util.Locale
41 import javax.inject.Inject
42 import javax.inject.Named
43
44 @VisibleForTesting
getTextForFormatnull45 internal fun getTextForFormat(date: Date?, format: DateFormat): String {
46 return if (format === EMPTY_FORMAT) { // Check if same object
47 ""
48 } else format.format(date)
49 }
50
51 @VisibleForTesting
getFormatFromPatternnull52 internal fun getFormatFromPattern(pattern: String?): DateFormat {
53 if (TextUtils.equals(pattern, "")) {
54 return EMPTY_FORMAT
55 }
56 val l = Locale.getDefault()
57 val format = DateFormat.getInstanceForSkeleton(pattern, l)
58 // The use of CAPITALIZATION_FOR_BEGINNING_OF_SENTENCE instead of
59 // CAPITALIZATION_FOR_STANDALONE is to address
60 // https://unicode-org.atlassian.net/browse/ICU-21631
61 // TODO(b/229287642): Switch back to CAPITALIZATION_FOR_STANDALONE
62 format.setContext(DisplayContext.CAPITALIZATION_FOR_BEGINNING_OF_SENTENCE)
63 return format
64 }
65
66 private val EMPTY_FORMAT: DateFormat = object : DateFormat() {
formatnull67 override fun format(
68 cal: Calendar,
69 toAppendTo: StringBuffer,
70 fieldPosition: FieldPosition
71 ): StringBuffer? {
72 return null
73 }
74
parsenull75 override fun parse(text: String, cal: Calendar, pos: ParsePosition) {}
76 }
77
78 private const val DEBUG = false
79 private const val TAG = "VariableDateViewController"
80
81 class VariableDateViewController(
82 private val systemClock: SystemClock,
83 private val broadcastDispatcher: BroadcastDispatcher,
84 private val shadeLogger: ShadeLogger,
85 private val timeTickHandler: Handler,
86 view: VariableDateView
87 ) : ViewController<VariableDateView>(view) {
88
89 private var dateFormat: DateFormat? = null
90 private var datePattern = view.longerPattern
91 set(value) {
92 if (field == value) return
93 field = value
94 dateFormat = null
95 if (isAttachedToWindow) {
96 post(::updateClock)
97 }
98 }
99 private var lastWidth = Integer.MAX_VALUE
100 private var lastText = ""
101 private var currentTime = Date()
102
103 // View class easy accessors
104 private val longerPattern: String
105 get() = mView.longerPattern
106 private val shorterPattern: String
107 get() = mView.shorterPattern
postnull108 private fun post(block: () -> Unit) = mView.handler?.post(block)
109
110 private val intentReceiver: BroadcastReceiver = object : BroadcastReceiver() {
111 override fun onReceive(context: Context, intent: Intent) {
112 val action = intent.action
113 if (
114 Intent.ACTION_LOCALE_CHANGED == action ||
115 Intent.ACTION_TIMEZONE_CHANGED == action
116 ) {
117 // need to get a fresh date format
118 dateFormat = null
119 shadeLogger.d("VariableDateViewController received intent to refresh date format")
120 }
121
122 val handler = mView.handler
123
124 // If the handler is null, it means we received a broadcast while the view has not
125 // finished being attached or in the process of being detached.
126 // In that case, do not post anything.
127 if (handler == null) {
128 shadeLogger.d("VariableDateViewController received intent but handler was null")
129 } else if (
130 Intent.ACTION_TIME_TICK == action ||
131 Intent.ACTION_TIME_CHANGED == action ||
132 Intent.ACTION_TIMEZONE_CHANGED == action ||
133 Intent.ACTION_LOCALE_CHANGED == action
134 ) {
135 handler.post(::updateClock)
136 }
137 }
138 }
139
140 private val onMeasureListener = object : VariableDateView.OnMeasureListener {
onMeasureActionnull141 override fun onMeasureAction(availableWidth: Int) {
142 if (availableWidth != lastWidth) {
143 // maybeChangeFormat will post if the pattern needs to change.
144 maybeChangeFormat(availableWidth)
145 lastWidth = availableWidth
146 }
147 }
148 }
149
onViewAttachednull150 override fun onViewAttached() {
151 val filter = IntentFilter().apply {
152 addAction(Intent.ACTION_TIME_TICK)
153 addAction(Intent.ACTION_TIME_CHANGED)
154 addAction(Intent.ACTION_TIMEZONE_CHANGED)
155 addAction(Intent.ACTION_LOCALE_CHANGED)
156 }
157
158 broadcastDispatcher.registerReceiver(intentReceiver, filter,
159 HandlerExecutor(timeTickHandler), UserHandle.SYSTEM)
160
161 post(::updateClock)
162 mView.onAttach(onMeasureListener)
163 }
164
onViewDetachednull165 override fun onViewDetached() {
166 dateFormat = null
167 mView.onAttach(null)
168 broadcastDispatcher.unregisterReceiver(intentReceiver)
169 }
170
updateClocknull171 private fun updateClock() {
172 if (dateFormat == null) {
173 dateFormat = getFormatFromPattern(datePattern)
174 }
175
176 currentTime.time = systemClock.currentTimeMillis()
177
178 val text = getTextForFormat(currentTime, dateFormat!!)
179 if (text != lastText) {
180 mView.setText(text)
181 lastText = text
182 }
183 }
184
maybeChangeFormatnull185 private fun maybeChangeFormat(availableWidth: Int) {
186 if (mView.freezeSwitching ||
187 availableWidth > lastWidth && datePattern == longerPattern ||
188 availableWidth < lastWidth && datePattern == ""
189 ) {
190 // Nothing to do
191 return
192 }
193 if (DEBUG) Log.d(TAG, "Width changed. Maybe changing pattern")
194 // Start with longer pattern and see what fits
195 var text = getTextForFormat(currentTime, getFormatFromPattern(longerPattern))
196 var length = mView.getDesiredWidthForText(text)
197 if (length <= availableWidth) {
198 changePattern(longerPattern)
199 return
200 }
201
202 text = getTextForFormat(currentTime, getFormatFromPattern(shorterPattern))
203 length = mView.getDesiredWidthForText(text)
204 if (length <= availableWidth) {
205 changePattern(shorterPattern)
206 return
207 }
208
209 changePattern("")
210 }
211
changePatternnull212 private fun changePattern(newPattern: String) {
213 if (newPattern.equals(datePattern)) return
214 if (DEBUG) Log.d(TAG, "Changing pattern to $newPattern")
215 datePattern = newPattern
216 }
217
218 class Factory @Inject constructor(
219 private val systemClock: SystemClock,
220 private val broadcastDispatcher: BroadcastDispatcher,
221 private val shadeLogger: ShadeLogger,
222 @Named(Dependency.TIME_TICK_HANDLER_NAME) private val handler: Handler
223 ) {
createnull224 fun create(view: VariableDateView): VariableDateViewController {
225 return VariableDateViewController(
226 systemClock,
227 broadcastDispatcher,
228 shadeLogger,
229 handler,
230 view
231 )
232 }
233 }
234 }
235