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
44 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
51 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() {
62     override fun format(
63         cal: Calendar,
64         toAppendTo: StringBuffer,
65         fieldPosition: FieldPosition
66     ): StringBuffer? {
67         return null
68     }
69 
70     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
102     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 {
130         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 
139     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 
154     override fun onViewDetached() {
155         dateFormat = null
156         mView.onAttach(null)
157         broadcastDispatcher.unregisterReceiver(intentReceiver)
158     }
159 
160     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 
174     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 
201     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     ) {
212         fun create(view: VariableDateView): VariableDateViewController {
213             return VariableDateViewController(
214                     systemClock,
215                     broadcastDispatcher,
216                     handler,
217                     view
218             )
219         }
220     }
221 }