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 android.view.View.MeasureSpec
32 import androidx.annotation.VisibleForTesting
33 import androidx.lifecycle.Lifecycle
34 import androidx.lifecycle.repeatOnLifecycle
35 import com.android.systemui.Dependency
36 import com.android.systemui.broadcast.BroadcastDispatcher
37 import com.android.systemui.lifecycle.repeatWhenAttached
38 import com.android.systemui.shade.ShadeLogger
39 import com.android.systemui.shade.domain.interactor.ShadeInteractor
40 import com.android.systemui.util.ViewController
41 import com.android.systemui.util.time.SystemClock
42 import java.text.FieldPosition
43 import java.text.ParsePosition
44 import java.util.Date
45 import java.util.Locale
46 import javax.inject.Inject
47 import javax.inject.Named
48
49 @VisibleForTesting
getTextForFormatnull50 internal fun getTextForFormat(date: Date?, format: DateFormat): String {
51 return if (format === EMPTY_FORMAT) { // Check if same object
52 ""
53 } else format.format(date)
54 }
55
56 @VisibleForTesting
getFormatFromPatternnull57 internal fun getFormatFromPattern(pattern: String?): DateFormat {
58 if (TextUtils.equals(pattern, "")) {
59 return EMPTY_FORMAT
60 }
61 val l = Locale.getDefault()
62 val format = DateFormat.getInstanceForSkeleton(pattern, l)
63 // The use of CAPITALIZATION_FOR_BEGINNING_OF_SENTENCE instead of
64 // CAPITALIZATION_FOR_STANDALONE is to address
65 // https://unicode-org.atlassian.net/browse/ICU-21631
66 // TODO(b/229287642): Switch back to CAPITALIZATION_FOR_STANDALONE
67 format.setContext(DisplayContext.CAPITALIZATION_FOR_BEGINNING_OF_SENTENCE)
68 return format
69 }
70
71 private val EMPTY_FORMAT: DateFormat = object : DateFormat() {
formatnull72 override fun format(
73 cal: Calendar,
74 toAppendTo: StringBuffer,
75 fieldPosition: FieldPosition
76 ): StringBuffer? {
77 return null
78 }
79
parsenull80 override fun parse(text: String, cal: Calendar, pos: ParsePosition) {}
81 }
82
83 private const val DEBUG = false
84 private const val TAG = "VariableDateViewController"
85
86 class VariableDateViewController(
87 private val systemClock: SystemClock,
88 private val broadcastDispatcher: BroadcastDispatcher,
89 private val shadeInteractor: ShadeInteractor,
90 private val shadeLogger: ShadeLogger,
91 private val timeTickHandler: Handler,
92 view: VariableDateView
93 ) : ViewController<VariableDateView>(view) {
94
95 private var dateFormat: DateFormat? = null
96 private var datePattern = view.longerPattern
97 set(value) {
98 if (field == value) return
99 field = value
100 dateFormat = null
101 if (isAttachedToWindow) {
102 post(::updateClock)
103 }
104 }
105 private var isQsExpanded = false
106 private var lastWidth = Integer.MAX_VALUE
107 private var lastText = ""
108 private var currentTime = Date()
109
110 // View class easy accessors
111 private val longerPattern: String
112 get() = mView.longerPattern
113 private val shorterPattern: String
114 get() = mView.shorterPattern
postnull115 private fun post(block: () -> Unit) = mView.handler?.post(block)
116
117 private val intentReceiver: BroadcastReceiver = object : BroadcastReceiver() {
118 override fun onReceive(context: Context, intent: Intent) {
119 val action = intent.action
120 if (
121 Intent.ACTION_LOCALE_CHANGED == action ||
122 Intent.ACTION_TIMEZONE_CHANGED == action
123 ) {
124 // need to get a fresh date format
125 dateFormat = null
126 shadeLogger.d("VariableDateViewController received intent to refresh date format")
127 }
128
129 val handler = mView.handler
130
131 // If the handler is null, it means we received a broadcast while the view has not
132 // finished being attached or in the process of being detached.
133 // In that case, do not post anything.
134 if (handler == null) {
135 shadeLogger.d("VariableDateViewController received intent but handler was null")
136 } else if (
137 Intent.ACTION_TIME_TICK == action ||
138 Intent.ACTION_TIME_CHANGED == action ||
139 Intent.ACTION_TIMEZONE_CHANGED == action ||
140 Intent.ACTION_LOCALE_CHANGED == action
141 ) {
142 handler.post(::updateClock)
143 }
144 }
145 }
146
147 private val onMeasureListener = object : VariableDateView.OnMeasureListener {
onMeasureActionnull148 override fun onMeasureAction(availableWidth: Int, widthMeasureSpec: Int) {
149 if (!isQsExpanded && MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.AT_MOST) {
150 // ignore measured width from AT_MOST passes when in QQS (b/289489856)
151 return
152 }
153 if (availableWidth != lastWidth) {
154 // maybeChangeFormat will post if the pattern needs to change.
155 maybeChangeFormat(availableWidth)
156 lastWidth = availableWidth
157 }
158 }
159 }
160
onQsExpansionFractionChangednull161 private fun onQsExpansionFractionChanged(qsExpansionFraction: Float) {
162 val newIsQsExpanded = qsExpansionFraction > 0.5
163 if (newIsQsExpanded != isQsExpanded) {
164 isQsExpanded = newIsQsExpanded
165 // manually trigger a measure pass midway through the transition from QS to QQS
166 post { mView.measure(0, 0) }
167 }
168 }
169
onViewAttachednull170 override fun onViewAttached() {
171 val filter = IntentFilter().apply {
172 addAction(Intent.ACTION_TIME_TICK)
173 addAction(Intent.ACTION_TIME_CHANGED)
174 addAction(Intent.ACTION_TIMEZONE_CHANGED)
175 addAction(Intent.ACTION_LOCALE_CHANGED)
176 }
177
178 broadcastDispatcher.registerReceiver(intentReceiver, filter,
179 HandlerExecutor(timeTickHandler), UserHandle.SYSTEM)
180 mView.repeatWhenAttached {
181 repeatOnLifecycle(Lifecycle.State.STARTED) {
182 shadeInteractor.qsExpansion.collect(::onQsExpansionFractionChanged)
183 }
184 }
185 post(::updateClock)
186 mView.onAttach(onMeasureListener)
187 }
188
onViewDetachednull189 override fun onViewDetached() {
190 dateFormat = null
191 mView.onAttach(null)
192 broadcastDispatcher.unregisterReceiver(intentReceiver)
193 }
194
updateClocknull195 private fun updateClock() {
196 if (dateFormat == null) {
197 dateFormat = getFormatFromPattern(datePattern)
198 }
199
200 currentTime.time = systemClock.currentTimeMillis()
201
202 val text = getTextForFormat(currentTime, dateFormat!!)
203 if (text != lastText) {
204 mView.setText(text)
205 lastText = text
206 }
207 }
208
maybeChangeFormatnull209 private fun maybeChangeFormat(availableWidth: Int) {
210 if (mView.freezeSwitching ||
211 availableWidth > lastWidth && datePattern == longerPattern ||
212 availableWidth < lastWidth && datePattern == ""
213 ) {
214 // Nothing to do
215 return
216 }
217 if (DEBUG) Log.d(TAG, "Width changed. Maybe changing pattern")
218 // Start with longer pattern and see what fits
219 var text = getTextForFormat(currentTime, getFormatFromPattern(longerPattern))
220 var length = mView.getDesiredWidthForText(text)
221 if (length <= availableWidth) {
222 changePattern(longerPattern)
223 return
224 }
225
226 text = getTextForFormat(currentTime, getFormatFromPattern(shorterPattern))
227 length = mView.getDesiredWidthForText(text)
228 if (length <= availableWidth) {
229 changePattern(shorterPattern)
230 return
231 }
232
233 changePattern("")
234 }
235
changePatternnull236 private fun changePattern(newPattern: String) {
237 if (newPattern.equals(datePattern)) return
238 if (DEBUG) Log.d(TAG, "Changing pattern to $newPattern")
239 datePattern = newPattern
240 }
241
242 class Factory @Inject constructor(
243 private val systemClock: SystemClock,
244 private val broadcastDispatcher: BroadcastDispatcher,
245 private val shadeInteractor: ShadeInteractor,
246 private val shadeLogger: ShadeLogger,
247 @Named(Dependency.TIME_TICK_HANDLER_NAME) private val handler: Handler
248 ) {
createnull249 fun create(view: VariableDateView): VariableDateViewController {
250 return VariableDateViewController(
251 systemClock,
252 broadcastDispatcher,
253 shadeInteractor,
254 shadeLogger,
255 handler,
256 view
257 )
258 }
259 }
260 }
261