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