com.heightechllc.breakify.MainActivity.java Source code

Java tutorial

Introduction

Here is the source code for com.heightechllc.breakify.MainActivity.java

Source

/**
 * Copyright (C) 2014  Shlomo Zalman Heigh
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.heightechllc.breakify;

import android.annotation.TargetApi;
import android.app.Activity;
import android.app.AlarmManager;
import android.app.AlertDialog;
import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcelable;
import android.os.SystemClock;
import android.preference.PreferenceActivity;
import android.preference.PreferenceManager;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.Button;
import android.widget.ImageButton;
import android.widget.TextView;

import com.cocosw.undobar.UndoBarController;
import com.heightechllc.breakify.preferences.MiscSettingsFragment;
import com.heightechllc.breakify.preferences.ScheduledStartSettingsFragment;
import com.heightechllc.breakify.preferences.SettingsActivity;
import com.heightechllc.breakify.preferences.TimerDurationsSettingsFragment;
import com.mixpanel.android.mpmetrics.MixpanelAPI;

import org.json.JSONException;
import org.json.JSONObject;

/**
 * The app's main activity. Controls the timer and main UI.
 * Displays the timer's progress using a {@link CircleTimerView}.
 *
 * For analytics, I'm trying out Mixpanel to see if they're any better than Google Analytics, et al.
 * Analytics can be disabled in the {@link com.heightechllc.breakify.preferences.SettingsActivity}
 *  for those who don't like their usage to be tracked.
 */
public class MainActivity extends Activity implements View.OnClickListener {
    public static MixpanelAPI mixpanel;

    /**
     * Extra to inform the Activity that it is being opened automatically by ScheduledStartReceiver.
     * FLAG_ACTIVITY_NO_USER_ACTION should also be set on the intent when using this extra.
     */
    public static final String EXTRA_SCHEDULED_START = "com.heightechllc.breakify.ScheduledStart";
    /**
     * Extra to instruct the Activity to open the RingingActivity. If FLAG_ACTIVITY_NO_USER_ACTION
     * is set on the Intent, the alarm will begin ringing as well.
     */
    public static final String EXTRA_ALARM_RING = "com.heightechllc.breakify.AlarmRing";
    /**
     * Extra to instruct the Activity to snooze the alarm. Add this when opening from the "Snooze"
     * action of the expanded notification.
     */
    public static final String EXTRA_SNOOZE = "com.heightechllc.breakify.Snooze";

    /**
     * The request code for the PendingIntent to ring the timer
     */
    public static final int ALARM_MANAGER_REQUEST_CODE = 613;

    // Timer states
    public static final int TIMER_STATE_RUNNING = 1;
    public static final int TIMER_STATE_PAUSED = 2;
    public static final int TIMER_STATE_STOPPED = 0;

    // Work states
    public static final int WORK_STATE_WORKING = 1;
    public static final int WORK_STATE_BREAKING = 2;

    private static final String tag = "MainActivity";

    private int timerState = TIMER_STATE_STOPPED;
    private int _workState = WORK_STATE_WORKING;

    private SharedPreferences sharedPref;
    private AlarmManager alarmManager;

    // UI Components
    private CircleTimerView circleTimer;
    private TextView stateLbl, timeLbl, startStopLbl;
    private ImageButton resetBtn;
    private Button skipBtn;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Set up the default values for the preferences
        PreferenceManager.setDefaultValues(this, R.xml.timer_durations_preferences, true);
        PreferenceManager.setDefaultValues(this, R.xml.alarm_preferences, true);
        PreferenceManager.setDefaultValues(this, R.xml.scheduled_start_preferences, true);
        PreferenceManager.setDefaultValues(this, R.xml.misc_preferences, true);

        setContentView(R.layout.activity_main);

        // If the user presses the device's volume keys, we want to adjust the alarm volume
        setVolumeControlStream(AlarmRinger.STREAM_TYPE);

        //
        // Set up components
        //
        stateLbl = (TextView) findViewById(R.id.state_lbl);
        timeLbl = (TextView) findViewById(R.id.time_lbl);
        startStopLbl = (TextView) findViewById(R.id.start_stop_lbl);

        circleTimer = (CircleTimerView) findViewById(R.id.circle_timer);
        circleTimer.setOnClickListener(this);
        circleTimer.setTimeDisplay(timeLbl);

        resetBtn = (ImageButton) findViewById(R.id.reset_btn);
        resetBtn.setOnClickListener(this);

        skipBtn = (Button) findViewById(R.id.skip_btn);
        skipBtn.setOnClickListener(this);

        sharedPref = PreferenceManager.getDefaultSharedPreferences(this);
        alarmManager = (AlarmManager) getSystemService(ALARM_SERVICE);

        // Check if analytics are enabled in preferences
        if (sharedPref.getBoolean(MiscSettingsFragment.KEY_ANALYTICS_ENABLED, false) && !BuildConfig.DEBUG)
            mixpanel = MixpanelAPI.getInstance(this, "d78a075fc861c288e24664a8905a6698");

        // Handle the intent
        boolean shouldRestoreSavedTimer = handleIntent(getIntent());

        if (shouldRestoreSavedTimer) {
            // Restore saved timer state
            restoreSavedTimer();
        }

        // Add the custom alarm tones to the phone's storage, if they weren't copied yet.
        // Works on a separate thread.
        if (!sharedPref.getBoolean(CustomAlarmTones.PREF_KEY_RINGTONES_COPIED, false))
            CustomAlarmTones.installToStorage(this);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();

        // Enable or disable the RescheduleReceiver, which restores AlarmManagers when the system
        //  boots or the time changes. We only want it enabled if an alarm is scheduled, or if
        //  Scheduled Start is enabled.
        int enabledState;
        if (timerState == TIMER_STATE_RUNNING || ScheduledStart.isEnabled(this))
            enabledState = PackageManager.COMPONENT_ENABLED_STATE_ENABLED;
        else
            enabledState = PackageManager.COMPONENT_ENABLED_STATE_DISABLED;

        ComponentName receiver = new ComponentName(this, RescheduleReceiver.class);
        getPackageManager().setComponentEnabledSetting(receiver, enabledState, PackageManager.DONT_KILL_APP);

        // Send any unsent analytics events
        if (mixpanel != null)
            mixpanel.flush();
    }

    @Override
    protected void onNewIntent(Intent intent) {
        super.onNewIntent(intent);

        handleIntent(intent);
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        MenuInflater inflater = getMenuInflater();
        inflater.inflate(R.menu.main_activity_actions, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
        case R.id.action_settings:
            // Open the SettingsActivity
            startActivity(new Intent(this, SettingsActivity.class));
            return true;
        default:
            return super.onOptionsItemSelected(item);
        }
    }

    @Override
    public void onClick(View view) {
        switch (view.getId()) {
        case R.id.circle_timer:
            if (timerState == TIMER_STATE_RUNNING)
                pauseTimer();
            else
                startTimer();

            break;
        case R.id.reset_btn:
            cancelScheduledAlarm();

            resetTimerUI(false);

            // Analytics
            if (mixpanel != null) {
                String eventName = getWorkState() == WORK_STATE_WORKING ? "Work timer reset" : "Break timer reset";
                mixpanel.track(eventName, null);
            }

            break;
        case R.id.skip_btn:
            skipToNextState();
        }
    }

    /**
     * Called when we get a result from RingingActivity, meaning the user chose an action
     */
    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);

        if (requestCode != RingingActivity.REQUEST_ALARM_RING)
            return; // We didn't request it and we don't know what to do with the result

        // Restore the work state from preferences, since `restoreSavedTimer()` wasn't called
        setWorkState(sharedPref.getInt("workState", _workState));

        switch (resultCode) {
        case RingingActivity.RESULT_ALARM_RING_OK:
            // Set the new state
            if (getWorkState() == WORK_STATE_WORKING)
                setWorkState(WORK_STATE_BREAKING);
            else
                setWorkState(WORK_STATE_WORKING);

            // Now start the timer
            startTimer();

            break;
        case RingingActivity.RESULT_ALARM_RING_SNOOZE:
            snoozeTimer();
            break;
        case RingingActivity.RESULT_ALARM_RING_CANCEL:
            // User chose to cancel
            resetTimerUI(true);
            // Analytics
            if (mixpanel != null) {
                // We want to have a separate event for when the user presses the "cancel" btn
                //  in RingingActivity, vs. when they press the "reset" btn
                String eventName = getWorkState() == WORK_STATE_WORKING ? "Work timer cancelled"
                        : "Break timer cancelled";
                mixpanel.track(eventName, null);
            }
        }
    }

    /**
     * Handles a new intent, either from onNewIntent() or onCreate()
     * @param intent The intent to handle
     * @return Whether we should attempt to restore the saved timer state. Will be false when
     *  this method opens another Activity.
     */
    private boolean handleIntent(Intent intent) {
        boolean shouldRestoreSavedTimer = true;

        if (intent.getBooleanExtra(EXTRA_SNOOZE, false)) {
            // The activity was launched from the expanded notification's "Snooze" action

            snoozeTimer();
            // In case user didn't interact with the RingingActivity, and instead snoozed directly
            //  from the notification
            AlarmRinger.stop(this);

            // Don't restore, since we're about to open a new Activity
            shouldRestoreSavedTimer = false;

        } else if (intent.getBooleanExtra(EXTRA_ALARM_RING, false)) {
            // The Activity was launched from AlarmReceiver, meaning the timer finished and we
            //  need to ring the alarm

            Intent ringingIntent = new Intent(this, RingingActivity.class);
            // Pass along FLAG_ACTIVITY_NO_USER_ACTION if it was set when calling this activity
            if ((intent.getFlags() & Intent.FLAG_ACTIVITY_NO_USER_ACTION) != 0)
                ringingIntent.setFlags(Intent.FLAG_ACTIVITY_NO_USER_ACTION);
            startActivityForResult(ringingIntent, RingingActivity.REQUEST_ALARM_RING);

            // Don't restore, since we're about to open a new Activity
            shouldRestoreSavedTimer = false;

        } else if (intent.getBooleanExtra(EXTRA_SCHEDULED_START, false)) {
            // The Activity was launched from ScheduledStartReceiver, meaning it's time for the
            //  scheduled start

            // Show a dialog prompting the user to start
            new AlertDialog.Builder(this).setTitle(R.string.scheduled_dialog_title)
                    .setMessage(R.string.scheduled_dialog_message)
                    .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialogInterface, int i) {
                            // Start the timer
                            circleTimer.performClick();
                        }
                    }).setNeutralButton(R.string.action_settings, new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialogInterface, int i) {
                            // Show the settings activity with the Scheduled Start settings
                            Intent intent = new Intent(MainActivity.this, SettingsActivity.class);
                            intent.putExtra(PreferenceActivity.EXTRA_SHOW_FRAGMENT,
                                    ScheduledStartSettingsFragment.class.getName());
                            intent.putExtra(PreferenceActivity.EXTRA_SHOW_FRAGMENT_TITLE,
                                    R.string.pref_category_scheduled);
                            startActivity(intent);
                        }
                    }).setNegativeButton(R.string.cancel, null).show();
        }

        return shouldRestoreSavedTimer;
    }

    /**
     * Attempts to restore the timer state from SharedPreferences
     * @return Whether the state was restored
     */
    private boolean restoreSavedTimer() {
        long totalTime = sharedPref.getLong("schedTotalTime", 0);
        if (totalTime < 1)
            return false; // Means no alarm is saved

        // Get the scheduled ring time (which will only be set if the timer was running)
        long ringUnixTime = sharedPref.getLong("schedRingTime", 0);
        // Get the saved time remaining for the paused timer (only set if the timer was paused)
        long pausedTimeRemaining = sharedPref.getLong("pausedTimeRemaining", 0);

        if (ringUnixTime < 1 && pausedTimeRemaining < 1)
            return false; // Defensive programming

        // Restore the work state
        setWorkState(sharedPref.getInt("workState", WORK_STATE_WORKING));

        circleTimer.setTotalTime(totalTime);

        if (ringUnixTime > 0) {
            // Attempt to restore running timer

            // Convert from Unix / epoch time to elapsedRealtime
            long timeFromNow = ringUnixTime - System.currentTimeMillis();

            // Check if the timer is scheduled to ring in the future or past
            if (timeFromNow > 0) {
                // Ring time is in the future
                // Update the time label
                circleTimer.updateTimeLbl(timeFromNow);
                // Cause startTimer() to treat it like we're resuming (b/c we are)
                timerState = TIMER_STATE_PAUSED;
                // Go!
                startTimer(timeFromNow);
            } else {
                // Time past! Ring the alarm.
                Intent ringingIntent = new Intent(this, RingingActivity.class);
                startActivityForResult(ringingIntent, RingingActivity.REQUEST_ALARM_RING);
            }
        } else {
            // Attempt to restore paused timer

            circleTimer.updateTimeLbl(pausedTimeRemaining);
            circleTimer.setPassedTime(totalTime - pausedTimeRemaining, true);
            // Set UI for paused state
            timerState = TIMER_STATE_PAUSED;
            setUIForPausedState();
            resetBtn.setVisibility(View.VISIBLE);
            skipBtn.setVisibility(View.VISIBLE);
        }

        return true;
    }

    /**
     * Starts the work or break timer
     * @param duration The number of milliseconds to run the timer for. If currently paused, this
     *                 is the remaining time.
     */
    @TargetApi(19)
    private void startTimer(long duration) {
        // Stop blinking the time and state labels
        timeLbl.clearAnimation();
        stateLbl.clearAnimation();

        // Show the "Reset" and "Skip" btns
        resetBtn.setVisibility(View.VISIBLE);
        skipBtn.setVisibility(View.VISIBLE);

        // Update the start / stop label
        startStopLbl.setText(R.string.stop);
        startStopLbl.setVisibility(View.VISIBLE);

        if (timerState == TIMER_STATE_PAUSED && circleTimer.getTotalTime() > 0) {
            // We're resuming from a paused state, so calculate how much time is remaining, based
            //  on the total time set in the circleTimer
            circleTimer.setPassedTime(circleTimer.getTotalTime() - duration, false);
        } else {
            circleTimer.setTotalTime(duration);
            circleTimer.setPassedTime(0, false);
            circleTimer.updateTimeLbl(duration);
            // Record the total duration, so we can resume if the activity is destroyed
            sharedPref.edit().putLong("schedTotalTime", duration).apply();
        }

        circleTimer.startIntervalAnimation();

        timerState = TIMER_STATE_RUNNING;

        // Schedule the alarm to go off
        PendingIntent pi = PendingIntent.getBroadcast(this, ALARM_MANAGER_REQUEST_CODE,
                new Intent(this, AlarmReceiver.class), PendingIntent.FLAG_UPDATE_CURRENT);
        long ringTime = SystemClock.elapsedRealtime() + duration;
        if (Build.VERSION.SDK_INT >= 19) {
            // API 19 needs setExact()
            alarmManager.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, ringTime, pi);
        } else {
            // APIs 1-18 use set()
            alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, ringTime, pi);
        }
        // Show the persistent notification
        AlarmNotifications.showUpcomingNotification(this, ringTime, getWorkState());

        // Record when the timer will ring and remove record of time remaining for the paused timer.
        // Save the scheduled ring time using Unix / epoch time, not elapsedRealtime, b/c that is
        //  reset on each boot.
        long timeFromNow = ringTime - SystemClock.elapsedRealtime();
        long ringUnixTime = System.currentTimeMillis() + timeFromNow;
        sharedPref.edit().putLong("schedRingTime", ringUnixTime).remove("pausedTimeRemaining").apply();
    }

    /**
     * Starts the work or break timer
     * Calculates the duration automatically, and then calls startTimer(duration)
     */
    private void startTimer() {
        long duration;
        if (timerState == TIMER_STATE_PAUSED) {
            // Timer already started before, so just get the remaining time
            duration = circleTimer.getRemainingTime();

            // Analytics
            if (mixpanel != null) {
                String eventName = getWorkState() == WORK_STATE_WORKING ? "Work timer resumed"
                        : "Break timer resumed";
                mixpanel.track(eventName, null);
            }

        } else {
            // Get duration from preferences, in minutes
            if (getWorkState() == WORK_STATE_WORKING) {
                duration = sharedPref.getInt(TimerDurationsSettingsFragment.KEY_WORK_DURATION, 0);
            } else {
                duration = sharedPref.getInt(TimerDurationsSettingsFragment.KEY_BREAK_DURATION, 0);
            }

            // Analytics
            if (mixpanel != null) {
                JSONObject props = new JSONObject();
                try {
                    props.put("Duration", duration);
                } catch (JSONException e) {
                    e.printStackTrace();
                }
                String eventName = getWorkState() == WORK_STATE_WORKING ? "Work timer started"
                        : "Break timer started";
                mixpanel.track(eventName, props);
            }

            duration *= 60000; // Multiply into milliseconds
        }

        startTimer(duration);
    }

    /**
     * Pauses the timer
     */
    private void pauseTimer() {
        // Record the remaining time, so we can restore if the activity is destroyed
        sharedPref.edit().putLong("pausedTimeRemaining", circleTimer.getRemainingTime()).apply();

        cancelScheduledAlarm();
        circleTimer.pauseIntervalAnimation();

        timerState = TIMER_STATE_PAUSED;

        setUIForPausedState();

        // Analytics
        if (mixpanel != null) {
            String eventName = getWorkState() == WORK_STATE_WORKING ? "Work timer paused" : "Break timer paused";
            mixpanel.track(eventName, null);
        }
    }

    /**
     * Sets the UI for the "paused" state
     */
    private void setUIForPausedState() {
        // Update the start / stop label
        startStopLbl.setText(R.string.resume);
        startStopLbl.setVisibility(View.VISIBLE);

        // Blink the time and state labels while paused
        Animation blinkAnim = AnimationUtils.loadAnimation(this, R.anim.blink);
        timeLbl.startAnimation(blinkAnim);
        stateLbl.startAnimation(blinkAnim);
    }

    /**
     * Snoozes the current timer for the duration stored in SharedPreferences
     *  (but show a toast if activity isn't open)
     */
    private void snoozeTimer() {
        setWorkState(sharedPref.getInt("workState", WORK_STATE_WORKING)); // Restore the timer state
        // Get duration from preferences, in minutes
        int snoozeDuration = sharedPref.getInt(TimerDurationsSettingsFragment.KEY_SNOOZE_DURATION, 0);
        // Snooze the timer. startTimer() also shows the upcoming notification, which will
        //  automatically hide the ringing notification, so we don't need to do it manually
        startTimer(snoozeDuration * 60000); // Multiply into milliseconds

        // Analytics
        if (mixpanel != null) {
            JSONObject props = new JSONObject();
            try {
                props.put("Duration", snoozeDuration);
            } catch (JSONException e) {
                e.printStackTrace();
            }
            String eventName = getWorkState() == WORK_STATE_WORKING ? "Work timer snoozed" : "Break timer snoozed";
            mixpanel.track(eventName, null);
        }
    }

    /**
     * Skips to the next timer state
     */
    private void skipToNextState() {
        int oldWorkState = getWorkState();
        // Record the state we're about to skip from, in case the user chooses to undo
        Bundle undoStateBundle = new Bundle();
        undoStateBundle.putLong("totalTime", circleTimer.getTotalTime());
        undoStateBundle.putLong("remainingTime", circleTimer.getRemainingTime());
        undoStateBundle.putInt("workState", oldWorkState);

        String toastMessage = getString(R.string.skip_toast);
        // Get duration from preferences, in minutes
        long duration;
        if (oldWorkState == WORK_STATE_WORKING) {
            // Means we're skipping to break
            duration = sharedPref.getInt(TimerDurationsSettingsFragment.KEY_BREAK_DURATION, 0);
            toastMessage += " break";
        } else {
            // Means we're skipping to work
            duration = sharedPref.getInt(TimerDurationsSettingsFragment.KEY_WORK_DURATION, 0);
            toastMessage += " work";
        }

        // Create and show the undo bar
        showUndoBar(toastMessage, undoStateBundle, new UndoBarController.UndoListener() {
            @Override
            public void onUndo(Parcelable parcelable) {
                if (parcelable == null)
                    return;

                // Extract the saved state from the Parcelable
                Bundle undoStateBundle = (Bundle) parcelable;
                long prevTotalTime = undoStateBundle.getLong("totalTime");
                long prevRemainingTime = undoStateBundle.getLong("remainingTime");
                int prevWorkState = undoStateBundle.getInt("workState");

                // Cause startTimer() to treat it like we're resuming (b/c we are)
                timerState = TIMER_STATE_PAUSED;
                setWorkState(prevWorkState);
                // Restore to the previous timer state, similar to how we restore a
                //  running timer from SharedPreferences in onCreate()
                circleTimer.setTotalTime(prevTotalTime);
                circleTimer.updateTimeLbl(prevRemainingTime);
                // Record the total duration, so we can resume if the activity is destroyed
                sharedPref.edit().putLong("schedTotalTime", prevTotalTime).apply();
                startTimer(prevRemainingTime);
                // Analytics
                if (mixpanel != null)
                    mixpanel.track("Skip undone", null);
            }
        });

        // Set the new state
        if (oldWorkState == WORK_STATE_WORKING)
            setWorkState(WORK_STATE_BREAKING);
        else
            setWorkState(WORK_STATE_WORKING);

        // We want to start the timer from scratch, not from a paused state
        timerState = TIMER_STATE_STOPPED;
        // Start the timer
        startTimer(duration * 60000); // Multiply into milliseconds

        // Analytics
        if (mixpanel != null) {
            JSONObject props = new JSONObject();
            try {
                props.put("Duration", duration);
            } catch (JSONException e) {
                e.printStackTrace();
            }
            String eventName = getWorkState() == WORK_STATE_WORKING ? "Skipped to work" : "Skipped to break";
            mixpanel.track(eventName, props);
        }
    }

    /**
     * Resets the CircleTimerView and reverts the Activity's UI to its initial state
     * @param isTimerComplete Whether the timer is complete
     */
    private void resetTimerUI(boolean isTimerComplete) {
        timerState = TIMER_STATE_STOPPED;

        // Reset the UI
        timeLbl.clearAnimation();
        stateLbl.clearAnimation();
        resetBtn.setVisibility(View.GONE);
        skipBtn.setVisibility(View.GONE);
        timeLbl.setText("");

        // Record the state we're about to reset from, in case the user chooses to undo
        Bundle undoStateBundle = new Bundle();
        if (isTimerComplete) {
            undoStateBundle.putLong("totalTime", 0);
            undoStateBundle.putLong("remainingTime", 0);
        } else {
            undoStateBundle.putLong("totalTime", circleTimer.getTotalTime());
            undoStateBundle.putLong("remainingTime", circleTimer.getRemainingTime());
        }
        undoStateBundle.putInt("workState", getWorkState());

        // Back to initial state
        setWorkState(WORK_STATE_WORKING);

        // Update the start / stop label
        startStopLbl.setText(R.string.start);
        startStopLbl.setVisibility(View.VISIBLE);

        circleTimer.stopIntervalAnimation();
        circleTimer.invalidate();

        // Remove record of total timer duration and the time remaining for the paused timer
        sharedPref.edit().remove("schedTotalTime").remove("pausedTimeRemaining").apply();

        // Create and show the undo bar
        showUndoBar(getString(R.string.reset_toast), undoStateBundle, new UndoBarController.UndoListener() {
            @Override
            public void onUndo(Parcelable parcelable) {
                if (parcelable == null)
                    return;

                // Extract the saved state from the Parcelable
                Bundle undoStateBundle = (Bundle) parcelable;
                long prevTotalTime = undoStateBundle.getLong("totalTime");
                long prevRemainingTime = undoStateBundle.getLong("remainingTime");
                int prevWorkState = undoStateBundle.getInt("workState");
                if (prevTotalTime > 0 && prevRemainingTime > 0) {
                    // Cause startTimer() to treat it like we're resuming (b/c we are)
                    timerState = TIMER_STATE_PAUSED;
                    setWorkState(prevWorkState);
                    // Restore to the previous timer state, similar to how we restore a
                    //  running timer from SharedPreferences in onCreate()
                    circleTimer.setTotalTime(prevTotalTime);
                    circleTimer.updateTimeLbl(prevRemainingTime);
                    // Record the total duration, so we can resume if the activity is destroyed
                    sharedPref.edit().putLong("schedTotalTime", prevTotalTime).apply();
                    startTimer(prevRemainingTime);
                } else {
                    // Means the timer was complete when resetTimerUI() was called, so we
                    //  need to start the timer from the beginning of the next state
                    if (prevWorkState == WORK_STATE_WORKING)
                        setWorkState(WORK_STATE_BREAKING);
                    else
                        setWorkState(WORK_STATE_WORKING);
                    startTimer();
                }
                // Analytics
                if (mixpanel != null)
                    mixpanel.track("Timer reset undone", null);
            }
        });
    }

    /**
     * Set the timer's work state and update the state label
     */
    private void setWorkState(int newState) {
        _workState = newState;

        // Update the state label
        if (_workState == WORK_STATE_WORKING)
            stateLbl.setText(R.string.state_working);
        else
            stateLbl.setText(R.string.state_breaking);

        // Save the work state to shared preferences
        sharedPref.edit().putInt("workState", _workState).apply();
    }

    /**
     * The current work state of the timer
     */
    private int getWorkState() {
        return _workState;
    }

    public static int getWorkState(Context context) {
        return PreferenceManager.getDefaultSharedPreferences(context).getInt("workState", WORK_STATE_WORKING);
    }

    /**
     * Cancels the alarm scheduled by startTimer()
     */
    private void cancelScheduledAlarm() {
        // Cancel the AlarmManager
        PendingIntent pendingIntent = PendingIntent.getBroadcast(this, ALARM_MANAGER_REQUEST_CODE,
                new Intent(this, AlarmReceiver.class), PendingIntent.FLAG_UPDATE_CURRENT);
        alarmManager.cancel(pendingIntent);
        // Hide the persistent notification
        AlarmNotifications.hideNotification(this);

        // Remove record of when the timer will ring
        sharedPref.edit().remove("schedRingTime").apply();
    }

    /**
     * Shows an UndoBar with a duration of 3500ms
     * @param message The message to display on the toast
     * @param token The Parcelable to be passed to the undo listener if the user pressed "Undo"
     * @param listener The UndoListener to be notified if the user presses "Undo"
     */
    private void showUndoBar(String message, Parcelable token, UndoBarController.UndoListener listener) {
        new UndoBarController.UndoBar(this).duration(3500).message(message).token(token).listener(listener).show();
    }

}