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.ActivityNotFoundException;
     20 import android.content.Context;
     21 import android.content.Intent;
     22 import android.os.Bundle;
     23 import android.os.PowerManager;
     24 import android.os.SystemClock;
     25 import android.support.v7.widget.LinearLayoutManager;
     26 import android.support.v7.widget.RecyclerView;
     27 import android.support.v7.widget.SimpleItemAnimator;
     28 import android.transition.AutoTransition;
     29 import android.transition.Transition;
     30 import android.transition.TransitionManager;
     31 import android.view.LayoutInflater;
     32 import android.view.View;
     33 import android.view.ViewGroup;
     34 import android.view.accessibility.AccessibilityManager;
     35 
     36 import com.android.deskclock.DeskClock;
     37 import com.android.deskclock.DeskClockFragment;
     38 import com.android.deskclock.LogUtils;
     39 import com.android.deskclock.R;
     40 import com.android.deskclock.data.DataModel;
     41 import com.android.deskclock.data.Lap;
     42 import com.android.deskclock.data.Stopwatch;
     43 import com.android.deskclock.events.Events;
     44 import com.android.deskclock.timer.CountingTimerView;
     45 
     46 import static android.content.Context.ACCESSIBILITY_SERVICE;
     47 import static android.content.Context.POWER_SERVICE;
     48 import static android.os.PowerManager.ON_AFTER_RELEASE;
     49 import static android.os.PowerManager.SCREEN_BRIGHT_WAKE_LOCK;
     50 import static android.view.View.GONE;
     51 import static android.view.View.INVISIBLE;
     52 import static android.view.View.VISIBLE;
     53 
     54 /**
     55  * Fragment that shows the stopwatch and recorded laps.
     56  */
     57 public final class StopwatchFragment extends DeskClockFragment {
     58 
     59     private static final String TAG = "StopwatchFragment";
     60 
     61     /** Scheduled to update the stopwatch time and current lap time while stopwatch is running. */
     62     private final Runnable mTimeUpdateRunnable = new TimeUpdateRunnable();
     63 
     64     /** Used to determine when talk back is on in order to lower the time update rate. */
     65     private AccessibilityManager mAccessibilityManager;
     66 
     67     /** {@code true} while the {@link #mLapsList} is transitioning between shown and hidden. */
     68     private boolean mLapsListIsTransitioning;
     69 
     70     /** The data source for {@link #mLapsList}. */
     71     private LapsAdapter mLapsAdapter;
     72 
     73     /** The layout manager for the {@link #mLapsAdapter}. */
     74     private LinearLayoutManager mLapsLayoutManager;
     75 
     76     /** Draws the reference lap while the stopwatch is running. */
     77     private StopwatchCircleView mTime;
     78 
     79     /** Displays the recorded lap times. */
     80     private RecyclerView mLapsList;
     81 
     82     /** Displays the current stopwatch time. */
     83     private CountingTimerView mTimeText;
     84 
     85     /** Held while the stopwatch is running and this fragment is forward to keep the screen on. */
     86     private PowerManager.WakeLock mWakeLock;
     87 
     88     /** The public no-arg constructor required by all fragments. */
     89     public StopwatchFragment() {}
     90 
     91     @Override
     92     public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle state) {
     93         mLapsAdapter = new LapsAdapter(getActivity());
     94         mLapsLayoutManager = new LinearLayoutManager(getActivity());
     95 
     96         final View v = inflater.inflate(R.layout.stopwatch_fragment, container, false);
     97         mTime = (StopwatchCircleView) v.findViewById(R.id.stopwatch_time);
     98         mLapsList = (RecyclerView) v.findViewById(R.id.laps_list);
     99         ((SimpleItemAnimator) mLapsList.getItemAnimator()).setSupportsChangeAnimations(false);
    100         mLapsList.setLayoutManager(mLapsLayoutManager);
    101         mLapsList.setAdapter(mLapsAdapter);
    102 
    103         // Timer text serves as a virtual start/stop button.
    104         mTimeText = (CountingTimerView) v.findViewById(R.id.stopwatch_time_text);
    105         mTimeText.setVirtualButtonEnabled(true);
    106         mTimeText.registerVirtualButtonAction(new ToggleStopwatchRunnable());
    107 
    108         return v;
    109     }
    110 
    111     @Override
    112     public void onActivityCreated(Bundle savedInstanceState) {
    113         super.onActivityCreated(savedInstanceState);
    114 
    115         mAccessibilityManager =
    116                 (AccessibilityManager) getActivity().getSystemService(ACCESSIBILITY_SERVICE);
    117     }
    118 
    119     @Override
    120     public void onResume() {
    121         super.onResume();
    122 
    123         // Conservatively assume the data in the adapter has changed while the fragment was paused.
    124         mLapsAdapter.notifyDataSetChanged();
    125 
    126         // Update the state of the buttons.
    127         setFabAppearance();
    128         setLeftRightButtonAppearance();
    129 
    130         // Draw the current stopwatch and lap times.
    131         updateTime();
    132 
    133         // Start updates if the stopwatch is running; blink text if it is paused.
    134         switch (getStopwatch().getState()) {
    135             case RUNNING:
    136                 acquireWakeLock();
    137                 mTime.update();
    138                 startUpdatingTime();
    139                 break;
    140             case PAUSED:
    141                 mTimeText.blinkTimeStr(true);
    142                 break;
    143         }
    144 
    145         // Adjust the visibility of the list of laps.
    146         showOrHideLaps(false);
    147 
    148         // Start watching for page changes away from this fragment.
    149         getDeskClock().registerPageChangedListener(this);
    150 
    151         // View is hidden in onPause, make sure it is visible now.
    152         final View view = getView();
    153         if (view != null) {
    154             view.setVisibility(VISIBLE);
    155         }
    156     }
    157 
    158     @Override
    159     public void onPause() {
    160         super.onPause();
    161 
    162         final View view = getView();
    163         if (view != null) {
    164             // Make the view invisible because when the lock screen is activated, the window stays
    165             // active under it. Later, when unlocking the screen, we see the old stopwatch time for
    166             // a fraction of a second.
    167             getView().setVisibility(INVISIBLE);
    168         }
    169 
    170         // Stop all updates while the fragment is not visible.
    171         stopUpdatingTime();
    172         mTimeText.blinkTimeStr(false);
    173 
    174         // Stop watching for page changes away from this fragment.
    175         getDeskClock().unregisterPageChangedListener(this);
    176 
    177         // Release the wake lock if it is currently held.
    178         releaseWakeLock();
    179     }
    180 
    181     @Override
    182     public void onPageChanged(int page) {
    183         if (page == DeskClock.STOPWATCH_TAB_INDEX && getStopwatch().isRunning()) {
    184             acquireWakeLock();
    185         } else {
    186             releaseWakeLock();
    187         }
    188     }
    189 
    190     @Override
    191     public void onFabClick(View view) {
    192         toggleStopwatchState();
    193     }
    194 
    195     @Override
    196     public void onLeftButtonClick(View view) {
    197         switch (getStopwatch().getState()) {
    198             case RUNNING:
    199                 doAddLap();
    200                 break;
    201             case PAUSED:
    202                 doReset();
    203                 break;
    204         }
    205     }
    206 
    207     @Override
    208     public void onRightButtonClick(View view) {
    209         doShare();
    210     }
    211 
    212     @Override
    213     public void setFabAppearance() {
    214         if (mFab == null || getSelectedTab() != DeskClock.STOPWATCH_TAB_INDEX) {
    215             return;
    216         }
    217 
    218         if (getStopwatch().isRunning()) {
    219             mFab.setImageResource(R.drawable.ic_pause_white_24dp);
    220             mFab.setContentDescription(getString(R.string.sw_pause_button));
    221         } else {
    222             mFab.setImageResource(R.drawable.ic_start_white_24dp);
    223             mFab.setContentDescription(getString(R.string.sw_start_button));
    224         }
    225         mFab.setVisibility(VISIBLE);
    226     }
    227 
    228     @Override
    229     public void setLeftRightButtonAppearance() {
    230         if (mLeftButton == null || mRightButton == null ||
    231                 getSelectedTab() != DeskClock.STOPWATCH_TAB_INDEX) {
    232             return;
    233         }
    234 
    235         mRightButton.setImageResource(R.drawable.ic_share);
    236         mRightButton.setContentDescription(getString(R.string.sw_share_button));
    237 
    238         switch (getStopwatch().getState()) {
    239             case RESET:
    240                 mLeftButton.setEnabled(false);
    241                 mLeftButton.setVisibility(INVISIBLE);
    242                 mRightButton.setVisibility(INVISIBLE);
    243                 break;
    244             case RUNNING:
    245                 mLeftButton.setImageResource(R.drawable.ic_lap);
    246                 mLeftButton.setContentDescription(getString(R.string.sw_lap_button));
    247                 mLeftButton.setEnabled(canRecordMoreLaps());
    248                 mLeftButton.setVisibility(canRecordMoreLaps() ? VISIBLE : INVISIBLE);
    249                 mRightButton.setVisibility(INVISIBLE);
    250                 break;
    251             case PAUSED:
    252                 mLeftButton.setEnabled(true);
    253                 mLeftButton.setImageResource(R.drawable.ic_reset);
    254                 mLeftButton.setContentDescription(getString(R.string.sw_reset_button));
    255                 mLeftButton.setVisibility(VISIBLE);
    256                 mRightButton.setVisibility(VISIBLE);
    257                 break;
    258         }
    259     }
    260 
    261     /**
    262      * Start the stopwatch.
    263      */
    264     private void doStart() {
    265         Events.sendStopwatchEvent(R.string.action_start, R.string.label_deskclock);
    266 
    267         // Update the stopwatch state.
    268         DataModel.getDataModel().startStopwatch();
    269 
    270         // Start UI updates.
    271         startUpdatingTime();
    272         mTime.update();
    273         mTimeText.blinkTimeStr(false);
    274 
    275         // Update button states.
    276         setFabAppearance();
    277         setLeftRightButtonAppearance();
    278 
    279         // Acquire the wake lock.
    280         acquireWakeLock();
    281     }
    282 
    283     /**
    284      * Pause the stopwatch.
    285      */
    286     private void doPause() {
    287         Events.sendStopwatchEvent(R.string.action_pause, R.string.label_deskclock);
    288 
    289         // Update the stopwatch state
    290         DataModel.getDataModel().pauseStopwatch();
    291 
    292         // Redraw the paused stopwatch time.
    293         updateTime();
    294 
    295         // Stop UI updates.
    296         stopUpdatingTime();
    297         mTimeText.blinkTimeStr(true);
    298 
    299         // Update button states.
    300         setFabAppearance();
    301         setLeftRightButtonAppearance();
    302 
    303         // Release the wake lock.
    304         releaseWakeLock();
    305     }
    306 
    307     /**
    308      * Reset the stopwatch.
    309      */
    310     private void doReset() {
    311         Events.sendStopwatchEvent(R.string.action_reset, R.string.label_deskclock);
    312 
    313         // Update the stopwatch state.
    314         DataModel.getDataModel().resetStopwatch();
    315 
    316         // Clear the laps.
    317         showOrHideLaps(true);
    318 
    319         // Clear the times.
    320         mTime.postInvalidateOnAnimation();
    321         mTimeText.setTime(0, true, true);
    322         mTimeText.blinkTimeStr(false);
    323 
    324         // Update button states.
    325         setFabAppearance();
    326         setLeftRightButtonAppearance();
    327 
    328         // Release the wake lock.
    329         releaseWakeLock();
    330     }
    331 
    332     /**
    333      * Send stopwatch time and lap times to an external sharing application.
    334      */
    335     private void doShare() {
    336         final String[] subjects = getResources().getStringArray(R.array.sw_share_strings);
    337         final String subject = subjects[(int)(Math.random() * subjects.length)];
    338         final String text = mLapsAdapter.getShareText();
    339 
    340         final Intent shareIntent = new Intent(Intent.ACTION_SEND)
    341                 .addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET)
    342                 .putExtra(Intent.EXTRA_SUBJECT, subject)
    343                 .putExtra(Intent.EXTRA_TEXT, text)
    344                 .setType("text/plain");
    345 
    346         final Context context = getActivity();
    347         final String title = context.getString(R.string.sw_share_button);
    348         final Intent shareChooserIntent = Intent.createChooser(shareIntent, title);
    349         try {
    350             context.startActivity(shareChooserIntent);
    351         } catch (ActivityNotFoundException anfe) {
    352             LogUtils.e("No compatible receiver is found");
    353         }
    354     }
    355 
    356     /**
    357      * Record and add a new lap ending now.
    358      */
    359     private void doAddLap() {
    360         Events.sendStopwatchEvent(R.string.action_lap, R.string.label_deskclock);
    361 
    362         // Record a new lap.
    363         final Lap lap = mLapsAdapter.addLap();
    364         if (lap == null) {
    365             return;
    366         }
    367 
    368         // Update button states.
    369         setLeftRightButtonAppearance();
    370 
    371         if (lap.getLapNumber() == 1) {
    372             // Child views from prior lap sets hang around and blit to the screen when adding the
    373             // first lap of the subsequent lap set. Remove those superfluous children here manually
    374             // to ensure they aren't seen as the first lap is drawn.
    375             mLapsList.removeAllViewsInLayout();
    376 
    377             // Start animating the reference lap.
    378             mTime.update();
    379 
    380             // Recording the first lap transitions the UI to display the laps list.
    381             showOrHideLaps(false);
    382         }
    383 
    384         // Ensure the newly added lap is visible on screen.
    385         mLapsList.scrollToPosition(0);
    386     }
    387 
    388     /**
    389      * Show or hide the list of laps.
    390      */
    391     private void showOrHideLaps(boolean clearLaps) {
    392         final Transition transition = new AutoTransition()
    393                 .addListener(new Transition.TransitionListener() {
    394                     @Override
    395                     public void onTransitionStart(Transition transition) {
    396                         mLapsListIsTransitioning = true;
    397                     }
    398 
    399                     @Override
    400                     public void onTransitionEnd(Transition transition) {
    401                         mLapsListIsTransitioning = false;
    402                     }
    403 
    404                     @Override
    405                     public void onTransitionCancel(Transition transition) {
    406                     }
    407 
    408                     @Override
    409                     public void onTransitionPause(Transition transition) {
    410                     }
    411 
    412                     @Override
    413                     public void onTransitionResume(Transition transition) {
    414                     }
    415                 });
    416 
    417         final ViewGroup sceneRoot = (ViewGroup) getView();
    418         TransitionManager.beginDelayedTransition(sceneRoot, transition);
    419 
    420         if (clearLaps) {
    421             mLapsAdapter.clearLaps();
    422         }
    423 
    424         final boolean lapsVisible = mLapsAdapter.getItemCount() > 0;
    425         mLapsList.setVisibility(lapsVisible ? VISIBLE : GONE);
    426     }
    427 
    428     private void acquireWakeLock() {
    429         if (mWakeLock == null) {
    430             final PowerManager pm = (PowerManager) getActivity().getSystemService(POWER_SERVICE);
    431             mWakeLock = pm.newWakeLock(SCREEN_BRIGHT_WAKE_LOCK | ON_AFTER_RELEASE, TAG);
    432             mWakeLock.setReferenceCounted(false);
    433         }
    434         mWakeLock.acquire();
    435     }
    436 
    437     private void releaseWakeLock() {
    438         if (mWakeLock != null && mWakeLock.isHeld()) {
    439             mWakeLock.release();
    440         }
    441     }
    442 
    443     /**
    444      * Either pause or start the stopwatch based on its current state.
    445      */
    446     private void toggleStopwatchState() {
    447         if (getStopwatch().isRunning()) {
    448             doPause();
    449         } else {
    450             doStart();
    451         }
    452     }
    453 
    454     private Stopwatch getStopwatch() {
    455         return DataModel.getDataModel().getStopwatch();
    456     }
    457 
    458     private boolean canRecordMoreLaps() {
    459         return DataModel.getDataModel().canAddMoreLaps();
    460     }
    461 
    462     /**
    463      * Post the first runnable to update times within the UI. It will reschedule itself as needed.
    464      */
    465     private void startUpdatingTime() {
    466         // Ensure only one copy of the runnable is ever scheduled by first stopping updates.
    467         stopUpdatingTime();
    468         mTime.post(mTimeUpdateRunnable);
    469     }
    470 
    471     /**
    472      * Remove the runnable that updates times within the UI.
    473      */
    474     private void stopUpdatingTime() {
    475         mTime.removeCallbacks(mTimeUpdateRunnable);
    476     }
    477 
    478     /**
    479      * Update all time displays based on a single snapshot of the stopwatch progress. This includes
    480      * the stopwatch time drawn in the circle, the current lap time and the total elapsed time in
    481      * the list of laps.
    482      */
    483     private void updateTime() {
    484         // Compute the total time of the stopwatch.
    485         final long totalTime = getStopwatch().getTotalTime();
    486 
    487         // Update the total time display.
    488         mTimeText.setTime(totalTime, true, true);
    489 
    490         // Update the current lap.
    491         final boolean currentLapIsVisible = mLapsLayoutManager.findFirstVisibleItemPosition() == 0;
    492         if (!mLapsListIsTransitioning && currentLapIsVisible) {
    493             mLapsAdapter.updateCurrentLap(mLapsList, totalTime);
    494         }
    495     }
    496 
    497     /**
    498      * This runnable periodically updates times throughout the UI. It stops these updates when the
    499      * stopwatch is no longer running.
    500      */
    501     private final class TimeUpdateRunnable implements Runnable {
    502         @Override
    503         public void run() {
    504             final long startTime = SystemClock.elapsedRealtime();
    505 
    506             updateTime();
    507 
    508             if (getStopwatch().isRunning()) {
    509                 // The stopwatch is still running so execute this runnable again after a delay.
    510                 final boolean talkBackOn = mAccessibilityManager.isTouchExplorationEnabled();
    511 
    512                 // Grant longer time between redraws when talk-back is on to let it catch up.
    513                 final int period = talkBackOn ? 500 : 25;
    514 
    515                 // Try to maintain a consistent period of time between redraws.
    516                 final long endTime = SystemClock.elapsedRealtime();
    517                 final long delay = Math.max(0, startTime + period - endTime);
    518 
    519                 mTime.postDelayed(this, delay);
    520             }
    521         }
    522     }
    523 
    524     /**
    525      * Tapping the stopwatch text also toggles the stopwatch state, just like the fab.
    526      */
    527     private final class ToggleStopwatchRunnable implements Runnable {
    528         @Override
    529         public void run() {
    530             toggleStopwatchState();
    531         }
    532     }
    533 }
    534