Home | History | Annotate | Download | only in stopwatch
      1 /*
      2  * Copyright (C) 2015 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.stopwatch;
     18 
     19 import android.content.Context;
     20 import android.support.annotation.VisibleForTesting;
     21 import android.support.v7.widget.RecyclerView;
     22 import android.text.format.DateUtils;
     23 import android.view.LayoutInflater;
     24 import android.view.View;
     25 import android.view.ViewGroup;
     26 import android.widget.TextView;
     27 
     28 import com.android.deskclock.R;
     29 import com.android.deskclock.data.DataModel;
     30 import com.android.deskclock.data.Lap;
     31 import com.android.deskclock.data.Stopwatch;
     32 import com.android.deskclock.uidata.UiDataModel;
     33 
     34 import java.text.DecimalFormatSymbols;
     35 import java.util.List;
     36 
     37 /**
     38  * Displays a list of lap times in reverse order. That is, the newest lap is at the top, the oldest
     39  * lap is at the bottom.
     40  */
     41 class LapsAdapter extends RecyclerView.Adapter<LapsAdapter.LapItemHolder> {
     42 
     43     private static final long TEN_MINUTES = 10 * DateUtils.MINUTE_IN_MILLIS;
     44     private static final long HOUR = DateUtils.HOUR_IN_MILLIS;
     45     private static final long TEN_HOURS = 10 * HOUR;
     46     private static final long HUNDRED_HOURS = 100 * HOUR;
     47 
     48     /** A single space preceded by a zero-width LRM; This groups adjacent chars left-to-right. */
     49     private static final String LRM_SPACE = "\u200E ";
     50 
     51     /** Reusable StringBuilder that assembles a formatted time; alleviates memory churn. */
     52     private static final StringBuilder sTimeBuilder = new StringBuilder(12);
     53 
     54     private final LayoutInflater mInflater;
     55     private final Context mContext;
     56 
     57     /** Used to determine when the time format for the lap time column has changed length. */
     58     private int mLastFormattedLapTimeLength;
     59 
     60     /** Used to determine when the time format for the total time column has changed length. */
     61     private int mLastFormattedAccumulatedTimeLength;
     62 
     63     LapsAdapter(Context context) {
     64         mContext = context;
     65         mInflater = LayoutInflater.from(context);
     66         setHasStableIds(true);
     67     }
     68 
     69     /**
     70      * After recording the first lap, there is always a "current lap" in progress.
     71      *
     72      * @return 0 if no laps are yet recorded; lap count + 1 if any laps exist
     73      */
     74     @Override
     75     public int getItemCount() {
     76         final int lapCount = getLaps().size();
     77         final int currentLapCount = lapCount == 0 ? 0 : 1;
     78         return currentLapCount + lapCount;
     79     }
     80 
     81     @Override
     82     public LapItemHolder onCreateViewHolder(ViewGroup parent, int viewType) {
     83         final View v = mInflater.inflate(R.layout.lap_view, parent, false /* attachToRoot */);
     84         return new LapItemHolder(v);
     85     }
     86 
     87     @Override
     88     public void onBindViewHolder(LapItemHolder viewHolder, int position) {
     89         final long lapTime;
     90         final int lapNumber;
     91         final long totalTime;
     92 
     93         // Lap will be null for the current lap.
     94         final Lap lap = position == 0 ? null : getLaps().get(position - 1);
     95         if (lap != null) {
     96             // For a recorded lap, merely extract the values to format.
     97             lapTime = lap.getLapTime();
     98             lapNumber = lap.getLapNumber();
     99             totalTime = lap.getAccumulatedTime();
    100         } else {
    101             // For the current lap, compute times relative to the stopwatch.
    102             totalTime = getStopwatch().getTotalTime();
    103             lapTime = DataModel.getDataModel().getCurrentLapTime(totalTime);
    104             lapNumber = getLaps().size() + 1;
    105         }
    106 
    107         // Bind data into the child views.
    108         viewHolder.lapTime.setText(formatLapTime(lapTime, true));
    109         viewHolder.accumulatedTime.setText(formatAccumulatedTime(totalTime, true));
    110         viewHolder.lapNumber.setText(formatLapNumber(getLaps().size() + 1, lapNumber));
    111     }
    112 
    113     @Override
    114     public long getItemId(int position) {
    115         final List<Lap> laps = getLaps();
    116         if (position == 0) {
    117             return laps.size() + 1;
    118         }
    119 
    120         return laps.get(position - 1).getLapNumber();
    121     }
    122 
    123     /**
    124      * @param rv the RecyclerView that contains the {@code childView}
    125      * @param totalTime time accumulated for the current lap and all prior laps
    126      */
    127     void updateCurrentLap(RecyclerView rv, long totalTime) {
    128         // If no laps exist there is nothing to do.
    129         if (getItemCount() == 0) {
    130             return;
    131         }
    132 
    133         final View currentLapView = rv.getChildAt(0);
    134         if (currentLapView != null) {
    135             // Compute the lap time using the total time.
    136             final long lapTime = DataModel.getDataModel().getCurrentLapTime(totalTime);
    137 
    138             final LapItemHolder holder = (LapItemHolder) rv.getChildViewHolder(currentLapView);
    139             holder.lapTime.setText(formatLapTime(lapTime, false));
    140             holder.accumulatedTime.setText(formatAccumulatedTime(totalTime, false));
    141         }
    142     }
    143 
    144     /**
    145      * Record a new lap and update this adapter to include it.
    146      *
    147      * @return a newly cleared lap
    148      */
    149     Lap addLap() {
    150         final Lap lap = DataModel.getDataModel().addLap();
    151 
    152         if (getItemCount() == 10) {
    153             // 10 total laps indicates all items switch from 1 to 2 digit lap numbers.
    154             notifyDataSetChanged();
    155         } else {
    156             // New current lap now exists.
    157             notifyItemInserted(0);
    158 
    159             // Prior current lap must be refreshed once with the true values in place.
    160             notifyItemChanged(1);
    161         }
    162 
    163         return lap;
    164     }
    165 
    166     /**
    167      * Remove all recorded laps and update this adapter.
    168      */
    169     void clearLaps() {
    170         // Clear the computed time lengths related to the old recorded laps.
    171         mLastFormattedLapTimeLength = 0;
    172         mLastFormattedAccumulatedTimeLength = 0;
    173 
    174         notifyDataSetChanged();
    175     }
    176 
    177     /**
    178      * @return a formatted textual description of lap times and total time
    179      */
    180     String getShareText() {
    181         final Stopwatch stopwatch = getStopwatch();
    182         final long totalTime = stopwatch.getTotalTime();
    183         final String stopwatchTime = formatTime(totalTime, totalTime, ":");
    184 
    185         // Choose a size for the builder that is unlikely to be resized.
    186         final StringBuilder builder = new StringBuilder(1000);
    187 
    188         // Add the total elapsed time of the stopwatch.
    189         builder.append(mContext.getString(R.string.sw_share_main, stopwatchTime));
    190         builder.append("\n");
    191 
    192         final List<Lap> laps = getLaps();
    193         if (!laps.isEmpty()) {
    194             // Add a header for lap times.
    195             builder.append(mContext.getString(R.string.sw_share_laps));
    196             builder.append("\n");
    197 
    198             // Loop through the laps in the order they were recorded; reverse of display order.
    199             final String separator = DecimalFormatSymbols.getInstance().getDecimalSeparator() + " ";
    200             for (int i = laps.size() - 1; i >= 0; i--) {
    201                 final Lap lap = laps.get(i);
    202                 builder.append(lap.getLapNumber());
    203                 builder.append(separator);
    204                 final long lapTime = lap.getLapTime();
    205                 builder.append(formatTime(lapTime, lapTime, " "));
    206                 builder.append("\n");
    207             }
    208 
    209             // Append the final lap
    210             builder.append(laps.size() + 1);
    211             builder.append(separator);
    212             final long lapTime = DataModel.getDataModel().getCurrentLapTime(totalTime);
    213             builder.append(formatTime(lapTime, lapTime, " "));
    214             builder.append("\n");
    215         }
    216 
    217         return builder.toString();
    218     }
    219 
    220     /**
    221      * @param lapCount the total number of recorded laps
    222      * @param lapNumber the number of the lap being formatted
    223      * @return e.g. "# 7" if {@code lapCount} less than 10; "# 07" if {@code lapCount} is 10 or more
    224      */
    225     @VisibleForTesting
    226     String formatLapNumber(int lapCount, int lapNumber) {
    227         if (lapCount < 10) {
    228             return mContext.getString(R.string.lap_number_single_digit, lapNumber);
    229         } else {
    230             return mContext.getString(R.string.lap_number_double_digit, lapNumber);
    231         }
    232     }
    233 
    234     /**
    235      * @param maxTime the maximum amount of time; used to choose a time format
    236      * @param time the time to format guaranteed not to exceed {@code maxTime}
    237      * @param separator displayed between hours and minutes as well as minutes and seconds
    238      * @return a formatted version of the time
    239      */
    240     @VisibleForTesting
    241     static String formatTime(long maxTime, long time, String separator) {
    242         final int hours, minutes, seconds, hundredths;
    243         if (time <= 0) {
    244             // A negative time should be impossible, but is tolerated to avoid crashing the app.
    245             hours = minutes = seconds = hundredths = 0;
    246         } else {
    247             hours = (int) (time / DateUtils.HOUR_IN_MILLIS);
    248             int remainder = (int) (time % DateUtils.HOUR_IN_MILLIS);
    249 
    250             minutes = (int) (remainder / DateUtils.MINUTE_IN_MILLIS);
    251             remainder = (int) (remainder % DateUtils.MINUTE_IN_MILLIS);
    252 
    253             seconds = (int) (remainder / DateUtils.SECOND_IN_MILLIS);
    254             remainder = (int) (remainder % DateUtils.SECOND_IN_MILLIS);
    255 
    256             hundredths = remainder / 10;
    257         }
    258 
    259         final char decimalSeparator = DecimalFormatSymbols.getInstance().getDecimalSeparator();
    260 
    261         sTimeBuilder.setLength(0);
    262 
    263         // The display of hours and minutes varies based on maxTime.
    264         if (maxTime < TEN_MINUTES) {
    265             sTimeBuilder.append(UiDataModel.getUiDataModel().getFormattedNumber(minutes, 1));
    266         } else if (maxTime < HOUR) {
    267             sTimeBuilder.append(UiDataModel.getUiDataModel().getFormattedNumber(minutes, 2));
    268         } else if (maxTime < TEN_HOURS) {
    269             sTimeBuilder.append(UiDataModel.getUiDataModel().getFormattedNumber(hours, 1));
    270             sTimeBuilder.append(separator);
    271             sTimeBuilder.append(UiDataModel.getUiDataModel().getFormattedNumber(minutes, 2));
    272         } else if (maxTime < HUNDRED_HOURS) {
    273             sTimeBuilder.append(UiDataModel.getUiDataModel().getFormattedNumber(hours, 2));
    274             sTimeBuilder.append(separator);
    275             sTimeBuilder.append(UiDataModel.getUiDataModel().getFormattedNumber(minutes, 2));
    276         } else {
    277             sTimeBuilder.append(UiDataModel.getUiDataModel().getFormattedNumber(hours, 3));
    278             sTimeBuilder.append(separator);
    279             sTimeBuilder.append(UiDataModel.getUiDataModel().getFormattedNumber(minutes, 2));
    280         }
    281 
    282         // The display of seconds and hundredths-of-a-second is constant.
    283         sTimeBuilder.append(separator);
    284         sTimeBuilder.append(UiDataModel.getUiDataModel().getFormattedNumber(seconds, 2));
    285         sTimeBuilder.append(decimalSeparator);
    286         sTimeBuilder.append(UiDataModel.getUiDataModel().getFormattedNumber(hundredths, 2));
    287 
    288         return sTimeBuilder.toString();
    289     }
    290 
    291     /**
    292      * @param lapTime the lap time to be formatted
    293      * @param isBinding if the lap time is requested so it can be bound avoid notifying of data
    294      *                  set changes; they are not allowed to occur during bind
    295      * @return a formatted version of the lap time
    296      */
    297     private String formatLapTime(long lapTime, boolean isBinding) {
    298         // The longest lap dictates the way the given lapTime must be formatted.
    299         final long longestLapTime = Math.max(DataModel.getDataModel().getLongestLapTime(), lapTime);
    300         final String formattedTime = formatTime(longestLapTime, lapTime, LRM_SPACE);
    301 
    302         // If the newly formatted lap time has altered the format, refresh all laps.
    303         final int newLength = formattedTime.length();
    304         if (!isBinding && mLastFormattedLapTimeLength != newLength) {
    305             mLastFormattedLapTimeLength = newLength;
    306             notifyDataSetChanged();
    307         }
    308 
    309         return formattedTime;
    310     }
    311 
    312     /**
    313      * @param accumulatedTime the accumulated time to be formatted
    314      * @param isBinding if the lap time is requested so it can be bound avoid notifying of data
    315      *                  set changes; they are not allowed to occur during bind
    316      * @return a formatted version of the accumulated time
    317      */
    318     private String formatAccumulatedTime(long accumulatedTime, boolean isBinding) {
    319         final long totalTime = getStopwatch().getTotalTime();
    320         final long longestAccumulatedTime = Math.max(totalTime, accumulatedTime);
    321         final String formattedTime = formatTime(longestAccumulatedTime, accumulatedTime, LRM_SPACE);
    322 
    323         // If the newly formatted accumulated time has altered the format, refresh all laps.
    324         final int newLength = formattedTime.length();
    325         if (!isBinding && mLastFormattedAccumulatedTimeLength != newLength) {
    326             mLastFormattedAccumulatedTimeLength = newLength;
    327             notifyDataSetChanged();
    328         }
    329 
    330         return formattedTime;
    331     }
    332 
    333     private Stopwatch getStopwatch() {
    334         return DataModel.getDataModel().getStopwatch();
    335     }
    336 
    337     private List<Lap> getLaps() {
    338         return DataModel.getDataModel().getLaps();
    339     }
    340 
    341     /**
    342      * Cache the child views of each lap item view.
    343      */
    344     static final class LapItemHolder extends RecyclerView.ViewHolder {
    345 
    346         private final TextView lapNumber;
    347         private final TextView lapTime;
    348         private final TextView accumulatedTime;
    349 
    350         LapItemHolder(View itemView) {
    351             super(itemView);
    352 
    353             lapTime = (TextView) itemView.findViewById(R.id.lap_time);
    354             lapNumber = (TextView) itemView.findViewById(R.id.lap_number);
    355             accumulatedTime = (TextView) itemView.findViewById(R.id.lap_total);
    356         }
    357     }
    358 }