1 /* 2 * Copyright (C) 2020 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.deskclock.timer 18 19 import android.content.Context 20 import android.content.res.ColorStateList 21 import android.graphics.PorterDuff 22 import android.text.BidiFormatter 23 import android.text.TextUtils 24 import android.text.format.DateUtils 25 import android.text.style.RelativeSizeSpan 26 import android.util.AttributeSet 27 import android.view.KeyEvent 28 import android.view.LayoutInflater 29 import android.view.View 30 import android.view.View.OnLongClickListener 31 import android.widget.LinearLayout 32 import android.widget.TextView 33 import androidx.annotation.IdRes 34 import androidx.core.view.ViewCompat 35 36 import com.android.deskclock.FabContainer 37 import com.android.deskclock.FormattedTextUtils 38 import com.android.deskclock.R 39 import com.android.deskclock.ThemeUtils 40 import com.android.deskclock.uidata.UiDataModel 41 42 import java.io.Serializable 43 44 class TimerSetupView @JvmOverloads constructor( 45 context: Context, 46 attrs: AttributeSet? = null 47 ) : LinearLayout(context, attrs), View.OnClickListener, OnLongClickListener { 48 private val mInput = intArrayOf(0, 0, 0, 0, 0, 0) 49 50 private var mInputPointer = -1 51 private val mTimeTemplate: CharSequence 52 53 private lateinit var mTimeView: TextView 54 private lateinit var mDeleteView: View 55 private lateinit var mDividerView: View 56 private lateinit var mDigitViews: Array<TextView> 57 58 /** Updates to the fab are requested via this container. */ 59 private lateinit var mFabContainer: FabContainer 60 61 init { 62 val bf = BidiFormatter.getInstance(false /* rtlContext */) 63 val hoursLabel = bf.unicodeWrap(context.getString(R.string.hours_label)) 64 val minutesLabel = bf.unicodeWrap(context.getString(R.string.minutes_label)) 65 val secondsLabel = bf.unicodeWrap(context.getString(R.string.seconds_label)) 66 67 // Create a formatted template for "00h 00m 00s". 68 mTimeTemplate = TextUtils.expandTemplate("^1^4 ^2^5 ^3^6", 69 bf.unicodeWrap("^1"), 70 bf.unicodeWrap("^2"), 71 bf.unicodeWrap("^3"), 72 FormattedTextUtils.formatText(hoursLabel, RelativeSizeSpan(0.5f)), 73 FormattedTextUtils.formatText(minutesLabel, RelativeSizeSpan(0.5f)), 74 FormattedTextUtils.formatText(secondsLabel, RelativeSizeSpan(0.5f))) 75 76 LayoutInflater.from(context).inflate(R.layout.timer_setup_container, this) 77 } 78 onFinishInflatenull79 override fun onFinishInflate() { 80 super.onFinishInflate() 81 82 mTimeView = findViewById<View>(R.id.timer_setup_time) as TextView 83 mDeleteView = findViewById(R.id.timer_setup_delete) 84 mDividerView = findViewById(R.id.timer_setup_divider) 85 mDigitViews = arrayOf( 86 findViewById<View>(R.id.timer_setup_digit_0) as TextView, 87 findViewById<View>(R.id.timer_setup_digit_1) as TextView, 88 findViewById<View>(R.id.timer_setup_digit_2) as TextView, 89 findViewById<View>(R.id.timer_setup_digit_3) as TextView, 90 findViewById<View>(R.id.timer_setup_digit_4) as TextView, 91 findViewById<View>(R.id.timer_setup_digit_5) as TextView, 92 findViewById<View>(R.id.timer_setup_digit_6) as TextView, 93 findViewById<View>(R.id.timer_setup_digit_7) as TextView, 94 findViewById<View>(R.id.timer_setup_digit_8) as TextView, 95 findViewById<View>(R.id.timer_setup_digit_9) as TextView) 96 97 // Tint the divider to match the disabled control color by default and used the activated 98 // control color when there is valid input. 99 val dividerContext = mDividerView.context 100 val colorControlActivated = ThemeUtils.resolveColor(dividerContext, 101 R.attr.colorControlActivated) 102 val colorControlDisabled = ThemeUtils.resolveColor(dividerContext, 103 R.attr.colorControlNormal, intArrayOf(android.R.attr.state_enabled.inv())) 104 ViewCompat.setBackgroundTintList(mDividerView, 105 ColorStateList( 106 arrayOf(intArrayOf(android.R.attr.state_activated), intArrayOf()), 107 intArrayOf(colorControlActivated, colorControlDisabled))) 108 ViewCompat.setBackgroundTintMode(mDividerView, PorterDuff.Mode.SRC) 109 110 // Initialize the digit buttons. 111 val uidm = UiDataModel.uiDataModel 112 for (digitView in mDigitViews) { 113 val digit = getDigitForId(digitView.id) 114 digitView.text = uidm.getFormattedNumber(digit, 1) 115 digitView.setOnClickListener(this) 116 } 117 118 mDeleteView.setOnClickListener(this) 119 mDeleteView.setOnLongClickListener(this) 120 121 updateTime() 122 updateDeleteAndDivider() 123 } 124 setFabContainernull125 fun setFabContainer(fabContainer: FabContainer) { 126 mFabContainer = fabContainer 127 } 128 onKeyDownnull129 override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { 130 var view: View? = null 131 if (keyCode == KeyEvent.KEYCODE_DEL) { 132 view = mDeleteView 133 } else if (keyCode >= KeyEvent.KEYCODE_0 && keyCode <= KeyEvent.KEYCODE_9) { 134 view = mDigitViews[keyCode - KeyEvent.KEYCODE_0] 135 } 136 137 if (view != null) { 138 val result = view.performClick() 139 if (result && hasValidInput()) { 140 mFabContainer.updateFab(FabContainer.FAB_REQUEST_FOCUS) 141 } 142 return result 143 } 144 145 return false 146 } 147 onClicknull148 override fun onClick(view: View) { 149 if (view === mDeleteView) { 150 delete() 151 } else { 152 append(getDigitForId(view.id)) 153 } 154 } 155 onLongClicknull156 override fun onLongClick(view: View): Boolean { 157 if (view === mDeleteView) { 158 reset() 159 updateFab() 160 return true 161 } 162 return false 163 } 164 getDigitForIdnull165 private fun getDigitForId(@IdRes id: Int): Int = when (id) { 166 R.id.timer_setup_digit_0 -> 0 167 R.id.timer_setup_digit_1 -> 1 168 R.id.timer_setup_digit_2 -> 2 169 R.id.timer_setup_digit_3 -> 3 170 R.id.timer_setup_digit_4 -> 4 171 R.id.timer_setup_digit_5 -> 5 172 R.id.timer_setup_digit_6 -> 6 173 R.id.timer_setup_digit_7 -> 7 174 R.id.timer_setup_digit_8 -> 8 175 R.id.timer_setup_digit_9 -> 9 176 else -> throw IllegalArgumentException("Invalid id: $id") 177 } 178 updateTimenull179 private fun updateTime() { 180 val seconds = mInput[1] * 10 + mInput[0] 181 val minutes = mInput[3] * 10 + mInput[2] 182 val hours = mInput[5] * 10 + mInput[4] 183 184 val uidm = UiDataModel.uiDataModel 185 mTimeView.text = TextUtils.expandTemplate(mTimeTemplate, 186 uidm.getFormattedNumber(hours, 2), 187 uidm.getFormattedNumber(minutes, 2), 188 uidm.getFormattedNumber(seconds, 2)) 189 190 val r = resources 191 mTimeView.contentDescription = r.getString(R.string.timer_setup_description, 192 r.getQuantityString(R.plurals.hours, hours, hours), 193 r.getQuantityString(R.plurals.minutes, minutes, minutes), 194 r.getQuantityString(R.plurals.seconds, seconds, seconds)) 195 } 196 updateDeleteAndDividernull197 private fun updateDeleteAndDivider() { 198 val enabled = hasValidInput() 199 mDeleteView.isEnabled = enabled 200 mDividerView.isActivated = enabled 201 } 202 updateFabnull203 private fun updateFab() { 204 mFabContainer.updateFab(FabContainer.FAB_SHRINK_AND_EXPAND) 205 } 206 appendnull207 private fun append(digit: Int) { 208 require(!(digit < 0 || digit > 9)) { "Invalid digit: $digit" } 209 210 // Pressing "0" as the first digit does nothing. 211 if (mInputPointer == -1 && digit == 0) { 212 return 213 } 214 215 // No space for more digits, so ignore input. 216 if (mInputPointer == mInput.size - 1) { 217 return 218 } 219 220 // Append the new digit. 221 System.arraycopy(mInput, 0, mInput, 1, mInputPointer + 1) 222 mInput[0] = digit 223 mInputPointer++ 224 updateTime() 225 226 // Update TalkBack to read the number being deleted. 227 mDeleteView.contentDescription = context.getString( 228 R.string.timer_descriptive_delete, 229 UiDataModel.uiDataModel.getFormattedNumber(digit)) 230 231 // Update the fab, delete, and divider when we have valid input. 232 if (mInputPointer == 0) { 233 updateFab() 234 updateDeleteAndDivider() 235 } 236 } 237 deletenull238 private fun delete() { 239 // Nothing exists to delete so return. 240 if (mInputPointer < 0) { 241 return 242 } 243 244 System.arraycopy(mInput, 1, mInput, 0, mInputPointer) 245 mInput[mInputPointer] = 0 246 mInputPointer-- 247 updateTime() 248 249 // Update TalkBack to read the number being deleted or its original description. 250 if (mInputPointer >= 0) { 251 mDeleteView.contentDescription = context.getString( 252 R.string.timer_descriptive_delete, 253 UiDataModel.uiDataModel.getFormattedNumber(mInput[0])) 254 } else { 255 mDeleteView.contentDescription = context.getString(R.string.timer_delete) 256 } 257 258 // Update the fab, delete, and divider when we no longer have valid input. 259 if (mInputPointer == -1) { 260 updateFab() 261 updateDeleteAndDivider() 262 } 263 } 264 resetnull265 fun reset() { 266 if (mInputPointer != -1) { 267 mInput.fill(0) 268 mInputPointer = -1 269 updateTime() 270 updateDeleteAndDivider() 271 } 272 } 273 hasValidInputnull274 fun hasValidInput(): Boolean { 275 return mInputPointer != -1 276 } 277 278 val timeInMillis: Long 279 get() { 280 val seconds = mInput[1] * 10 + mInput[0] 281 val minutes = mInput[3] * 10 + mInput[2] 282 val hours = mInput[5] * 10 + mInput[4] 283 return seconds * DateUtils.SECOND_IN_MILLIS + 284 minutes * DateUtils.MINUTE_IN_MILLIS + 285 hours * DateUtils.HOUR_IN_MILLIS 286 } 287 288 var state: Serializable? 289 /** 290 * @return an opaque representation of the state of timer setup 291 */ 292 get() = mInput.copyOf(mInput.size) 293 /** 294 * @param state an opaque state of this view previously produced by [.getState] 295 */ 296 set(state) { 297 val input = state as IntArray? 298 if (input != null && mInput.size == input.size) { 299 for (i in mInput.indices) { 300 mInput[i] = input[i] 301 if (mInput[i] != 0) { 302 mInputPointer = i 303 } 304 } 305 updateTime() 306 updateDeleteAndDivider() 307 } 308 } 309 }