Home | History | Annotate | Download | only in timer
      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 android.support.annotation.IdRes;
     24 import android.support.v4.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 
     64     public TimerSetupView(Context context) {
     65         this(context, null /* attrs */);
     66     }
     67 
     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
     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 
    135     public void setFabContainer(FabContainer fabContainer) {
    136         mFabContainer = fabContainer;
    137     }
    138 
    139     @Override
    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
    160     public void onClick(View view) {
    161         if (view == mDeleteView) {
    162             delete();
    163         } else {
    164             append(getDigitForId(view.getId()));
    165         }
    166     }
    167 
    168     @Override
    169     public boolean onLongClick(View view) {
    170         if (view == mDeleteView) {
    171             reset();
    172             updateFab();
    173             return true;
    174         }
    175         return false;
    176     }
    177 
    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 
    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 
    222     private void updateDeleteAndDivider() {
    223         final boolean enabled = hasValidInput();
    224         mDeleteView.setEnabled(enabled);
    225         mDividerView.setActivated(enabled);
    226     }
    227 
    228     private void updateFab() {
    229         mFabContainer.updateFab(FAB_SHRINK_AND_EXPAND);
    230     }
    231 
    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 
    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 
    292     public void reset() {
    293         if (mInputPointer != -1) {
    294             Arrays.fill(mInput, 0);
    295             mInputPointer = -1;
    296             updateTime();
    297             updateDeleteAndDivider();
    298         }
    299     }
    300 
    301     public boolean hasValidInput() {
    302         return mInputPointer != -1;
    303     }
    304 
    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      */
    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      */
    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