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