blob: 99d84c4d0cedf729cec119f78954764946abf797 [file] [log] [blame]
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.statusbar.policy
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.icu.text.DateFormat
import android.icu.text.DisplayContext
import android.icu.util.Calendar
import android.os.Handler
import android.os.HandlerExecutor
import android.os.UserHandle
import android.text.TextUtils
import android.util.Log
import androidx.annotation.VisibleForTesting
import com.android.systemui.Dependency
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.util.ViewController
import com.android.systemui.util.time.SystemClock
import java.text.FieldPosition
import java.text.ParsePosition
import java.util.Date
import java.util.Locale
import javax.inject.Inject
import javax.inject.Named
@VisibleForTesting
internal fun getTextForFormat(date: Date?, format: DateFormat): String {
return if (format === EMPTY_FORMAT) { // Check if same object
""
} else format.format(date)
}
@VisibleForTesting
internal fun getFormatFromPattern(pattern: String?): DateFormat {
if (TextUtils.equals(pattern, "")) {
return EMPTY_FORMAT
}
val l = Locale.getDefault()
val format = DateFormat.getInstanceForSkeleton(pattern, l)
format.setContext(DisplayContext.CAPITALIZATION_FOR_STANDALONE)
return format
}
private val EMPTY_FORMAT: DateFormat = object : DateFormat() {
override fun format(
cal: Calendar,
toAppendTo: StringBuffer,
fieldPosition: FieldPosition
): StringBuffer? {
return null
}
override fun parse(text: String, cal: Calendar, pos: ParsePosition) {}
}
private const val DEBUG = false
private const val TAG = "VariableDateViewController"
class VariableDateViewController(
private val systemClock: SystemClock,
private val broadcastDispatcher: BroadcastDispatcher,
private val timeTickHandler: Handler,
view: VariableDateView
) : ViewController<VariableDateView>(view) {
private var dateFormat: DateFormat? = null
private var datePattern = view.longerPattern
set(value) {
if (field == value) return
field = value
dateFormat = null
if (isAttachedToWindow) {
post(::updateClock)
}
}
private var lastWidth = Integer.MAX_VALUE
private var lastText = ""
private var currentTime = Date()
// View class easy accessors
private val longerPattern: String
get() = mView.longerPattern
private val shorterPattern: String
get() = mView.shorterPattern
private fun post(block: () -> Unit) = mView.handler?.post(block)
private val intentReceiver: BroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
// If the handler is null, it means we received a broadcast while the view has not
// finished being attached or in the process of being detached.
// In that case, do not post anything.
val handler = mView.handler ?: return
val action = intent.action
if (
Intent.ACTION_TIME_TICK == action ||
Intent.ACTION_TIME_CHANGED == action ||
Intent.ACTION_TIMEZONE_CHANGED == action ||
Intent.ACTION_LOCALE_CHANGED == action
) {
if (
Intent.ACTION_LOCALE_CHANGED == action ||
Intent.ACTION_TIMEZONE_CHANGED == action
) {
// need to get a fresh date format
handler.post { dateFormat = null }
}
handler.post(::updateClock)
}
}
}
private val onMeasureListener = object : VariableDateView.OnMeasureListener {
override fun onMeasureAction(availableWidth: Int) {
if (availableWidth != lastWidth) {
// maybeChangeFormat will post if the pattern needs to change.
maybeChangeFormat(availableWidth)
lastWidth = availableWidth
}
}
}
override fun onViewAttached() {
val filter = IntentFilter().apply {
addAction(Intent.ACTION_TIME_TICK)
addAction(Intent.ACTION_TIME_CHANGED)
addAction(Intent.ACTION_TIMEZONE_CHANGED)
addAction(Intent.ACTION_LOCALE_CHANGED)
}
broadcastDispatcher.registerReceiver(intentReceiver, filter,
HandlerExecutor(timeTickHandler), UserHandle.SYSTEM)
post(::updateClock)
mView.onAttach(onMeasureListener)
}
override fun onViewDetached() {
dateFormat = null
mView.onAttach(null)
broadcastDispatcher.unregisterReceiver(intentReceiver)
}
private fun updateClock() {
if (dateFormat == null) {
dateFormat = getFormatFromPattern(datePattern)
}
currentTime.time = systemClock.currentTimeMillis()
val text = getTextForFormat(currentTime, dateFormat!!)
if (text != lastText) {
mView.setText(text)
lastText = text
}
}
private fun maybeChangeFormat(availableWidth: Int) {
if (mView.freezeSwitching ||
availableWidth > lastWidth && datePattern == longerPattern ||
availableWidth < lastWidth && datePattern == ""
) {
// Nothing to do
return
}
if (DEBUG) Log.d(TAG, "Width changed. Maybe changing pattern")
// Start with longer pattern and see what fits
var text = getTextForFormat(currentTime, getFormatFromPattern(longerPattern))
var length = mView.getDesiredWidthForText(text)
if (length <= availableWidth) {
changePattern(longerPattern)
return
}
text = getTextForFormat(currentTime, getFormatFromPattern(shorterPattern))
length = mView.getDesiredWidthForText(text)
if (length <= availableWidth) {
changePattern(shorterPattern)
return
}
changePattern("")
}
private fun changePattern(newPattern: String) {
if (newPattern.equals(datePattern)) return
if (DEBUG) Log.d(TAG, "Changing pattern to $newPattern")
datePattern = newPattern
}
class Factory @Inject constructor(
private val systemClock: SystemClock,
private val broadcastDispatcher: BroadcastDispatcher,
@Named(Dependency.TIME_TICK_HANDLER_NAME) private val handler: Handler
) {
fun create(view: VariableDateView): VariableDateViewController {
return VariableDateViewController(
systemClock,
broadcastDispatcher,
handler,
view
)
}
}
}