TimerFragment.java
/*
* Copyright 2016 the Cook-E development team
*
* This file is part of Cook-E.
*
* Cook-E 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.
*
* Cook-E 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 Cook-E. If not, see <http://www.gnu.org/licenses/>.
*/
package org.cook_e.cook_e.ui;
import android.app.Activity;
import android.app.Fragment;
import android.os.Bundle;
import android.os.CountDownTimer;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.TextView;
import org.cook_e.cook_e.R;
import org.cook_e.data.Objects;
import org.cook_e.data.Recipe;
import org.cook_e.data.Step;
import org.joda.time.Duration;
import org.joda.time.Period;
import org.joda.time.ReadableDuration;
import org.joda.time.format.PeriodFormat;
import org.joda.time.format.PeriodFormatter;
import org.joda.time.format.PeriodFormatterBuilder;
/**
* A fragment used in {@link org.cook_e.cook_e.CookActivity} that displays a timer for a
* simultaneous {@link org.cook_e.data.Step}
*
* Activities that use this fragment must implement the
* {@link org.cook_e.cook_e.ui.TimerFragment.StepFinishListener} interface.
*/
public class TimerFragment extends Fragment {
/**
* An interface for things that can be notified when a simultaneous step is finished
*/
public interface StepFinishListener {
/**
* Called when a step is finished.
*
* The current implementation calls this method when the timer runs out or when the user
* presses the done button.
*
* @param timerFragment the fragment associated with the completed step
* @param recipe the recipe that contains the step
* @param step the step that was completed
*/
void onStepFinished(TimerFragment timerFragment, Recipe recipe, Step step);
}
/**
* The interval between updates
*/
private static final Duration INTERVAL = Duration.millis(500);
/**
* Key for the step argument
*/
private static final String ARG_STEP = TimerFragment.class.getName() + ".ARG_STEP";
/**
* Key for the recipe argument
*/
private static final String ARG_RECIPE = TimerFragment.class.getName() + ".ARG_RECIPE";
/**
* Key used when saving state to store the remaining time
*/
private static final String KEY_TIME_REMAINING = TimerFragment.class.getName() + ".KEY_TIME_REMAINING";
/**
* The recipe that contains the step being timed
*/
private Recipe mRecipe;
/**
* The step being timed
*/
private Step mStep;
/**
* The remaining countdown time
*/
private ReadableDuration mRemainingTime;
/**
* The text view that displays the remaining time
*/
private TextView mTimerView;
/**
* The formatter used to format remaining time periods
*/
private final PeriodFormatter mFormatter;
public TimerFragment() {
// Required empty public constructor
mFormatter = new PeriodFormatterBuilder()
.printZeroAlways()
.minimumPrintedDigits(1)
.appendHours()
.appendLiteral(":")
.minimumPrintedDigits(2)
.appendMinutes()
.appendLiteral(":")
.appendSeconds()
.toFormatter();
}
/**
* Creates a fragment to act as a timer for a step
*
* @param recipe the recipe that contains the step
* @param step the step to time
* @return a TimerFragment for the provided step
* @throws NullPointerException if any argument is null
* @throws IllegalArgumentException if the step is not simultaneous or if the recipe does not
* contain the step
*/
public static TimerFragment newInstance(Recipe recipe, Step step) {
Objects.requireNonNull(recipe, "recipe must not be null");
Objects.requireNonNull(step, "step must not be null");
if (!recipe.getSteps().contains(step)) {
throw new IllegalArgumentException("Recipe does not contain step");
}
final Bundle args = new Bundle();
args.putParcelable(ARG_RECIPE, recipe);
args.putParcelable(ARG_STEP, step);
final TimerFragment fragment = new TimerFragment();
fragment.setArguments(args);
return fragment;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mRecipe = getArguments().getParcelable(ARG_RECIPE);
mStep = getArguments().getParcelable(ARG_STEP);
if (mRecipe == null) {
throw new IllegalStateException("No recipe argument");
}
if (mStep == null) {
throw new IllegalStateException("No step argument");
}
if (savedInstanceState != null) {
// Restore remaining time
mRemainingTime = (ReadableDuration) savedInstanceState.getSerializable(KEY_TIME_REMAINING);
} else {
// Get duration from step
mRemainingTime = mStep.getTime();
}
// Check parent
if (!(getActivity() instanceof StepFinishListener)) {
throw new IllegalStateException("The parent of this fragment must implement StepFinishListener");
}
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putSerializable(KEY_TIME_REMAINING, mRemainingTime.toDuration());
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
final View view = inflater.inflate(R.layout.fragment_timer, container, false);
mTimerView = (TextView) view.findViewById(R.id.timer_view);
// Set up description text
final TextView descriptionView = (TextView) view.findViewById(R.id.description_view);
descriptionView.setText(mStep.getDescription());
// Set up done button
final ImageButton doneButton = (ImageButton) view.findViewById(R.id.done_button);
doneButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// Notify
notifyStepCompleted();
}
});
// Start timer
new StepTimer(mRemainingTime).start();
return view;
}
/**
* Notifies the containing activity that the step has been completed
*/
private void notifyStepCompleted() {
// Activity may be null if the fragment was removed early
// (the timer cannot be canceled)
final Activity parent = getActivity();
if (parent != null) {
((StepFinishListener) parent).onStepFinished(this, mRecipe, mStep);
}
}
@Override
public void onStart() {
super.onStart();
}
private void updateTimer(ReadableDuration remainingTime) {
// Round the duration down to remove milliseconds
final Duration roundedTime = roundDownToSecond(remainingTime);
final Period remainingPeriod = roundedTime.toPeriod();
mTimerView.setText(mFormatter.print(remainingPeriod));
mRemainingTime = remainingTime;
}
/**
* Rounds a duration down to the nearest second
* @param duration the duration to round. Must not be null.
* @return a duration up to 1 second less than the provided duration, representing an
* integer number of seconds
*/
private static Duration roundDownToSecond(ReadableDuration duration) {
final long millis = duration.getMillis();
return Duration.millis(millis - (millis % 1000));
}
private class StepTimer extends CountDownTimer {
/**
* Creates a new StepTimer
*
* @param duration the duration to count for
*/
public StepTimer(ReadableDuration duration) {
super(duration.getMillis(), INTERVAL.getMillis());
}
@Override
public void onTick(long millisUntilFinished) {
updateTimer(Duration.millis(millisUntilFinished));
}
@Override
public void onFinish() {
updateTimer(Duration.ZERO);
notifyStepCompleted();
}
}
}