package com.embeddedlog.LightUpDroid.stopwatch; import android.animation.LayoutTransition; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.SharedPreferences.OnSharedPreferenceChangeListener; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.res.Configuration; import android.graphics.drawable.Drawable; import android.os.Build; import android.os.Bundle; import android.os.PowerManager; import android.os.PowerManager.WakeLock; import android.preference.PreferenceManager; import android.text.format.DateUtils; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.view.animation.Animation; import android.view.animation.TranslateAnimation; import android.widget.AdapterView; import android.widget.AdapterView.OnItemClickListener; import android.widget.ArrayAdapter; import android.widget.BaseAdapter; import android.widget.ImageButton; import android.widget.ListPopupWindow; import android.widget.ListView; import android.widget.PopupWindow.OnDismissListener; import android.widget.TextView; import com.embeddedlog.LightUpDroid.CircleButtonsLayout; import com.embeddedlog.LightUpDroid.CircleTimerView; import com.embeddedlog.LightUpDroid.DeskClock; import com.embeddedlog.LightUpDroid.DeskClockFragment; import com.embeddedlog.LightUpDroid.Log; import com.embeddedlog.LightUpDroid.R; import com.embeddedlog.LightUpDroid.Utils; import com.embeddedlog.LightUpDroid.timer.CountingTimerView; import java.util.ArrayList; import java.util.List; public class StopwatchFragment extends DeskClockFragment implements OnSharedPreferenceChangeListener { private static final boolean DEBUG = false; private static final String TAG = "StopwatchFragment"; int mState = Stopwatches.STOPWATCH_RESET; // Stopwatch views that are accessed by the activity private ImageButton mLeftButton; private TextView mCenterButton; private CircleTimerView mTime; private CountingTimerView mTimeText; private ListView mLapsList; private ImageButton mShareButton; private ListPopupWindow mSharePopup; private WakeLock mWakeLock; private CircleButtonsLayout mCircleLayout; // Animation constants and objects private LayoutTransition mLayoutTransition; private LayoutTransition mCircleLayoutTransition; private View mStartSpace; private View mEndSpace; private boolean mSpacersUsed; // Used for calculating the time from the start taking into account the pause times long mStartTime = 0; long mAccumulatedTime = 0; // Lap information class Lap { Lap (long time, long total) { mLapTime = time; mTotalTime = total; } public long mLapTime; public long mTotalTime; public void updateView() { View lapInfo = mLapsList.findViewWithTag(this); if (lapInfo != null) { mLapsAdapter.setTimeText(lapInfo, this); } } } // Adapter for the ListView that shows the lap times. class LapsListAdapter extends BaseAdapter { ArrayList<Lap> mLaps = new ArrayList<Lap>(); private final LayoutInflater mInflater; private final int mBackgroundColor; private final String[] mFormats; private final String[] mLapFormatSet; // Size of this array must match the size of formats private final long[] mThresholds = { 10 * DateUtils.MINUTE_IN_MILLIS, // < 10 minutes DateUtils.HOUR_IN_MILLIS, // < 1 hour 10 * DateUtils.HOUR_IN_MILLIS, // < 10 hours 100 * DateUtils.HOUR_IN_MILLIS, // < 100 hours 1000 * DateUtils.HOUR_IN_MILLIS // < 1000 hours }; private int mLapIndex = 0; private int mTotalIndex = 0; private String mLapFormat; public LapsListAdapter(Context context) { mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); mBackgroundColor = getResources().getColor(R.color.blackish); mFormats = context.getResources().getStringArray(R.array.stopwatch_format_set); mLapFormatSet = context.getResources().getStringArray(R.array.sw_lap_number_set); updateLapFormat(); } @Override public long getItemId(int position) { return position; } @Override public View getView(int position, View convertView, ViewGroup parent) { if (mLaps.size() == 0 || position >= mLaps.size()) { return null; } Lap lap = getItem(position); View lapInfo; if (convertView != null) { lapInfo = convertView; } else { lapInfo = mInflater.inflate(R.layout.lap_view, parent, false); lapInfo.setBackgroundColor(mBackgroundColor); } lapInfo.setTag(lap); TextView count = (TextView)lapInfo.findViewById(R.id.lap_number); count.setText(String.format(mLapFormat, mLaps.size() - position).toUpperCase()); setTimeText(lapInfo, lap); return lapInfo; } protected void setTimeText(View lapInfo, Lap lap) { TextView lapTime = (TextView)lapInfo.findViewById(R.id.lap_time); TextView totalTime = (TextView)lapInfo.findViewById(R.id.lap_total); lapTime.setText(Stopwatches.formatTimeText(lap.mLapTime, mFormats[mLapIndex])); totalTime.setText(Stopwatches.formatTimeText(lap.mTotalTime, mFormats[mTotalIndex])); } @Override public int getCount() { return mLaps.size(); } @Override public Lap getItem(int position) { if (mLaps.size() == 0 || position >= mLaps.size()) { return null; } return mLaps.get(position); } private void updateLapFormat() { // Note Stopwatches.MAX_LAPS < 100 mLapFormat = mLapFormatSet[mLaps.size() < 10 ? 0 : 1]; } private void resetTimeFormats() { mLapIndex = mTotalIndex = 0; } /** * A lap is printed into two columns: the total time and the lap time. To make this print * as pretty as possible, multiple formats were created which minimize the width of the * print. As the total or lap time exceed the limit of that format, this code updates * the format used for the total and/or lap times. * * @param lap to measure * @return true if this lap exceeded either threshold and a format was updated. */ public boolean updateTimeFormats(Lap lap) { boolean formatChanged = false; while (mLapIndex + 1 < mThresholds.length && lap.mLapTime >= mThresholds[mLapIndex]) { mLapIndex++; formatChanged = true; } while (mTotalIndex + 1 < mThresholds.length && lap.mTotalTime >= mThresholds[mTotalIndex]) { mTotalIndex++; formatChanged = true; } return formatChanged; } public void addLap(Lap l) { mLaps.add(0, l); // for efficiency caller also calls notifyDataSetChanged() } public void clearLaps() { mLaps.clear(); updateLapFormat(); resetTimeFormats(); notifyDataSetChanged(); } // Helper function used to get the lap data to be stored in the activity's bundle public long [] getLapTimes() { int size = mLaps.size(); if (size == 0) { return null; } long [] laps = new long[size]; for (int i = 0; i < size; i ++) { laps[i] = mLaps.get(i).mTotalTime; } return laps; } // Helper function to restore adapter's data from the activity's bundle public void setLapTimes(long [] laps) { if (laps == null || laps.length == 0) { return; } int size = laps.length; mLaps.clear(); for (long lap : laps) { mLaps.add(new Lap(lap, 0)); } long totalTime = 0; for (int i = size -1; i >= 0; i --) { totalTime += laps[i]; mLaps.get(i).mTotalTime = totalTime; updateTimeFormats(mLaps.get(i)); } updateLapFormat(); showLaps(); notifyDataSetChanged(); } } LapsListAdapter mLapsAdapter; public StopwatchFragment() { } private void rightButtonAction() { long time = Utils.getTimeNow(); Context context = getActivity().getApplicationContext(); Intent intent = new Intent(context, StopwatchService.class); intent.putExtra(Stopwatches.MESSAGE_TIME, time); intent.putExtra(Stopwatches.SHOW_NOTIF, false); switch (mState) { case Stopwatches.STOPWATCH_RUNNING: // do stop long curTime = Utils.getTimeNow(); mAccumulatedTime += (curTime - mStartTime); doStop(); intent.setAction(Stopwatches.STOP_STOPWATCH); context.startService(intent); releaseWakeLock(); break; case Stopwatches.STOPWATCH_RESET: case Stopwatches.STOPWATCH_STOPPED: // do start doStart(time); intent.setAction(Stopwatches.START_STOPWATCH); context.startService(intent); acquireWakeLock(); break; default: Log.wtf("Illegal state " + mState + " while pressing the right stopwatch button"); break; } } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // Inflate the layout for this fragment ViewGroup v = (ViewGroup)inflater.inflate(R.layout.stopwatch_fragment, container, false); mLeftButton = (ImageButton)v.findViewById(R.id.stopwatch_left_button); mLeftButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { long time = Utils.getTimeNow(); Context context = getActivity().getApplicationContext(); Intent intent = new Intent(context, StopwatchService.class); intent.putExtra(Stopwatches.MESSAGE_TIME, time); intent.putExtra(Stopwatches.SHOW_NOTIF, false); switch (mState) { case Stopwatches.STOPWATCH_RUNNING: // Save lap time addLapTime(time); doLap(); intent.setAction(Stopwatches.LAP_STOPWATCH); context.startService(intent); break; case Stopwatches.STOPWATCH_STOPPED: // do reset doReset(); intent.setAction(Stopwatches.RESET_STOPWATCH); context.startService(intent); releaseWakeLock(); break; default: // Happens in monkey tests Log.i("Illegal state " + mState + " while pressing the left stopwatch button"); break; } } }); mCenterButton = (TextView)v.findViewById(R.id.stopwatch_stop); mShareButton = (ImageButton)v.findViewById(R.id.stopwatch_share_button); mShareButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { showSharePopup(); } }); mTime = (CircleTimerView)v.findViewById(R.id.stopwatch_time); mTimeText = (CountingTimerView)v.findViewById(R.id.stopwatch_time_text); mLapsList = (ListView)v.findViewById(R.id.laps_list); mLapsList.setDividerHeight(0); mLapsAdapter = new LapsListAdapter(getActivity()); mLapsList.setAdapter(mLapsAdapter); // Timer text serves as a virtual start/stop button. mTimeText.registerVirtualButtonAction(new Runnable() { @Override public void run() { rightButtonAction(); } }); mTimeText.registerStopTextView(mCenterButton); mTimeText.setVirtualButtonEnabled(true); mCircleLayout = (CircleButtonsLayout)v.findViewById(R.id.stopwatch_circle); mCircleLayout.setCircleTimerViewIds(R.id.stopwatch_time, R.id.stopwatch_left_button, R.id.stopwatch_share_button, R.id.stopwatch_stop, R.dimen.plusone_reset_button_padding, R.dimen.share_button_padding, 0, 0); /** No label for a stopwatch**/ // Animation setup mLayoutTransition = new LayoutTransition(); mCircleLayoutTransition = new LayoutTransition(); // The CircleButtonsLayout only needs to undertake location changes if (Build.VERSION.SDK_INT >= 16) { mCircleLayoutTransition.enableTransitionType(LayoutTransition.CHANGING); mCircleLayoutTransition.disableTransitionType(LayoutTransition.APPEARING); mCircleLayoutTransition.disableTransitionType(LayoutTransition.DISAPPEARING); mCircleLayoutTransition.disableTransitionType(LayoutTransition.CHANGE_APPEARING); mCircleLayoutTransition.disableTransitionType(LayoutTransition.CHANGE_DISAPPEARING); } mCircleLayoutTransition.setAnimateParentHierarchy(false); // These spacers assist in keeping the size of CircleButtonsLayout constant mStartSpace = v.findViewById(R.id.start_space); mEndSpace = v.findViewById(R.id.end_space); mSpacersUsed = mStartSpace != null || mEndSpace != null; // Listener to invoke extra animation within the laps-list mLayoutTransition.addTransitionListener(new LayoutTransition.TransitionListener() { @Override public void startTransition(LayoutTransition transition, ViewGroup container, View view, int transitionType) { if (view == mLapsList) { if (transitionType == LayoutTransition.DISAPPEARING) { if (DEBUG) Log.v("StopwatchFragment.start laps-list disappearing"); boolean shiftX = view.getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE; int first = mLapsList.getFirstVisiblePosition(); int last = mLapsList.getLastVisiblePosition(); // Ensure index range will not cause a divide by zero if (last < first) { last = first; } long duration = transition.getDuration(LayoutTransition.DISAPPEARING); long offset = duration / (last - first + 1) / 5; for (int visibleIndex = first; visibleIndex <= last; visibleIndex++) { View lapView = mLapsList.getChildAt(visibleIndex - first); if (lapView != null) { float toXValue = shiftX ? 1.0f * (visibleIndex - first + 1) : 0; float toYValue = shiftX ? 0 : 4.0f * (visibleIndex - first + 1); TranslateAnimation animation = new TranslateAnimation( Animation.RELATIVE_TO_SELF, 0, Animation.RELATIVE_TO_SELF, toXValue, Animation.RELATIVE_TO_SELF, 0, Animation.RELATIVE_TO_SELF, toYValue); animation.setStartOffset((last - visibleIndex) * offset); animation.setDuration(duration); lapView.startAnimation(animation); } } } } } @Override public void endTransition(LayoutTransition transition, ViewGroup container, View view, int transitionType) { if (transitionType == LayoutTransition.DISAPPEARING) { if (DEBUG) Log.v("StopwatchFragment.end laps-list disappearing"); int last = mLapsList.getLastVisiblePosition(); for (int visibleIndex = mLapsList.getFirstVisiblePosition(); visibleIndex <= last; visibleIndex++) { View lapView = mLapsList.getChildAt(visibleIndex); if (lapView != null) { Animation animation = lapView.getAnimation(); if (animation != null) { animation.cancel(); } } } } } }); return v; } /** * Make the final display setup. * * If the fragment is starting with an existing list of laps, shows the laps list and if the * spacers around the clock exist, hide them. If there are not laps at the start, hide the laps * list and show the clock spacers if they exist. */ @Override public void onStart() { super.onStart(); boolean lapsVisible = mLapsAdapter.getCount() > 0; mLapsList.setVisibility(lapsVisible ? View.VISIBLE : View.GONE); if (mSpacersUsed) { int spacersVisibility = lapsVisible ? View.GONE : View.VISIBLE; if (mStartSpace != null) { mStartSpace.setVisibility(spacersVisibility); } if (mEndSpace != null) { mEndSpace.setVisibility(spacersVisibility); } } ((ViewGroup)getView()).setLayoutTransition(mLayoutTransition); mCircleLayout.setLayoutTransition(mCircleLayoutTransition); } @Override public void onResume() { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getActivity()); prefs.registerOnSharedPreferenceChangeListener(this); readFromSharedPref(prefs); mTime.readFromSharedPref(prefs, "sw"); mTime.postInvalidate(); setButtons(mState); mTimeText.setTime(mAccumulatedTime, true, true); if (mState == Stopwatches.STOPWATCH_RUNNING) { acquireWakeLock(); startUpdateThread(); } else if (mState == Stopwatches.STOPWATCH_STOPPED && mAccumulatedTime != 0) { mTimeText.blinkTimeStr(true); } showLaps(); ((DeskClock)getActivity()).registerPageChangedListener(this); // View was hidden in onPause, make sure it is visible now. View v = getView(); if (v != null) { v.setVisibility(View.VISIBLE); } super.onResume(); } @Override public void onPause() { // This is called because the lock screen was activated, the window stay // active under it and when we unlock the screen, we see the old time for // a fraction of a second. View v = getView(); if (v != null) { v.setVisibility(View.INVISIBLE); } if (mState == Stopwatches.STOPWATCH_RUNNING) { stopUpdateThread(); } // The stopwatch must keep running even if the user closes the app so save stopwatch state // in shared prefs SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getActivity()); prefs.unregisterOnSharedPreferenceChangeListener(this); writeToSharedPref(prefs); mTime.writeToSharedPref(prefs, "sw"); mTimeText.blinkTimeStr(false); if (mSharePopup != null) { mSharePopup.dismiss(); mSharePopup = null; } ((DeskClock)getActivity()).unregisterPageChangedListener(this); releaseWakeLock(); super.onPause(); } @Override public void onPageChanged(int page) { if (page == DeskClock.STOPWATCH_TAB_INDEX && mState == Stopwatches.STOPWATCH_RUNNING) { acquireWakeLock(); } else { releaseWakeLock(); } } private void doStop() { if (DEBUG) Log.v("StopwatchFragment.doStop"); stopUpdateThread(); mTime.pauseIntervalAnimation(); mTimeText.setTime(mAccumulatedTime, true, true); mTimeText.blinkTimeStr(true); updateCurrentLap(mAccumulatedTime); setButtons(Stopwatches.STOPWATCH_STOPPED); mState = Stopwatches.STOPWATCH_STOPPED; } private void doStart(long time) { if (DEBUG) Log.v("StopwatchFragment.doStart"); mStartTime = time; startUpdateThread(); mTimeText.blinkTimeStr(false); if (mTime.isAnimating()) { mTime.startIntervalAnimation(); } setButtons(Stopwatches.STOPWATCH_RUNNING); mState = Stopwatches.STOPWATCH_RUNNING; } private void doLap() { if (DEBUG) Log.v("StopwatchFragment.doLap"); showLaps(); setButtons(Stopwatches.STOPWATCH_RUNNING); } private void doReset() { if (DEBUG) Log.v("StopwatchFragment.doReset"); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getActivity()); Utils.clearSwSharedPref(prefs); mTime.clearSharedPref(prefs, "sw"); mAccumulatedTime = 0; mLapsAdapter.clearLaps(); showLaps(); mTime.stopIntervalAnimation(); mTime.reset(); mTimeText.setTime(mAccumulatedTime, true, true); mTimeText.blinkTimeStr(false); setButtons(Stopwatches.STOPWATCH_RESET); mState = Stopwatches.STOPWATCH_RESET; } private void showShareButton(boolean show) { if (mShareButton != null) { mShareButton.setVisibility(show ? View.VISIBLE : View.INVISIBLE); mShareButton.setEnabled(show); } } private void showSharePopup() { Intent intent = getShareIntent(); Activity parent = getActivity(); PackageManager packageManager = parent.getPackageManager(); // Get a list of sharable options. List<ResolveInfo> shareOptions = packageManager .queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); if (shareOptions.size() == 0) { return; } ArrayList<CharSequence> shareOptionTitles = new ArrayList<CharSequence>(); ArrayList<Drawable> shareOptionIcons = new ArrayList<Drawable>(); ArrayList<CharSequence> shareOptionThreeTitles = new ArrayList<CharSequence>(); ArrayList<Drawable> shareOptionThreeIcons = new ArrayList<Drawable>(); ArrayList<String> shareOptionPackageNames = new ArrayList<String>(); ArrayList<String> shareOptionClassNames = new ArrayList<String>(); for (int option_i = 0; option_i < shareOptions.size(); option_i++) { ResolveInfo option = shareOptions.get(option_i); CharSequence label = option.loadLabel(packageManager); Drawable icon = option.loadIcon(packageManager); shareOptionTitles.add(label); shareOptionIcons.add(icon); if (shareOptions.size() > 4 && option_i < 3) { shareOptionThreeTitles.add(label); shareOptionThreeIcons.add(icon); } shareOptionPackageNames.add(option.activityInfo.packageName); shareOptionClassNames.add(option.activityInfo.name); } if (shareOptionTitles.size() > 4) { shareOptionThreeTitles.add(getResources().getString(R.string.see_all)); shareOptionThreeIcons.add(getResources().getDrawable(android.R.color.transparent)); } if (mSharePopup != null) { mSharePopup.dismiss(); mSharePopup = null; } mSharePopup = new ListPopupWindow(parent); mSharePopup.setAnchorView(mShareButton); mSharePopup.setModal(true); // This adapter to show the rest will be used to quickly repopulate if "See all..." is hit. ImageLabelAdapter showAllAdapter = new ImageLabelAdapter(parent, R.layout.popup_window_item, shareOptionTitles, shareOptionIcons, shareOptionPackageNames, shareOptionClassNames); if (shareOptionTitles.size() > 4) { mSharePopup.setAdapter(new ImageLabelAdapter(parent, R.layout.popup_window_item, shareOptionThreeTitles, shareOptionThreeIcons, shareOptionPackageNames, shareOptionClassNames, showAllAdapter)); } else { mSharePopup.setAdapter(showAllAdapter); } mSharePopup.setOnItemClickListener(new OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { CharSequence label = ((TextView) view.findViewById(R.id.title)).getText(); if (label.equals(getResources().getString(R.string.see_all))) { mSharePopup.setAdapter( ((ImageLabelAdapter) parent.getAdapter()).getShowAllAdapter()); mSharePopup.show(); return; } Intent intent = getShareIntent(); ImageLabelAdapter adapter = (ImageLabelAdapter) parent.getAdapter(); String packageName = adapter.getPackageName(position); String className = adapter.getClassName(position); intent.setClassName(packageName, className); startActivity(intent); } }); mSharePopup.setOnDismissListener(new OnDismissListener() { @Override public void onDismiss() { mSharePopup = null; } }); mSharePopup.setWidth((int) getResources().getDimension(R.dimen.popup_window_width)); mSharePopup.show(); } private Intent getShareIntent() { Intent intent = new Intent(android.content.Intent.ACTION_SEND); intent.setType("text/plain"); intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); intent.putExtra(Intent.EXTRA_SUBJECT, Stopwatches.getShareTitle(getActivity().getApplicationContext())); intent.putExtra(Intent.EXTRA_TEXT, Stopwatches.buildShareResults( getActivity().getApplicationContext(), mTimeText.getTimeString(), getLapShareTimes(mLapsAdapter.getLapTimes()))); return intent; } /** Turn laps as they would be saved in prefs into format for sharing. **/ private long[] getLapShareTimes(long[] input) { if (input == null) { return null; } int numLaps = input.length; long[] output = new long[numLaps]; long prevLapElapsedTime = 0; for (int lap_i = numLaps - 1; lap_i >= 0; lap_i--) { long lap = input[lap_i]; Log.v("lap "+lap_i+": "+lap); output[lap_i] = lap - prevLapElapsedTime; prevLapElapsedTime = lap; } return output; } /*** * Update the buttons on the stopwatch according to the watch's state */ private void setButtons(int state) { switch (state) { case Stopwatches.STOPWATCH_RESET: setButton(mLeftButton, R.string.sw_lap_button, R.drawable.ic_lap, false, View.INVISIBLE); setStartStopText(mCircleLayout, mCenterButton, R.string.sw_start_button); showShareButton(false); break; case Stopwatches.STOPWATCH_RUNNING: setButton(mLeftButton, R.string.sw_lap_button, R.drawable.ic_lap, !reachedMaxLaps(), View.VISIBLE); setStartStopText(mCircleLayout, mCenterButton, R.string.sw_stop_button); showShareButton(false); break; case Stopwatches.STOPWATCH_STOPPED: setButton(mLeftButton, R.string.sw_reset_button, R.drawable.ic_reset, true, View.VISIBLE); setStartStopText(mCircleLayout, mCenterButton, R.string.sw_start_button); showShareButton(true); break; default: break; } } private boolean reachedMaxLaps() { return mLapsAdapter.getCount() >= Stopwatches.MAX_LAPS; } /*** * Set a single button with the string and states provided. * @param b - Button view to update * @param text - Text in button * @param enabled - enable/disables the button * @param visibility - Show/hide the button */ private void setButton( ImageButton b, int text, int drawableId, boolean enabled, int visibility) { b.setContentDescription(getActivity().getResources().getString(text)); b.setImageResource(drawableId); b.setVisibility(visibility); b.setEnabled(enabled); } /** * Update the Start/Stop text. The button is within a view group with a transition that * is needed to animate the button moving. The transition also animates the the text changing, * but that animation does not provide a good look and feel. Temporarily disable the view group * transition while the text is changing and restore it afterwards. * * @param parent - View Group holding the start/stop button * @param textView - The start/stop button * @param text - Start or Stop id */ private void setStartStopText(final ViewGroup parent, TextView textView, int text) { final LayoutTransition layoutTransition = parent.getLayoutTransition(); // Tap into the parent layout->draw flow just before the draw ViewTreeObserver viewTreeObserver = parent.getViewTreeObserver(); if (viewTreeObserver != null) { viewTreeObserver.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { /** * Re-establish the transition handler * Remove this listener * * @return true so that onDraw() is called */ @Override public boolean onPreDraw() { parent.setLayoutTransition(layoutTransition); ViewTreeObserver viewTreeObserver = parent.getViewTreeObserver(); if (viewTreeObserver != null) { viewTreeObserver.removeOnPreDrawListener(this); } return true; } }); } // Remove the transition while the text is updated parent.setLayoutTransition(null); String textStr = getActivity().getResources().getString(text); textView.setText(textStr); textView.setContentDescription(textStr); } /*** * Handle action when user presses the lap button * @param time - in hundredth of a second */ private void addLapTime(long time) { // The total elapsed time final long curTime = time - mStartTime + mAccumulatedTime; int size = mLapsAdapter.getCount(); if (size == 0) { // Create and add the first lap Lap firstLap = new Lap(curTime, curTime); mLapsAdapter.addLap(firstLap); // Create the first active lap mLapsAdapter.addLap(new Lap(0, curTime)); // Update the interval on the clock and check the lap and total time formatting mTime.setIntervalTime(curTime); mLapsAdapter.updateTimeFormats(firstLap); } else { // Finish active lap final long lapTime = curTime - mLapsAdapter.getItem(1).mTotalTime; mLapsAdapter.getItem(0).mLapTime = lapTime; mLapsAdapter.getItem(0).mTotalTime = curTime; // Create a new active lap mLapsAdapter.addLap(new Lap(0, curTime)); // Update marker on clock and check that formatting for the lap number mTime.setMarkerTime(lapTime); mLapsAdapter.updateLapFormat(); } // Repaint the laps list mLapsAdapter.notifyDataSetChanged(); // Start lap animation starting from the second lap mTime.stopIntervalAnimation(); if (!reachedMaxLaps()) { mTime.startIntervalAnimation(); } } private void updateCurrentLap(long totalTime) { // There are either 0, 2 or more Laps in the list See {@link #addLapTime} if (mLapsAdapter.getCount() > 0) { Lap curLap = mLapsAdapter.getItem(0); curLap.mLapTime = totalTime - mLapsAdapter.getItem(1).mTotalTime; curLap.mTotalTime = totalTime; // If this lap has caused a change in the format for total and/or lap time, all of // the rows need a fresh print. The simplest way to refresh all of the rows is // calling notifyDataSetChanged. if (mLapsAdapter.updateTimeFormats(curLap)) { mLapsAdapter.notifyDataSetChanged(); } else { curLap.updateView(); } } } /** * Show or hide the laps-list */ private void showLaps() { if (DEBUG) Log.v(String.format("StopwatchFragment.showLaps: count=%d", mLapsAdapter.getCount())); boolean lapsVisible = mLapsAdapter.getCount() > 0; // Layout change animations will start upon the first add/hide view. Temporarily disable // the layout transition animation for the spacers, make the changes, then re-enable // the animation for the add/hide laps-list if (mSpacersUsed) { int spacersVisibility = lapsVisible ? View.GONE : View.VISIBLE; ViewGroup rootView = (ViewGroup) getView(); if (rootView != null) { rootView.setLayoutTransition(null); if (mStartSpace != null) { mStartSpace.setVisibility(spacersVisibility); } if (mEndSpace != null) { mEndSpace.setVisibility(spacersVisibility); } rootView.setLayoutTransition(mLayoutTransition); } } if (lapsVisible) { // There are laps - show the laps-list // No delay for the CircleButtonsLayout changes - start immediately so that the // circle has shifted before the laps-list starts appearing. mCircleLayoutTransition.setStartDelay(LayoutTransition.CHANGING, 0); mLapsList.setVisibility(View.VISIBLE); } else { // There are no laps - hide the laps list // Delay the CircleButtonsLayout animation until after the laps-list disappears long startDelay = mLayoutTransition.getStartDelay(LayoutTransition.DISAPPEARING) + mLayoutTransition.getDuration(LayoutTransition.DISAPPEARING); mCircleLayoutTransition.setStartDelay(LayoutTransition.CHANGING, startDelay); mLapsList.setVisibility(View.GONE); } } private void startUpdateThread() { mTime.post(mTimeUpdateThread); } private void stopUpdateThread() { mTime.removeCallbacks(mTimeUpdateThread); } Runnable mTimeUpdateThread = new Runnable() { @Override public void run() { long curTime = Utils.getTimeNow(); long totalTime = mAccumulatedTime + (curTime - mStartTime); if (mTime != null) { mTimeText.setTime(totalTime, true, true); } if (mLapsAdapter.getCount() > 0) { updateCurrentLap(totalTime); } mTime.postDelayed(mTimeUpdateThread, 10); } }; private void writeToSharedPref(SharedPreferences prefs) { SharedPreferences.Editor editor = prefs.edit(); editor.putLong (Stopwatches.PREF_START_TIME, mStartTime); editor.putLong (Stopwatches.PREF_ACCUM_TIME, mAccumulatedTime); editor.putInt (Stopwatches.PREF_STATE, mState); if (mLapsAdapter != null) { long [] laps = mLapsAdapter.getLapTimes(); if (laps != null) { editor.putInt (Stopwatches.PREF_LAP_NUM, laps.length); for (int i = 0; i < laps.length; i++) { String key = Stopwatches.PREF_LAP_TIME + Integer.toString(laps.length - i); editor.putLong (key, laps[i]); } } } if (mState == Stopwatches.STOPWATCH_RUNNING) { editor.putLong(Stopwatches.NOTIF_CLOCK_BASE, mStartTime-mAccumulatedTime); editor.putLong(Stopwatches.NOTIF_CLOCK_ELAPSED, -1); editor.putBoolean(Stopwatches.NOTIF_CLOCK_RUNNING, true); } else if (mState == Stopwatches.STOPWATCH_STOPPED) { editor.putLong(Stopwatches.NOTIF_CLOCK_ELAPSED, mAccumulatedTime); editor.putLong(Stopwatches.NOTIF_CLOCK_BASE, -1); editor.putBoolean(Stopwatches.NOTIF_CLOCK_RUNNING, false); } else if (mState == Stopwatches.STOPWATCH_RESET) { editor.remove(Stopwatches.NOTIF_CLOCK_BASE); editor.remove(Stopwatches.NOTIF_CLOCK_RUNNING); editor.remove(Stopwatches.NOTIF_CLOCK_ELAPSED); } editor.putBoolean(Stopwatches.PREF_UPDATE_CIRCLE, false); editor.apply(); } private void readFromSharedPref(SharedPreferences prefs) { mStartTime = prefs.getLong(Stopwatches.PREF_START_TIME, 0); mAccumulatedTime = prefs.getLong(Stopwatches.PREF_ACCUM_TIME, 0); mState = prefs.getInt(Stopwatches.PREF_STATE, Stopwatches.STOPWATCH_RESET); int numLaps = prefs.getInt(Stopwatches.PREF_LAP_NUM, Stopwatches.STOPWATCH_RESET); if (mLapsAdapter != null) { long[] oldLaps = mLapsAdapter.getLapTimes(); if (oldLaps == null || oldLaps.length < numLaps) { long[] laps = new long[numLaps]; long prevLapElapsedTime = 0; for (int lap_i = 0; lap_i < numLaps; lap_i++) { String key = Stopwatches.PREF_LAP_TIME + Integer.toString(lap_i + 1); long lap = prefs.getLong(key, 0); laps[numLaps - lap_i - 1] = lap - prevLapElapsedTime; prevLapElapsedTime = lap; } mLapsAdapter.setLapTimes(laps); } } if (prefs.getBoolean(Stopwatches.PREF_UPDATE_CIRCLE, true)) { if (mState == Stopwatches.STOPWATCH_STOPPED) { doStop(); } else if (mState == Stopwatches.STOPWATCH_RUNNING) { doStart(mStartTime); } else if (mState == Stopwatches.STOPWATCH_RESET) { doReset(); } } } public class ImageLabelAdapter extends ArrayAdapter<CharSequence> { private final ArrayList<CharSequence> mStrings; private final ArrayList<Drawable> mDrawables; private final ArrayList<String> mPackageNames; private final ArrayList<String> mClassNames; private ImageLabelAdapter mShowAllAdapter; public ImageLabelAdapter(Context context, int textViewResourceId, ArrayList<CharSequence> strings, ArrayList<Drawable> drawables, ArrayList<String> packageNames, ArrayList<String> classNames) { super(context, textViewResourceId, strings); mStrings = strings; mDrawables = drawables; mPackageNames = packageNames; mClassNames = classNames; } // Use this constructor if showing a "see all" option, to pass in the adapter // that will be needed to quickly show all the remaining options. public ImageLabelAdapter(Context context, int textViewResourceId, ArrayList<CharSequence> strings, ArrayList<Drawable> drawables, ArrayList<String> packageNames, ArrayList<String> classNames, ImageLabelAdapter showAllAdapter) { super(context, textViewResourceId, strings); mStrings = strings; mDrawables = drawables; mPackageNames = packageNames; mClassNames = classNames; mShowAllAdapter = showAllAdapter; } @Override public View getView(int position, View convertView, ViewGroup parent) { LayoutInflater li = getActivity().getLayoutInflater(); View row = li.inflate(R.layout.popup_window_item, parent, false); ((TextView) row.findViewById(R.id.title)).setText( mStrings.get(position)); row.findViewById(R.id.icon).setBackground(mDrawables.get(position)); return row; } public String getPackageName(int position) { return mPackageNames.get(position); } public String getClassName(int position) { return mClassNames.get(position); } public ImageLabelAdapter getShowAllAdapter() { return mShowAllAdapter; } } @Override public void onSharedPreferenceChanged(SharedPreferences prefs, String key) { if (prefs.equals(PreferenceManager.getDefaultSharedPreferences(getActivity()))) { if (! (key.equals(Stopwatches.PREF_LAP_NUM) || key.startsWith(Stopwatches.PREF_LAP_TIME))) { readFromSharedPref(prefs); if (prefs.getBoolean(Stopwatches.PREF_UPDATE_CIRCLE, true)) { mTime.readFromSharedPref(prefs, "sw"); } } } } // Used to keeps screen on when stopwatch is running. private void acquireWakeLock() { if (mWakeLock == null) { final PowerManager pm = (PowerManager) getActivity().getSystemService(Context.POWER_SERVICE); mWakeLock = pm.newWakeLock( PowerManager.SCREEN_BRIGHT_WAKE_LOCK | PowerManager.ON_AFTER_RELEASE, TAG); mWakeLock.setReferenceCounted(false); } mWakeLock.acquire(); } private void releaseWakeLock() { if (mWakeLock != null && mWakeLock.isHeld()) { mWakeLock.release(); } } }