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