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
45 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
52 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() {
67     override fun format(
68         cal: Calendar,
69         toAppendTo: StringBuffer,
70         fieldPosition: FieldPosition
71     ): StringBuffer? {
72         return null
73     }
74 
75     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
108     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 {
141         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 
150     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 
165     override fun onViewDetached() {
166         dateFormat = null
167         mView.onAttach(null)
168         broadcastDispatcher.unregisterReceiver(intentReceiver)
169     }
170 
171     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 
185     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 
212     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     ) {
224         fun create(view: VariableDateView): VariableDateViewController {
225             return VariableDateViewController(
226                     systemClock,
227                     broadcastDispatcher,
228                     shadeLogger,
229                     handler,
230                     view
231             )
232         }
233     }
234 }
235