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