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 }