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 }