• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 }