diff --git a/README.md b/README.md index 7ec6252e..fdd6b6ad 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ An android application created for /r/bodyweightfitness subreddit. Simple interface for accessing multiple exercise videos and up-to-date beginner routine with a handy timer helpful to finish the exercise.

- Nexus 5 Screenshot + Nexus 6 Screenshot

### LICENSE diff --git a/app/build.gradle b/app/build.gradle index 0443834f..88390b86 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -33,14 +33,14 @@ android { productFlavors { pro { applicationId 'io.mazur.fit.pro' - versionCode 100 - versionName "1.0.0" + versionCode 101 + versionName "1.0.1" } free { applicationId 'io.mazur.fit.free' - versionCode 100 - versionName "1.0.0" + versionCode 101 + versionName "1.0.1" } } @@ -52,6 +52,9 @@ android { } dependencies { + testCompile 'junit:junit:4.12' + testCompile 'org.mockito:mockito-core:1.9.5' + compile fileTree(dir: 'libs', include: ['*.jar']) compile 'com.android.support:support-v4:22.2.0' @@ -60,7 +63,7 @@ dependencies { compile 'com.android.support:design:22.2.0' compile 'com.google.code.gson:gson:2.3.1' - + compile 'com.github.mazurio:glacier:0.0.3' compile 'net.danlew:android.joda:2.7.1' compile 'commons-io:commons-io:2.4' diff --git a/app/src/main/java/io/mazur/fit/App.java b/app/src/main/java/io/mazur/fit/App.java index 3d69d9e7..d4fbf7b9 100644 --- a/app/src/main/java/io/mazur/fit/App.java +++ b/app/src/main/java/io/mazur/fit/App.java @@ -4,9 +4,9 @@ import android.content.Context; import com.crashlytics.android.Crashlytics; - import net.danlew.android.joda.JodaTimeAndroid; +import io.mazur.glacier.Glacier; import io.fabric.sdk.android.Fabric; public class App extends Application { @@ -16,7 +16,9 @@ public class App extends Application { public void onCreate() { super.onCreate(); - JodaTimeAndroid.init(this); + Glacier.init(getApplicationContext()); + JodaTimeAndroid.init(getApplicationContext()); + if(!BuildConfig.DEBUG) { Fabric.with(this, new Crashlytics()); } diff --git a/app/src/main/java/io/mazur/fit/Constants.java b/app/src/main/java/io/mazur/fit/Constants.java index 814db934..d64063a7 100644 --- a/app/src/main/java/io/mazur/fit/Constants.java +++ b/app/src/main/java/io/mazur/fit/Constants.java @@ -1,5 +1,7 @@ package io.mazur.fit; public class Constants { + public static final String PREFERENCE_PLAY_SOUND_WHEN_TIMER_STOPS = "PREFERENCE_PLAY_SOUND_WHEN_TIMER_STOPS"; + public static final String PREFERENCE_KEEP_SCREEN_ON = "PREFERENCE_KEEP_SCREEN_ON"; public static final String PREFERENCE_TIMER_KEY = "PREFERENCE_TIMER_KEY"; } diff --git a/app/src/main/java/io/mazur/fit/adapter/RoutineAdapter.java b/app/src/main/java/io/mazur/fit/adapter/RoutineAdapter.java index 1c32fb3c..d0c70162 100644 --- a/app/src/main/java/io/mazur/fit/adapter/RoutineAdapter.java +++ b/app/src/main/java/io/mazur/fit/adapter/RoutineAdapter.java @@ -8,9 +8,14 @@ import butterknife.ButterKnife; import butterknife.InjectView; + import io.mazur.fit.R; +import io.mazur.fit.model.Category; import io.mazur.fit.model.Routine; +import io.mazur.fit.model.Exercise; +import io.mazur.fit.model.LinkedRoutine; import io.mazur.fit.model.RoutineType; +import io.mazur.fit.model.Section; import io.mazur.fit.stream.RoutineStream; public class RoutineAdapter extends RecyclerView.Adapter { @@ -35,7 +40,7 @@ public RoutinePresenter onCreateViewHolder(ViewGroup parent, int viewType) { @Override public void onBindViewHolder(RoutinePresenter holder, int position) { - holder.onBindView(mRoutine.getPartRoutines().get(position)); + holder.onBindView(mRoutine.getLinkedRoutine().get(position)); } @Override @@ -44,12 +49,12 @@ public int getItemCount() { return 0; } - return mRoutine.getSize(); + return mRoutine.getLinkedRoutine().size(); } @Override public int getItemViewType(int position) { - return mRoutine.getPartRoutines().get(position).getType().ordinal(); + return mRoutine.getLinkedRoutine().get(position).getType().ordinal(); } public void setRoutine(Routine routine) { @@ -58,16 +63,17 @@ public void setRoutine(Routine routine) { notifyDataSetChanged(); } - public abstract class RoutinePresenter extends RecyclerView.ViewHolder { + public abstract class RoutinePresenter extends RecyclerView.ViewHolder { public RoutinePresenter(View itemView) { super(itemView); + ButterKnife.inject(this, itemView); } - public abstract void onBindView(Routine.PartRoutine partRoutine); + public abstract void onBindView(T linkedRoutine); } - public class RoutineCategoryPresenter extends RoutinePresenter { + public class RoutineCategoryPresenter extends RoutinePresenter { @InjectView(R.id.routine_category_text_view) TextView mRoutineCategoryTextView; public RoutineCategoryPresenter(View itemView) { @@ -75,25 +81,28 @@ public RoutineCategoryPresenter(View itemView) { } @Override - public void onBindView(Routine.PartRoutine partRoutine) { - mRoutineCategoryTextView.setText(partRoutine.getTitle()); + public void onBindView(Category category) { + mRoutineCategoryTextView.setText(category.getTitle()); } } - public class RoutineSectionPresenter extends RoutinePresenter { + public class RoutineSectionPresenter extends RoutinePresenter
{ @InjectView(R.id.routine_section_text_view) TextView mRoutineSectionTextView; + @InjectView(R.id.routine_section_description_text_view) TextView mRoutineSectionDescriptionTextView; public RoutineSectionPresenter(View itemView) { super(itemView); } @Override - public void onBindView(Routine.PartRoutine partRoutine) { - mRoutineSectionTextView.setText(partRoutine.getTitle()); + public void onBindView(Section section) { + mRoutineSectionTextView.setText(section.getTitle()); + mRoutineSectionDescriptionTextView.setText(section.getDescription()); } } - public class RoutineExercisePresenter extends RoutinePresenter { + public class RoutineExercisePresenter extends RoutinePresenter { + @InjectView(R.id.routine_exercise_level_text_view) TextView mRoutineExerciseLevelTextView; @InjectView(R.id.routine_exercise_text_view) TextView mRoutineExerciseTextView; public RoutineExercisePresenter(View itemView) { @@ -101,23 +110,30 @@ public RoutineExercisePresenter(View itemView) { } @Override - public void onBindView(Routine.PartRoutine partRoutine) { - mRoutineExerciseTextView.setText(partRoutine.getTitle()); + public void onBindView(Exercise exercise) { + if(exercise.getLevel() == null) { + mRoutineExerciseLevelTextView.setVisibility(View.GONE); + } else { + mRoutineExerciseLevelTextView.setVisibility(View.VISIBLE); + mRoutineExerciseLevelTextView.setText(exercise.getLevel()); + } + + mRoutineExerciseTextView.setText(exercise.getTitle()); - itemView.setOnClickListener(new OnItemClickListener(partRoutine)); + itemView.setOnClickListener(new OnItemClickListener(exercise)); } } public class OnItemClickListener implements View.OnClickListener { - private Routine.PartRoutine mPartRoutine; + private LinkedRoutine mLinkedRoutine; - public OnItemClickListener(Routine.PartRoutine partRoutine) { - mPartRoutine = partRoutine; + public OnItemClickListener(LinkedRoutine linkedRoutine) { + mLinkedRoutine = linkedRoutine; } @Override public void onClick(View v) { - RoutineStream.getInstance().setExercise(mPartRoutine); + RoutineStream.getInstance().setExercise((Exercise) mLinkedRoutine); } } } diff --git a/app/src/main/java/io/mazur/fit/model/ActivityState.java b/app/src/main/java/io/mazur/fit/model/ActivityState.java new file mode 100644 index 00000000..3cb95cda --- /dev/null +++ b/app/src/main/java/io/mazur/fit/model/ActivityState.java @@ -0,0 +1,7 @@ +package io.mazur.fit.model; + +public enum ActivityState { + OnCreate, + OnPause, + OnStop +} diff --git a/app/src/main/java/io/mazur/fit/model/Category.java b/app/src/main/java/io/mazur/fit/model/Category.java new file mode 100644 index 00000000..4686fc55 --- /dev/null +++ b/app/src/main/java/io/mazur/fit/model/Category.java @@ -0,0 +1,32 @@ +package io.mazur.fit.model; + +import java.io.Serializable; +import java.util.ArrayList; + +public class Category extends LinkedRoutine implements Serializable { + private String mTitle; + + private ArrayList
mSections = new ArrayList<>(); + + public Category(String title) { + mTitle = title; + } + + public String getTitle() { + return mTitle; + } + + public RoutineType getType() { + return RoutineType.CATEGORY; + } + + public void insertSection(Section section) { + section.setCategory(this); + + mSections.add(section); + } + + public ArrayList
getSections() { + return mSections; + } +} diff --git a/app/src/main/java/io/mazur/fit/model/Exercise.java b/app/src/main/java/io/mazur/fit/model/Exercise.java new file mode 100644 index 00000000..3d91b4de --- /dev/null +++ b/app/src/main/java/io/mazur/fit/model/Exercise.java @@ -0,0 +1,83 @@ +package io.mazur.fit.model; + +import java.io.Serializable; + +public class Exercise extends LinkedRoutine implements Serializable { + private String mId; + private String mLevel; + private String mTitle; + private String mDescription; + + private Category mCategory; + private Section mSection; + + private Exercise mPrevious; + private Exercise mNext; + + public Exercise(String id, String level, String title, String description) { + mId = id; + mLevel = level; + mTitle = title; + mDescription = description; + } + + public String getId() { + return mId; + } + + public String getLevel() { + return mLevel; + } + + public String getTitle() { + return mTitle; + } + + public RoutineType getType() { + return RoutineType.EXERCISE; + } + + public String getDescription() { + return mDescription; + } + + public void setCategory(Category category) { + mCategory = category; + } + + public Category getCategory() { + return mCategory; + } + + public void setSection(Section section) { + mSection = section; + } + + public Section getSection() { + return mSection; + } + + public boolean isPrevious() { + return mPrevious != null; + } + + public void setPrevious(Exercise previous) { + mPrevious = previous; + } + + public Exercise getPrevious() { + return mPrevious; + } + + public boolean isNext() { + return mNext != null; + } + + public void setNext(Exercise next) { + mNext = next; + } + + public Exercise getNext() { + return mNext; + } +} \ No newline at end of file diff --git a/app/src/main/java/io/mazur/fit/model/JSONRoutine.java b/app/src/main/java/io/mazur/fit/model/JSONRoutine.java new file mode 100644 index 00000000..aa649bb4 --- /dev/null +++ b/app/src/main/java/io/mazur/fit/model/JSONRoutine.java @@ -0,0 +1,81 @@ +package io.mazur.fit.model; + +import com.google.gson.annotations.SerializedName; + +import java.util.ArrayList; + +public class JSONRoutine { + @SerializedName("routine") + private ArrayList mJSONLinkedRoutines = new ArrayList<>(); + + public ArrayList getPartRoutines() { + return mJSONLinkedRoutines; + } + + public int getSize() { + if(mJSONLinkedRoutines != null) { + return mJSONLinkedRoutines.size(); + } + + return 0; + } + + public class JSONLinkedRoutine { + @SerializedName("id") + private String mId; + + @SerializedName("level") + private String mLevel; + + @SerializedName("title") + private String mTitle; + + @SerializedName("description") + private String mDescription; + + @SerializedName("type") + private String mType; + + /** + * Mode can be one of those: All, Pick, Levels + */ + @SerializedName("mode") + private String mMode; + + public String getId() { + return mId; + } + + public String getLevel() { + return mLevel; + } + + public String getTitle() { + return mTitle; + } + + public String getDescription() { + return mDescription; + } + + public RoutineType getType() { + if(mType.matches("category")) { + return RoutineType.CATEGORY; + } else if(mType.matches("section")) { + return RoutineType.SECTION; + } else { + return RoutineType.EXERCISE; + } + } + + public SectionMode getMode() { + if(mMode.matches("all")) { + return SectionMode.ALL; + } else if(mMode.matches("pick")) { + return SectionMode.PICK; + } else { + return SectionMode.LEVELS; + } + } + } +} diff --git a/app/src/main/java/io/mazur/fit/model/LinkedRoutine.java b/app/src/main/java/io/mazur/fit/model/LinkedRoutine.java new file mode 100644 index 00000000..dbbec545 --- /dev/null +++ b/app/src/main/java/io/mazur/fit/model/LinkedRoutine.java @@ -0,0 +1,8 @@ +package io.mazur.fit.model; + +import java.io.Serializable; + +public abstract class LinkedRoutine implements Serializable { + public abstract String getTitle(); + public abstract RoutineType getType(); +} diff --git a/app/src/main/java/io/mazur/fit/model/Routine.java b/app/src/main/java/io/mazur/fit/model/Routine.java index 34220e12..873e27b6 100644 --- a/app/src/main/java/io/mazur/fit/model/Routine.java +++ b/app/src/main/java/io/mazur/fit/model/Routine.java @@ -1,105 +1,184 @@ package io.mazur.fit.model; -import com.google.gson.annotations.SerializedName; - +import java.io.Serializable; import java.util.ArrayList; -import java.util.LinkedList; -public class Routine { - @SerializedName("routine") - private ArrayList mPartRoutines = new ArrayList<>(); +import io.mazur.glacier.Glacier; + +public class Routine implements Serializable { + /** + * Categories are treated as top level parent for the sections and exercises. + */ + private ArrayList mCategories = new ArrayList<>(); + + /** + * All sections put together inside a list. + */ + private ArrayList
mSections = new ArrayList<>(); + + /** + * All exercises put together inside a list, used to link exercises after changes. + */ + private ArrayList mExercises = new ArrayList<>(); + + /** + * Linked exercises for switching between them. This could and should be implemented as an + * independent screen manager later which allows linking different views together + * (e.g. rest view or summary). + */ + private ArrayList mLinkedExercises = new ArrayList<>(); + + /** + * Linked routine for displaying views in the recycler view as we can just request + * type of the object. + */ + private ArrayList mLinkedRoutine = new ArrayList<>(); + + public Routine(JSONRoutine JSONRoutine) { + Category currentCategory = null; + Section currentSection = null; + Exercise currentExercise = null; + + for(io.mazur.fit.model.JSONRoutine.JSONLinkedRoutine JSONLinkedRoutine : JSONRoutine.getPartRoutines()) { + /** + * Update categories in both lists. + */ + if(JSONLinkedRoutine.getType() == RoutineType.CATEGORY) { + currentCategory = new Category(JSONLinkedRoutine.getTitle()); + + mCategories.add(currentCategory); + mLinkedRoutine.add(currentCategory); + } - public ArrayList getPartRoutines() { - return mPartRoutines; - } + /** + * Updated sections for each category and add into linked routine. + */ + else if(JSONLinkedRoutine.getType() == RoutineType.SECTION) { + currentSection = new Section( + JSONLinkedRoutine.getTitle(), + JSONLinkedRoutine.getDescription(), + JSONLinkedRoutine.getMode()); - public PartRoutine getFirstExercise() { - for(PartRoutine partRoutine : getPartRoutines()) { - if(partRoutine.getType() == RoutineType.EXERCISE) { - return partRoutine; - } - } + currentCategory.insertSection(currentSection); - return new PartRoutine(); - } + mSections.add(currentSection); + mLinkedRoutine.add(currentSection); + } - public int getSize() { - if(mPartRoutines != null) { - return mPartRoutines.size(); + /** + * Update exercises for each section, link them together. + */ + else if(JSONLinkedRoutine.getType() == RoutineType.EXERCISE) { + Exercise exercise = new Exercise( + JSONLinkedRoutine.getId(), + JSONLinkedRoutine.getLevel(), + JSONLinkedRoutine.getTitle(), + JSONLinkedRoutine.getDescription()); + + /** + * Add exercise into a section. + */ + currentSection.insertExercise(exercise); + + /** + * List contains all exercises, no matter what level. + */ + mExercises.add(exercise); + + /** + * Add only first level of exercise when linking. + */ + if(currentSection.getSectionMode() == SectionMode.LEVELS || currentSection.getSectionMode() == SectionMode.PICK) { + String currentExerciseId = Glacier.get(currentSection.getTitle(), String.class); + + if(currentExerciseId != null) { + if(exercise.getId().matches(currentExerciseId)) { + mLinkedExercises.add(exercise); + + exercise.setPrevious(currentExercise); + + if(currentExercise != null) { + currentExercise.setNext(exercise); + } + + currentExercise = exercise; + currentSection.setCurrentLevel(exercise); + } + } else { + if(currentSection.getExercises().size() == 1) { + mLinkedExercises.add(exercise); + + exercise.setPrevious(currentExercise); + + if(currentExercise != null) { + currentExercise.setNext(exercise); + } + + currentExercise = exercise; + } + } + } else { + exercise.setPrevious(currentExercise); + + if(currentExercise != null) { + currentExercise.setNext(exercise); + } + + currentExercise = exercise; + + mLinkedExercises.add(exercise); + } + + /** + * Linked Routine contains all exercises. + */ + mLinkedRoutine.add(exercise); + } } - - return 0; } - public class PartRoutine { - @SerializedName("id") - private String mId; - - @SerializedName("title") - private String mTitle; - - @SerializedName("description") - private String mDescription; - - @SerializedName("type") - private String mType; - - private PartRoutine mPrevious; + public ArrayList getCategories() { + return mCategories; + } - private PartRoutine mNext; + public ArrayList
getSections() { + return mSections; + } - public String getId() { - return mId; - } + public ArrayList getExercises() { + return mExercises; + } - public String getTitle() { - return mTitle; - } + public ArrayList getLinkedExercises() { + return mLinkedExercises; + } - public String getDescription() { - return mDescription; - } + /** + * Set level of the exercise in given section. + */ + public void setLevel(Exercise exercise, int level) { + Exercise currentSectionExercise = exercise.getSection().getCurrentExercise(); - public RoutineType getType() { - if(mType.matches("category")) { - return RoutineType.CATEGORY; - } else if(mType.matches("section")) { - return RoutineType.SECTION; - } else { - return RoutineType.EXERCISE; + if(currentSectionExercise != exercise) { + if(currentSectionExercise.getPrevious() != null) { + currentSectionExercise.getPrevious().setNext(exercise); } - } - - public void setPrevious(PartRoutine mPrevious) { - this.mPrevious = mPrevious; - } - - public PartRoutine getPrevious() { - return mPrevious; - } - public void setNext(PartRoutine mNext) { - this.mNext = mNext; - } - - public PartRoutine getNext() { - return mNext; - } - - public boolean isPrevious() { - if(mPrevious == null) { - return false; + if(currentSectionExercise.getNext() != null) { + currentSectionExercise.getNext().setPrevious(exercise); } - return true; - } + exercise.setPrevious(currentSectionExercise.getPrevious()); + exercise.setNext(currentSectionExercise.getNext()); - public boolean isNext() { - if(mNext == null) { - return false; - } + exercise.getSection().setCurrentLevel(level); - return true; + currentSectionExercise.setPrevious(null); + currentSectionExercise.setNext(null); } } + + public ArrayList getLinkedRoutine() { + return mLinkedRoutine; + } } diff --git a/app/src/main/java/io/mazur/fit/model/RoutineType.java b/app/src/main/java/io/mazur/fit/model/RoutineType.java index d6efaf6c..86419050 100644 --- a/app/src/main/java/io/mazur/fit/model/RoutineType.java +++ b/app/src/main/java/io/mazur/fit/model/RoutineType.java @@ -1,6 +1,8 @@ package io.mazur.fit.model; -public enum RoutineType { +import java.io.Serializable; + +public enum RoutineType implements Serializable { CATEGORY, SECTION, EXERCISE; diff --git a/app/src/main/java/io/mazur/fit/model/Section.java b/app/src/main/java/io/mazur/fit/model/Section.java new file mode 100644 index 00000000..9ca37983 --- /dev/null +++ b/app/src/main/java/io/mazur/fit/model/Section.java @@ -0,0 +1,94 @@ +package io.mazur.fit.model; + +import java.io.Serializable; +import java.util.ArrayList; + +public class Section extends LinkedRoutine implements Serializable { + private String mTitle; + private String mDescription; + private SectionMode mSectionMode; + + private Category mCategory; + private int mCurrentLevel = 0; + + private ArrayList mExercises = new ArrayList<>(); + + public Section(String title, String description, SectionMode sectionMode) { + mTitle = title; + mDescription = description; + mSectionMode = sectionMode; + } + + public String getTitle() { + return mTitle; + } + + public String getDescription() { + return mDescription; + } + + public RoutineType getType() { + return RoutineType.SECTION; + } + + public SectionMode getSectionMode() { + return mSectionMode; + } + + public void setCategory(Category category) { + mCategory = category; + } + + public Category getCategory() { + return mCategory; + } + + public void setCurrentLevel(int level) { + if(getSectionMode() == SectionMode.LEVELS || getSectionMode() == SectionMode.PICK) { + if(level < 0 || level >= getExercises().size()) { + return; + } + + mCurrentLevel = level; + } + } + + public void setCurrentLevel(Exercise exercise) { + int current = 0; + for(Exercise ex : getExercises()) { + if(ex.equals(exercise)) { + mCurrentLevel = current; + return; + } + + current++; + } + } + + public int getCurrentLevel() { + return mCurrentLevel; + } + + public int getAvailableLevels() { + if(getSectionMode() == SectionMode.LEVELS || getSectionMode() == SectionMode.PICK) { + return getExercises().size(); + } else { + return 0; + } + } + + public void insertExercise(Exercise exercise) { + exercise.setCategory(mCategory); + exercise.setSection(this); + + mExercises.add(exercise); + } + + public Exercise getCurrentExercise() { + return mExercises.get(mCurrentLevel); + } + + public ArrayList getExercises() { + return mExercises; + } +} \ No newline at end of file diff --git a/app/src/main/java/io/mazur/fit/model/SectionMode.java b/app/src/main/java/io/mazur/fit/model/SectionMode.java new file mode 100644 index 00000000..464564b9 --- /dev/null +++ b/app/src/main/java/io/mazur/fit/model/SectionMode.java @@ -0,0 +1,15 @@ +package io.mazur.fit.model; + +import java.io.Serializable; + +public enum SectionMode implements Serializable { + PICK, + ALL, + LEVELS; + + private static SectionMode[] values = SectionMode.values(); + + public static SectionMode fromInt(int value) { + return values[value]; + } +} \ No newline at end of file diff --git a/app/src/main/java/io/mazur/fit/model/TimerState.java b/app/src/main/java/io/mazur/fit/model/TimerState.java index 1fae82ea..94edafac 100644 --- a/app/src/main/java/io/mazur/fit/model/TimerState.java +++ b/app/src/main/java/io/mazur/fit/model/TimerState.java @@ -2,5 +2,6 @@ public enum TimerState { STARTED, - PAUSED + PAUSED, + FORCE_STOPPED } diff --git a/app/src/main/java/io/mazur/fit/presenter/TimerPresenter.java b/app/src/main/java/io/mazur/fit/presenter/TimerPresenter.java index ca99212b..09fee967 100644 --- a/app/src/main/java/io/mazur/fit/presenter/TimerPresenter.java +++ b/app/src/main/java/io/mazur/fit/presenter/TimerPresenter.java @@ -1,6 +1,7 @@ package io.mazur.fit.presenter; import android.app.TimePickerDialog; +import android.media.MediaPlayer; import android.os.CountDownTimer; import android.view.View; @@ -12,7 +13,9 @@ import io.mazur.fit.R; import io.mazur.fit.model.DialogState; import io.mazur.fit.model.TimerState; +import io.mazur.fit.stream.ActivityStream; import io.mazur.fit.stream.RoutineStream; +import io.mazur.fit.utils.Logger; import io.mazur.fit.utils.PreferenceUtil; import io.mazur.fit.view.TimerView; @@ -39,20 +42,47 @@ public class TimerPresenter implements Serializable { public void onCreateView(TimerView timerView) { mTimerView = timerView; - RoutineStream.getInstance().getExerciseObservable().subscribe(routine -> { - if(routine.isPrevious()) { + if(mTimerState == TimerState.FORCE_STOPPED) { + mTimerState = TimerState.STARTED; + } + + /** + * Allows to cancel the timer when activity is paused. + */ + ActivityStream.getInstance().getObservable().subscribe(activityState -> { + switch(mTimerState) { + case STARTED: { + mTimerState = TimerState.FORCE_STOPPED; + + mTimerView.getStartStopTimerButton().setImageDrawable( + mTimerView.getResources().getDrawable(R.drawable.ic_play) + ); + + mTimeInMillis = mTime.getMillis(); + + if(mCountDownTimer != null) { + mCountDownTimer.cancel(); + } + + break; + } + } + }); + + RoutineStream.getInstance().getExerciseObservable().subscribe(exercise -> { + if(exercise.isPrevious()) { mTimerView.getPrevExerciseButton().setVisibility(View.VISIBLE); mTimerView.getPrevExerciseButton().setOnClickListener(v -> { - RoutineStream.getInstance().setExercise(routine.getPrevious()); + RoutineStream.getInstance().setExercise(exercise.getPrevious()); }); } else { mTimerView.getPrevExerciseButton().setVisibility(View.INVISIBLE); } - if(routine.isNext()) { + if(exercise.isNext()) { mTimerView.getNextExerciseButton().setVisibility(View.VISIBLE); mTimerView.getNextExerciseButton().setOnClickListener(v -> { - RoutineStream.getInstance().setExercise(routine.getNext()); + RoutineStream.getInstance().setExercise(exercise.getNext()); }); } else { mTimerView.getNextExerciseButton().setVisibility(View.INVISIBLE); @@ -65,6 +95,13 @@ public void onCreateView(TimerView timerView) { long minutes = ONE_MINUTE * hourOfDay; long seconds = ONE_SECOND * minute; + /** + * If user chooses 0 minutes and 0 seconds, let's set default value to 15 seconds. + */ + if(minutes == 0 && seconds <= 15000) { + seconds = 15000; + } + mTimerState = TimerState.PAUSED; mTimeInMillis = minutes + seconds; mTime = new DateTime(mTimeInMillis); @@ -81,10 +118,9 @@ public void onCreateView(TimerView timerView) { if(mCountDownTimer != null) { mCountDownTimer.cancel(); } - - // set those (chosen values) in prefences using PreferenceUtil.getInstance() }, mTime.getMinuteOfHour(), mTime.getSecondOfMinute(), true); + mTimePickerDialog.setOnDismissListener(d -> mDialogState = DialogState.HIDDEN); mTimePickerDialog.setOnCancelListener(d -> mDialogState = DialogState.HIDDEN); RoutineStream.getInstance().getExerciseChangedObservable().subscribe(exercise -> { @@ -104,6 +140,7 @@ public void onCreateView(TimerView timerView) { mTimerView.getIncreaseTimerButton().setOnClickListener(v -> { switch(mTimerState) { + case FORCE_STOPPED: case PAUSED: { mTimerState = TimerState.PAUSED; mTimeInMillis = mTimeInMillis + (ONE_SECOND * 5); @@ -143,6 +180,7 @@ public void onCreateView(TimerView timerView) { mTimerView.getStartStopTimerButton().setOnClickListener(v -> { switch(mTimerState) { + case FORCE_STOPPED: case PAUSED: { mTimerState = TimerState.STARTED; @@ -180,6 +218,7 @@ public void onCreateView(TimerView timerView) { mTimerView.getRestartTimerButton().setOnClickListener(v -> restartTimer()); switch(mTimerState) { + case FORCE_STOPPED: case PAUSED: { mTimerView.getStartStopTimerButton().setImageDrawable( mTimerView.getResources().getDrawable(R.drawable.ic_play) @@ -243,6 +282,12 @@ public void onFinish() { mTimerView.getTimerMinutesTextView().setText(DateTimeFormat.forPattern("mm").print(mTime)); mTimerView.getTimerSecondsTextView().setText(DateTimeFormat.forPattern("ss").print(mTime)); + + if(PreferenceUtil.getInstance().playSoundWhenTimerStops()) { + MediaPlayer p = MediaPlayer.create(mTimerView.getContext(), R.raw.finished); + p.setLooping(false); + p.start(); + } } }; } diff --git a/app/src/main/java/io/mazur/fit/stream/ActivityStream.java b/app/src/main/java/io/mazur/fit/stream/ActivityStream.java new file mode 100644 index 00000000..659b7867 --- /dev/null +++ b/app/src/main/java/io/mazur/fit/stream/ActivityStream.java @@ -0,0 +1,29 @@ +package io.mazur.fit.stream; + +import io.mazur.fit.model.ActivityState; +import rx.Observable; +import rx.subjects.PublishSubject; + +public class ActivityStream { + private static ActivityStream sInstance; + + private final PublishSubject mActivitySubject = PublishSubject.create(); + + private ActivityStream() {} + + public static ActivityStream getInstance() { + if(sInstance == null) { + sInstance = new ActivityStream(); + } + + return sInstance; + } + + public void setActivityState(ActivityState activityState) { + mActivitySubject.onNext(activityState); + } + + public Observable getObservable() { + return mActivitySubject; + } +} diff --git a/app/src/main/java/io/mazur/fit/stream/RoutineStream.java b/app/src/main/java/io/mazur/fit/stream/RoutineStream.java index 7d29b680..fe975806 100644 --- a/app/src/main/java/io/mazur/fit/stream/RoutineStream.java +++ b/app/src/main/java/io/mazur/fit/stream/RoutineStream.java @@ -7,11 +7,15 @@ import java.io.IOException; import io.mazur.fit.App; +import io.mazur.fit.Constants; import io.mazur.fit.R; import io.mazur.fit.model.Routine; -import io.mazur.fit.model.RoutineType; +import io.mazur.fit.model.Exercise; +import io.mazur.fit.model.JSONRoutine; import io.mazur.fit.utils.Logger; +import io.mazur.glacier.Duration; +import io.mazur.glacier.Glacier; import rx.Observable; import rx.Subscriber; import rx.android.schedulers.AndroidSchedulers; @@ -21,45 +25,31 @@ public class RoutineStream { private static RoutineStream sInstance; private Routine mRoutine; - private Routine.PartRoutine mExercise; + private Exercise mExercise; private final PublishSubject mRoutineSubject = PublishSubject.create(); - private final PublishSubject mExerciseSubject = PublishSubject.create(); + private final PublishSubject mExerciseSubject = PublishSubject.create(); private RoutineStream() { + /** + * We should save levels only, not the whole object + * as we will never get any updates when JSON is udpated. + */ try { - mRoutine = new Gson().fromJson(IOUtils.toString(App.getContext() - .getResources() - .openRawResource(R.raw.beginner_routine) - ), Routine.class); - - /** - * TODO: Link Routine, I don't like how this is done at the moment. - * TODO: There is definitely a better way. - * - * Allows to link objects inside the Array List together so we can have previous - * and next buttons inside the application. - */ - Routine.PartRoutine prev = null; - for(Routine.PartRoutine partRoutine : mRoutine.getPartRoutines()) { - if(partRoutine.getType() == RoutineType.EXERCISE) { - if(prev != null) { - partRoutine.setPrevious(prev); - - prev.setNext(partRoutine); - } - - prev = partRoutine; - } - } + JSONRoutine jsonRoutine = new Gson().fromJson(IOUtils.toString(App.getContext() + .getResources() + .openRawResource(R.raw.beginner_routine) + ), JSONRoutine.class); + + mRoutine = new Routine(jsonRoutine); + } catch(IOException e) { + Logger.e("Exception when loading Beginner Routine from JSON file: " + e.getMessage()); + } - mExercise = mRoutine.getFirstExercise(); + mExercise = mRoutine.getLinkedExercises().get(0); - mRoutineSubject.onNext(mRoutine); - mExerciseSubject.onNext(mExercise); - } catch (IOException e) { - Logger.e("Exception when loading Beginner Routine from JSON file: " + e); - } + mRoutineSubject.onNext(mRoutine); + mExerciseSubject.onNext(mExercise); } public static RoutineStream getInstance() { @@ -84,22 +74,31 @@ public void call(Subscriber subscriber) { return Observable.merge(mRoutineSubject, routineObservable); } - public void setExercise(Routine.PartRoutine exercise) { + public void setExercise(Exercise exercise) { mExercise = exercise; mExerciseSubject.onNext(exercise); } - public Observable getExerciseChangedObservable() { + public void setLevel(Exercise exercise, int level) { + mRoutine.setLevel(exercise, level); + setExercise(exercise); + + // We save our current exercise for given section + // TODO: This should use section id (not title). + Glacier.put(exercise.getSection().getTitle(), exercise.getSection().getCurrentExercise().getId()); + } + + public Observable getExerciseChangedObservable() { return mExerciseSubject; } /** * @return Observable that allows to subscribe when only exercise has changed. */ - public Observable getExerciseObservable() { - Observable exerciseObservable = Observable.create(new Observable.OnSubscribe() { + public Observable getExerciseObservable() { + Observable exerciseObservable = Observable.create(new Observable.OnSubscribe() { @Override - public void call(Subscriber subscriber) { + public void call(Subscriber subscriber) { subscriber.onNext(mExercise); } }).observeOn(AndroidSchedulers.mainThread()).publish().refCount(); diff --git a/app/src/main/java/io/mazur/fit/ui/MainActivity.java b/app/src/main/java/io/mazur/fit/ui/MainActivity.java index 95c16c8b..01799373 100644 --- a/app/src/main/java/io/mazur/fit/ui/MainActivity.java +++ b/app/src/main/java/io/mazur/fit/ui/MainActivity.java @@ -1,9 +1,10 @@ package io.mazur.fit.ui; import android.content.Intent; +import android.content.SharedPreferences; import android.content.res.Configuration; +import android.net.Uri; import android.os.Bundle; -import android.support.design.widget.TabLayout; import android.support.v4.widget.DrawerLayout; import android.support.v7.app.ActionBar; import android.support.v7.app.AppCompatActivity; @@ -11,15 +12,21 @@ import android.view.Menu; import android.view.MenuItem; +import android.view.WindowManager; import io.mazur.fit.R; import io.mazur.fit.fragment.NavigationDrawerFragment; +import io.mazur.fit.model.ActivityState; +import io.mazur.fit.model.Exercise; +import io.mazur.fit.model.SectionMode; +import io.mazur.fit.stream.ActivityStream; import io.mazur.fit.stream.RoutineStream; -import io.mazur.fit.utils.Logger; -import rx.Subscription; +import io.mazur.fit.utils.PreferenceUtil; +import io.mazur.fit.view.ProgressDialog; -public class MainActivity extends AppCompatActivity { +public class MainActivity extends AppCompatActivity implements SharedPreferences.OnSharedPreferenceChangeListener { private NavigationDrawerFragment mNavigationDrawerFragment; + private Exercise mExercise; @Override protected void onCreate(Bundle savedInstanceState) { @@ -29,8 +36,29 @@ protected void onCreate(Bundle savedInstanceState) { setToolbar(); setDrawer(); + + RoutineStream.getInstance().getExerciseObservable().subscribe(exercise -> { + mExercise = exercise; + invalidateOptionsMenu(); + }); + + keepScreenOnWhenAppIsRunning(); } + @Override + protected void onPause() { + super.onPause(); + + ActivityStream.getInstance().setActivityState(ActivityState.OnPause); + } + + @Override + protected void onStop() { + super.onStop(); + + clearFlagKeepScreenOn(); + } + @Override protected void onPostCreate(Bundle savedInstanceState) { super.onPostCreate(savedInstanceState); @@ -52,23 +80,63 @@ public boolean onCreateOptionsMenu(Menu menu) { return true; } - @Override + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + MenuItem progressionMenuItem = menu.findItem(R.id.action_progression); + + if(mExercise != null) { + if(mExercise.getSection().getSectionMode() == SectionMode.LEVELS || mExercise.getSection().getSectionMode() == SectionMode.PICK) { + progressionMenuItem.setVisible(true); + } else { + progressionMenuItem.setVisible(false); + } + } else { + progressionMenuItem.setVisible(false); + } + + return super.onPrepareOptionsMenu(menu); + } + + @Override public boolean onOptionsItemSelected(MenuItem item) { if(mNavigationDrawerFragment.getActionBarDrawerToggle().onOptionsItemSelected(item)) { return true; } switch(item.getItemId()) { - case (R.id.action_settings): - startActivity(new Intent(getApplicationContext(), SettingsActivity.class)); + case(R.id.action_progression): { + ProgressDialog progressDialog = new ProgressDialog(this, mExercise); + progressDialog.show(); + + return true; + } + + case (R.id.action_faq): { + startActivity(new Intent( + Intent.ACTION_VIEW, + Uri.parse("http://www.reddit.com/r/bodyweightfitness/wiki/faq"))); - return true; + return true; + } + + case (R.id.action_settings): { + startActivity(new Intent( + getApplicationContext(), + SettingsActivity.class)); + + return true; + } } return super.onOptionsItemSelected(item); } - private void setToolbar() { + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + keepScreenOnWhenAppIsRunning(); + } + + private void setToolbar() { Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); setSupportActionBar(toolbar); @@ -76,6 +144,7 @@ private void setToolbar() { if(actionBar != null) { RoutineStream.getInstance().getExerciseObservable().subscribe(exercise -> { actionBar.setTitle(exercise.getTitle()); + actionBar.setSubtitle(exercise.getDescription()); }); actionBar.setElevation(0); @@ -89,4 +158,16 @@ private void setDrawer() { mNavigationDrawerFragment = (NavigationDrawerFragment) getSupportFragmentManager().findFragmentById(R.id.drawer_fragment); mNavigationDrawerFragment.setDrawer(R.id.drawer_fragment, (DrawerLayout) findViewById(R.id.drawer_layout)); } + + private void keepScreenOnWhenAppIsRunning() { + if(PreferenceUtil.getInstance().keepScreenOnWhenAppIsRunning()) { + getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } else { + clearFlagKeepScreenOn(); + } + } + + private void clearFlagKeepScreenOn() { + getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } } diff --git a/app/src/main/java/io/mazur/fit/ui/SettingsActivity.java b/app/src/main/java/io/mazur/fit/ui/SettingsActivity.java index 3e5389a3..162b81c7 100644 --- a/app/src/main/java/io/mazur/fit/ui/SettingsActivity.java +++ b/app/src/main/java/io/mazur/fit/ui/SettingsActivity.java @@ -11,7 +11,8 @@ public class SettingsActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setupActionBar(); + + setActionBar(); if(getFragmentManager().findFragmentById(android.R.id.content) == null) { getFragmentManager().beginTransaction().add(android.R.id.content, new SettingsFragment()).commit(); @@ -29,7 +30,7 @@ public boolean onOptionsItemSelected(MenuItem item) { return super.onOptionsItemSelected(item); } - public void setupActionBar() { + public void setActionBar() { ActionBar actionBar = getSupportActionBar(); if(actionBar != null) { diff --git a/app/src/main/java/io/mazur/fit/utils/PreferenceUtil.java b/app/src/main/java/io/mazur/fit/utils/PreferenceUtil.java index 3758af7c..78ff81a5 100644 --- a/app/src/main/java/io/mazur/fit/utils/PreferenceUtil.java +++ b/app/src/main/java/io/mazur/fit/utils/PreferenceUtil.java @@ -19,6 +19,16 @@ private PreferenceUtil() { PreferenceManager.setDefaultValues(App.getContext(), R.xml.settings, false); } + public boolean playSoundWhenTimerStops() { + return PreferenceManager.getDefaultSharedPreferences(App.getContext()) + .getBoolean(Constants.PREFERENCE_PLAY_SOUND_WHEN_TIMER_STOPS, true); + } + + public boolean keepScreenOnWhenAppIsRunning() { + return PreferenceManager.getDefaultSharedPreferences(App.getContext()) + .getBoolean(Constants.PREFERENCE_KEEP_SCREEN_ON, true); + } + public void setTimerValue(long value) { PreferenceManager.getDefaultSharedPreferences(App.getContext()).edit() .putLong(Constants.PREFERENCE_TIMER_KEY, value).commit(); diff --git a/app/src/main/java/io/mazur/fit/view/CircularProgressBar.java b/app/src/main/java/io/mazur/fit/view/CircularProgressBar.java new file mode 100644 index 00000000..e61b86ba --- /dev/null +++ b/app/src/main/java/io/mazur/fit/view/CircularProgressBar.java @@ -0,0 +1,334 @@ +package io.mazur.fit.view; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.RectF; +import android.os.Build; +import android.os.Bundle; +import android.os.Parcelable; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.View; + +import io.mazur.fit.R; + +public class CircularProgressBar extends View { + private static final String INSTANCE_STATE_SAVEDSTATE = "saved_state"; + private static final String INSTANCE_STATE_PROGRESS = "progress"; + private static final String INSTANCE_STATE_MARKER_PROGRESS = "marker_progress"; + private static final String INSTANCE_STATE_PROGRESS_BACKGROUND_COLOR = "progress_background_color"; + private static final String INSTANCE_STATE_PROGRESS_COLOR = "progress_color"; + private static final String INSTANCE_STATE_THUMB_VISIBLE = "thumb_visible"; + private static final String INSTANCE_STATE_MARKER_VISIBLE = "marker_visible"; + + private final RectF mCircleBounds = new RectF(); + private Paint mBackgroundColorPaint = new Paint(); + + private int mCircleStrokeWidth = 10; + private int mGravity = Gravity.CENTER; + private int mHorizontalInset = 0; + + private boolean mIsInitializing = true; + private boolean mIsThumbEnabled = true; + + private Paint mMarkerColorPaint; + private Paint mProgressColorPaint; + private Paint mThumbColorPaint = new Paint(); + + private float mMarkerProgress = 0.0f; + private boolean mOverrdraw = false; + private float mProgress = 0.3f; + private int mProgressBackgroundColor; + private int mProgressColor; + + private float mRadius; + private float mThumbPosX; + private float mThumbPosY; + private int mThumbRadius = 20; + private float mTranslationOffsetX; + private float mTranslationOffsetY; + private int mVerticalInset = 0; + + public CircularProgressBar(final Context context) { + this(context, null); + } + + public CircularProgressBar(final Context context, final AttributeSet attrs) { + this(context, attrs, R.attr.CircularProgressBarStyle); + } + + public CircularProgressBar(final Context context, final AttributeSet attrs, final int defStyle) { + super(context, attrs, defStyle); + + final TypedArray attributes = context + .obtainStyledAttributes(attrs, R.styleable.CircularProgressBar, + defStyle, 0); + if (attributes != null) + { + try + { + setProgressColor(attributes.getColor(R.styleable.CircularProgressBar_progress_color, Color.CYAN)); + setProgressBackgroundColor(attributes.getColor(R.styleable.CircularProgressBar_progress_background_color,Color.GREEN)); + setProgress(attributes.getFloat(R.styleable.CircularProgressBar_progress, 0.0f)); + setWheelSize((int) attributes.getDimension(R.styleable.CircularProgressBar_stroke_width, 10)); + setThumbEnabled(attributes.getBoolean(R.styleable.CircularProgressBar_thumb_visible, true)); + + mGravity = attributes.getInt(R.styleable.CircularProgressBar_android_gravity,Gravity.CENTER); + } + finally + { + attributes.recycle(); + } + } + + mThumbRadius = mCircleStrokeWidth * 2; + updateBackgroundColor(); + updateMarkerColor(); + updateProgressColor(); + + // the view has now all properties and can be drawn + mIsInitializing = false; + + } + + @Override + protected void onDraw(final Canvas canvas) { + canvas.translate(mTranslationOffsetX, mTranslationOffsetY); + final float progressRotation = getCurrentRotation(); + // draw the background + if (!mOverrdraw) + { + canvas.drawArc(mCircleBounds, 270, -(360 - progressRotation), false,mBackgroundColorPaint); + } + // draw the progress or a full circle if overdraw is true + canvas.drawArc(mCircleBounds, 270, mOverrdraw ? 360 : progressRotation, false,mProgressColorPaint); + // draw the marker at the correct rotated position + } + + @Override + protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) { + final int height = getDefaultSize(getSuggestedMinimumHeight() + getPaddingTop() + getPaddingBottom(),heightMeasureSpec); + final int width = getDefaultSize(getSuggestedMinimumWidth() + getPaddingLeft() + getPaddingRight(), widthMeasureSpec); + + final int diameter; + if (heightMeasureSpec == MeasureSpec.UNSPECIFIED) + { + // ScrollView + diameter = width; + computeInsets(0, 0); + } + else if (widthMeasureSpec == MeasureSpec.UNSPECIFIED) + { + // HorizontalScrollView + diameter = height; + computeInsets(0, 0); + } + else + { + // Default + diameter = Math.min(width, height); + computeInsets(width - diameter, height - diameter); + } + + setMeasuredDimension(diameter, diameter); + + final float halfWidth = diameter * 0.5f; + // width of the drawed circle (+ the drawedThumb) + final float drawedWith; + if (isThumbEnabled()) + { + drawedWith = mThumbRadius * (5f / 6f); + } + else + { + drawedWith = mCircleStrokeWidth / 2f; + } + // -0.5f for pixel perfect fit inside the viewbounds + mRadius = halfWidth - drawedWith - 0.5f; + + mCircleBounds.set(-mRadius, -mRadius, mRadius, mRadius); + + mThumbPosX = (float) (mRadius * Math.cos(0)); + mThumbPosY = (float) (mRadius * Math.sin(0)); + + mTranslationOffsetX = halfWidth + mHorizontalInset; + mTranslationOffsetY = halfWidth + mVerticalInset; + + } + + @Override + protected void onRestoreInstanceState(final Parcelable state) { + if (state instanceof Bundle) { + final Bundle bundle = (Bundle) state; + setProgress(bundle.getFloat(INSTANCE_STATE_PROGRESS)); + + final int progressColor = bundle.getInt(INSTANCE_STATE_PROGRESS_COLOR); + if (progressColor != mProgressColor) { + mProgressColor = progressColor; + updateProgressColor(); + } + + final int progressBackgroundColor = bundle + .getInt(INSTANCE_STATE_PROGRESS_BACKGROUND_COLOR); + if (progressBackgroundColor != mProgressBackgroundColor) { + mProgressBackgroundColor = progressBackgroundColor; + updateBackgroundColor(); + } + + mIsThumbEnabled = bundle.getBoolean(INSTANCE_STATE_THUMB_VISIBLE); + + + super.onRestoreInstanceState(bundle.getParcelable(INSTANCE_STATE_SAVEDSTATE)); + return; + } + + super.onRestoreInstanceState(state); + } + + @Override + protected Parcelable onSaveInstanceState() { + final Bundle bundle = new Bundle(); + bundle.putParcelable(INSTANCE_STATE_SAVEDSTATE, super.onSaveInstanceState()); + bundle.putFloat(INSTANCE_STATE_PROGRESS, mProgress); + bundle.putFloat(INSTANCE_STATE_MARKER_PROGRESS, mMarkerProgress); + bundle.putInt(INSTANCE_STATE_PROGRESS_COLOR, mProgressColor); + bundle.putInt(INSTANCE_STATE_PROGRESS_BACKGROUND_COLOR, mProgressBackgroundColor); + bundle.putBoolean(INSTANCE_STATE_THUMB_VISIBLE, mIsThumbEnabled); + return bundle; + } + + public boolean isThumbEnabled() { + return mIsThumbEnabled; + } + + public void setProgress(final float progress) { + if (progress == mProgress) { + mProgress = 0.3f; + } + + if (progress == 1) { + mOverrdraw = false; + mProgress = 1; + } else { + + if (progress >= 1) { + mOverrdraw = true; + } else { + mOverrdraw = false; + } + + mProgress = progress % 1.0f; + } + + if (!mIsInitializing) { + invalidate(); + } + } + + public void setProgressBackgroundColor(final int color) { + mProgressBackgroundColor = color; + + updateMarkerColor(); + updateBackgroundColor(); + } + + public void setProgressColor(final int color) { + mProgressColor = color; + + updateProgressColor(); + } + + public void setThumbEnabled(final boolean enabled) { + mIsThumbEnabled = enabled; + } + + + public void setWheelSize(final int dimension) { + mCircleStrokeWidth = dimension; + + // update the paints + updateBackgroundColor(); + updateMarkerColor(); + updateProgressColor(); + } + + @SuppressLint("NewApi") + private void computeInsets(final int dx, final int dy) { + int absoluteGravity = mGravity; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + absoluteGravity = Gravity.getAbsoluteGravity(mGravity, getLayoutDirection()); + } + + switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { + case Gravity.LEFT: + mHorizontalInset = 0; + break; + case Gravity.RIGHT: + mHorizontalInset = dx; + break; + case Gravity.CENTER_HORIZONTAL: + default: + mHorizontalInset = dx / 2; + break; + } + switch (absoluteGravity & Gravity.VERTICAL_GRAVITY_MASK) { + case Gravity.TOP: + mVerticalInset = 0; + break; + case Gravity.BOTTOM: + mVerticalInset = dy; + break; + case Gravity.CENTER_VERTICAL: + default: + mVerticalInset = dy / 2; + break; + } + } + + private float getCurrentRotation() { + return 360 * mProgress; + } + + private float getMarkerRotation() { + return 360 * mMarkerProgress; + } + + private void updateBackgroundColor() { + mBackgroundColorPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mBackgroundColorPaint.setColor(mProgressBackgroundColor); + mBackgroundColorPaint.setStyle(Paint.Style.STROKE); + mBackgroundColorPaint.setStrokeWidth(mCircleStrokeWidth); + + invalidate(); + } + + + private void updateMarkerColor() { + mMarkerColorPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mMarkerColorPaint.setColor(mProgressBackgroundColor); + mMarkerColorPaint.setStyle(Paint.Style.STROKE); + mMarkerColorPaint.setStrokeWidth(mCircleStrokeWidth / 2); + + invalidate(); + } + + private void updateProgressColor() { + mProgressColorPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mProgressColorPaint.setColor(mProgressColor); + mProgressColorPaint.setStyle(Paint.Style.STROKE); + mProgressColorPaint.setStrokeWidth(mCircleStrokeWidth); + + mThumbColorPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mThumbColorPaint.setColor(mProgressColor); + mThumbColorPaint.setStyle(Paint.Style.FILL_AND_STROKE); + mThumbColorPaint.setStrokeWidth(mCircleStrokeWidth); + + invalidate(); + } + + +} diff --git a/app/src/main/java/io/mazur/fit/view/ProgressDialog.java b/app/src/main/java/io/mazur/fit/view/ProgressDialog.java new file mode 100644 index 00000000..9635873f --- /dev/null +++ b/app/src/main/java/io/mazur/fit/view/ProgressDialog.java @@ -0,0 +1,105 @@ +package io.mazur.fit.view; + +import android.app.Dialog; +import android.content.Context; +import android.graphics.Color; +import android.support.design.widget.FloatingActionButton; +import android.view.View; +import android.view.Window; +import android.widget.ImageButton; +import android.widget.TextView; + +import butterknife.ButterKnife; +import butterknife.InjectView; + +import io.mazur.fit.R; +import io.mazur.fit.model.Exercise; +import io.mazur.fit.model.SectionMode; +import io.mazur.fit.stream.RoutineStream; + +public class ProgressDialog { + @InjectView(R.id.level_title_text_view) TextView mLevelTitleTextView; + @InjectView(R.id.level_previous_button) ImageButton mLevelPreviousButton; + @InjectView(R.id.level_next_button) ImageButton mLevelNextButton; + @InjectView(R.id.level_progress_bar) CircularProgressBar mLevelProgressBar; + @InjectView(R.id.level_text_view) TextView mLevelTextView; + @InjectView(R.id.level_confirm_button) FloatingActionButton mLevelConfirmButton; + + private Dialog mDialog; + + private Exercise mExercise; + + private int mAvailableLevels; + private int mChosenLevel = 0; + + public ProgressDialog(Context context, Exercise exercise) { + mExercise = exercise; + mAvailableLevels = mExercise.getSection().getAvailableLevels(); + mChosenLevel = mExercise.getSection().getCurrentLevel(); + + mDialog = new Dialog(context); + mDialog.requestWindowFeature(Window.FEATURE_NO_TITLE); + mDialog.setContentView(R.layout.view_dialog_level); + mDialog.setCanceledOnTouchOutside(true); + } + + public void show() { + ButterKnife.inject(this, mDialog); + + mLevelPreviousButton.setOnClickListener(v -> { + mChosenLevel = mChosenLevel - 1; + updateDialog(); + }); + + mLevelNextButton.setOnClickListener(v -> { + mChosenLevel = mChosenLevel + 1; + updateDialog(); + }); + + mLevelConfirmButton.setOnClickListener(v -> { + Exercise chosenExercise = mExercise.getSection().getExercises().get(mChosenLevel); + + RoutineStream.getInstance().setLevel(chosenExercise, mChosenLevel); + + mDialog.dismiss(); + }); + + mLevelProgressBar.setWheelSize(12); + mLevelProgressBar.setProgressColor(Color.parseColor("#009688")); // 00453E + mLevelProgressBar.setProgressBackgroundColor(Color.parseColor("#00453E")); + + updateDialog(); + + mDialog.show(); + } + + private void updateDialog() { + Exercise chosenExercise = mExercise.getSection().getExercises().get(mChosenLevel); + + if(mExercise.getSection().getSectionMode() == SectionMode.LEVELS) { + mLevelTitleTextView.setText(chosenExercise.getTitle()); + mLevelTextView.setText(chosenExercise.getLevel()); + } else { + mLevelTitleTextView.setText(chosenExercise.getTitle()); + mLevelTextView.setText("Pick One"); + } + + if(mChosenLevel == 0) { + mLevelPreviousButton.setVisibility(View.INVISIBLE); + } else { + mLevelPreviousButton.setVisibility(View.VISIBLE); + } + + if(mChosenLevel >= (mAvailableLevels - 1)) { + mLevelNextButton.setVisibility(View.INVISIBLE); + } else { + mLevelNextButton.setVisibility(View.VISIBLE); + } + + updateProgressBar(); + } + + private void updateProgressBar() { + mLevelProgressBar.setProgress((1f / mAvailableLevels) * (mChosenLevel + 1)); + } +} diff --git a/app/src/main/java/io/mazur/fit/view/TimerView.java b/app/src/main/java/io/mazur/fit/view/TimerView.java index 14047b6e..35d05a43 100644 --- a/app/src/main/java/io/mazur/fit/view/TimerView.java +++ b/app/src/main/java/io/mazur/fit/view/TimerView.java @@ -1,6 +1,7 @@ package io.mazur.fit.view; import android.content.Context; +import android.os.Build; import android.os.Parcelable; import android.support.design.widget.FloatingActionButton; import android.util.AttributeSet; @@ -56,6 +57,10 @@ public void onCreate() { public void onCreateView() { ButterKnife.inject(this); + resetFloatingActionButtonMargin(getIncreaseTimerButton()); + resetFloatingActionButtonMargin(getStartStopTimerButton()); + resetFloatingActionButtonMargin(getRestartTimerButton()); + mTimerPresenter.onCreateView(this); } @@ -78,6 +83,14 @@ public void setSaveEnabled(boolean enabled) { super.setSaveEnabled(true); } + private void resetFloatingActionButtonMargin(FloatingActionButton floatingActionButton) { + if(Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) floatingActionButton.getLayoutParams(); + layoutParams.setMargins(0, 0, 0, 0); + floatingActionButton.setLayoutParams(layoutParams); + } + } + public RelativeLayout getTimerLayout() { return mTimerLayout; } diff --git a/app/src/main/res/drawable-hdpi/level_1.png b/app/src/main/res/drawable-hdpi/level_1.png new file mode 100644 index 00000000..225c26e4 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/level_1.png differ diff --git a/app/src/main/res/drawable-mdpi/level_1.png b/app/src/main/res/drawable-mdpi/level_1.png new file mode 100644 index 00000000..cf95fb0e Binary files /dev/null and b/app/src/main/res/drawable-mdpi/level_1.png differ diff --git a/app/src/main/res/drawable-v21/view_dialog_level_drawable.xml b/app/src/main/res/drawable-v21/view_dialog_level_drawable.xml new file mode 100644 index 00000000..b5e9cb9b --- /dev/null +++ b/app/src/main/res/drawable-v21/view_dialog_level_drawable.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-xhdpi/level_1.png b/app/src/main/res/drawable-xhdpi/level_1.png new file mode 100644 index 00000000..1d32f539 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/level_1.png differ diff --git a/app/src/main/res/drawable-xxhdpi/level_1.png b/app/src/main/res/drawable-xxhdpi/level_1.png new file mode 100644 index 00000000..7fc84e22 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/level_1.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_dropdown.png b/app/src/main/res/drawable-xxxhdpi/ic_dropdown.png new file mode 100755 index 00000000..7b39373b Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_dropdown.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/level_1.png b/app/src/main/res/drawable-xxxhdpi/level_1.png new file mode 100644 index 00000000..8938fc81 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/level_1.png differ diff --git a/app/src/main/res/drawable/view_dialog_level_drawable.xml b/app/src/main/res/drawable/view_dialog_level_drawable.xml new file mode 100644 index 00000000..b5e9cb9b --- /dev/null +++ b/app/src/main/res/drawable/view_dialog_level_drawable.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-port/activity_main.xml b/app/src/main/res/layout-port/activity_main.xml index 8d94b09e..5a236504 100644 --- a/app/src/main/res/layout-port/activity_main.xml +++ b/app/src/main/res/layout-port/activity_main.xml @@ -17,7 +17,7 @@ @@ -110,5 +108,4 @@ android:background="@drawable/view_action_exercise_drawable" android:scaleType="center" android:src="@drawable/ic_next"/> - \ No newline at end of file diff --git a/app/src/main/res/layout-port/view_toolbar.xml b/app/src/main/res/layout-port/view_toolbar.xml index 3e2d1ed6..a0100465 100644 --- a/app/src/main/res/layout-port/view_toolbar.xml +++ b/app/src/main/res/layout-port/view_toolbar.xml @@ -10,6 +10,6 @@ android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" - android:background="?attr/colorPrimary" + android:theme="@style/AppTheme.Toolbar" app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/> \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index a4508cdb..0cf5fdb6 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -18,7 +18,7 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/view_routine_category.xml b/app/src/main/res/layout/view_routine_category.xml index 26a2111e..8e795d23 100644 --- a/app/src/main/res/layout/view_routine_category.xml +++ b/app/src/main/res/layout/view_routine_category.xml @@ -4,9 +4,11 @@ android:id="@+id/routine_category_text_view" android:layout_width="match_parent" android:layout_height="match_parent" - android:minHeight="?attr/actionBarSize" + android:paddingTop="16dp" + android:paddingLeft="16dp" + android:paddingBottom="16dp" android:background="@color/white" - android:gravity="center" - android:textColor="@color/primaryDark" + android:gravity="left|center" + android:textColor="@color/primary" android:textSize="16sp" - app:typeface="roboto_light" /> \ No newline at end of file + app:typeface="roboto_black" /> \ No newline at end of file diff --git a/app/src/main/res/layout/view_routine_exercise.xml b/app/src/main/res/layout/view_routine_exercise.xml index 6a28495b..0bb178bd 100644 --- a/app/src/main/res/layout/view_routine_exercise.xml +++ b/app/src/main/res/layout/view_routine_exercise.xml @@ -5,19 +5,29 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:background="@drawable/view_routine_exercise_drawable" - android:clickable="true"> + android:clickable="true" + android:paddingTop="12dp" + android:paddingLeft="16dp" + android:paddingBottom="12dp"> + android:textSize="13sp" + android:text="Level 1" + app:typeface="roboto_black" /> - + android:layout_height="0dp" + android:layout_weight=".70" + android:textColor="@color/black" + android:textSize="16sp" + app:typeface="roboto_light"/> \ No newline at end of file diff --git a/app/src/main/res/layout/view_routine_section.xml b/app/src/main/res/layout/view_routine_section.xml index cb9a2d00..d394fa77 100644 --- a/app/src/main/res/layout/view_routine_section.xml +++ b/app/src/main/res/layout/view_routine_section.xml @@ -1,12 +1,35 @@ - \ No newline at end of file + android:orientation="vertical"> + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/view_timer.xml b/app/src/main/res/layout/view_timer.xml index 1ed1c930..9887d93a 100644 --- a/app/src/main/res/layout/view_timer.xml +++ b/app/src/main/res/layout/view_timer.xml @@ -33,7 +33,6 @@ android:layout_toLeftOf="@+id/timer_spacing" android:gravity="center" android:textSize="60sp" - android:text="00" app:typeface="roboto_bold" /> @@ -69,7 +67,6 @@ android:src="@drawable/ic_add" app:borderWidth="0dp" app:elevation="12dp" - app:pressedTranslationZ="12dp" app:fabSize="mini"/> + app:elevation="12dp"/> diff --git a/app/src/main/res/layout/view_toolbar.xml b/app/src/main/res/layout/view_toolbar.xml index 3e2d1ed6..a0100465 100644 --- a/app/src/main/res/layout/view_toolbar.xml +++ b/app/src/main/res/layout/view_toolbar.xml @@ -10,6 +10,6 @@ android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" - android:background="?attr/colorPrimary" + android:theme="@style/AppTheme.Toolbar" app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/> \ No newline at end of file diff --git a/app/src/main/res/menu/main.xml b/app/src/main/res/menu/main.xml index 0e882061..fb657f62 100644 --- a/app/src/main/res/menu/main.xml +++ b/app/src/main/res/menu/main.xml @@ -3,8 +3,19 @@ xmlns:tools="http://schemas.android.com/tools" tools:context=".MainActivity"> + + + + + android:title="@string/action_settings" + android:orderInCategory="110" + app:showAsAction="never"/> diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png index 2cdf41c1..b606d5e1 100644 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png index a8460380..8559de3a 100644 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png index eadea51e..8d53a6a8 100644 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png index 04cdba55..ea10c306 100644 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index cf920fde..71e3183e 100644 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/raw/beginner_routine.json b/app/src/main/res/raw/beginner_routine.json index 5c14d894..1a241a41 100644 --- a/app/src/main/res/raw/beginner_routine.json +++ b/app/src/main/res/raw/beginner_routine.json @@ -5,8 +5,10 @@ "type": "category" }, { - "title": "Dynamic Stretches (Do all, 5-10 reps each)", - "type": "section" + "title": "Dynamic Stretches", + "description": "Do all, 5-10 reps each", + "type": "section", + "mode": "all" }, { "id": "wall_extensions", @@ -51,8 +53,10 @@ "type": "exercise" }, { - "title": "Bodyline Drills (Do all, 10-60s hold each)", - "type": "section" + "title": "Bodyline Drills", + "description": "Do all, 10-60s hold each", + "type": "section", + "mode": "all" }, { "id": "plank", @@ -62,7 +66,7 @@ }, { "id": "side_plank", - "title": "Side Plank (Do both sides)", + "title": "Side Plank (Both sides)", "description": "10-60s hold", "type": "exercise" }, @@ -85,8 +89,10 @@ "type": "exercise" }, { - "title": "Activity (Pick one, 10-20 reps)", - "type": "section" + "title": "Activity", + "description": "Pick one, 10-20 reps", + "type": "section", + "mode": "pick" }, { "id": "squat_jumps", @@ -105,46 +111,56 @@ "type": "category" }, { - "title": "Handstand (5 min)", - "type": "section" + "title": "Handstand", + "description": "5 min", + "type": "section", + "mode": "levels" }, { "id": "wall_plank", - "title": "Level 1: Wall Plank", + "level": "Level 1", + "title": "Wall Plank", "description": "5 minutes", "type": "exercise" }, { "id": "wall_handstand", - "title": "Level 2: Stomach-to-Wall Handstand", + "level": "Level 2", + "title": "Stomach-to-Wall Handstand", "description": "5 minutes", "type": "exercise" }, { "id": "freestanding_handstand", - "title": "Level 3: Freestanding Handstand", + "level": "Level 3", + "title": "Freestanding Handstand", "description": "5 minutes", "type": "exercise" }, { - "title": "Support Practice (2-3 min)", - "type": "section" + "title": "Support Practice", + "description": "2-3 min", + "type": "section", + "mode": "levels" }, { "id": "parallel_bar_hold", - "title": "Level 1: Parallel Bar support", + "level": "Level 1", + "title": "Parallel Bar support", "description": "2-3 minutes", "type": "exercise" }, { "id": "ring_support", - "title": "Level 2: Ring Support", + "level": "Level 2", + "title": "Ring Support", "description": "2-3 minutes", "type": "exercise" }, { "id": "rings_turned_out_support", - "title": "Level 3: Rings Turned Out Support Hold", + "level": "Level 3", + "title": "Rings Turned Out Support Hold", "description": "2-3 minutes", "type": "exercise" }, @@ -153,212 +169,248 @@ "type": "category" }, { - "title": "Pullup Progression 3x(5-8)", - "type": "section" + "title": "Pullup Progression", + "description": "3x(5-8)", + "type": "section", + "mode": "levels" }, { "id": "negative_pullup", - "title": "Level 1: Negative Pullups", + "level": "Level 1", + "title": "Negative Pullups", "description": "3 sets of 5-8 reps", "type": "exercise" }, { "id": "pullup", - "title": "Level 2: Pull Up", + "level": "Level 2", + "title": "Pull Up", "description": "3 sets of 5-8 reps", "type": "exercise" }, { "id": "lsit_pullup", - "title": "Level 3: L-sit Pull Up", + "level": "Level 3", + "title": "L-sit Pull Up", "description": "3 sets of 5-8 reps", "type": "exercise" }, { "id": "pullover", - "title": "Level 4: Pull Over", + "level": "Level 4", + "title": "Pull Over", "description": "3 sets of 5-8 reps", "type": "exercise" }, { - "title": "Dipping Progression 3x(5-8)", - "type": "section" + "title": "Dipping Progression", + "description": "3x(5-8)", + "type": "section", + "mode": "levels" }, { "id": "parallel_bar_dips", - "title": "Level 1: Parallel Bar Dips", + "level": "Level 1", + "title": "Parallel Bar Dips", "description": "3 sets of 5-8 reps", "type": "exercise" }, { "id": "ring_dips", - "title": "Level 2: Ring Dips", + "level": "Level 2", + "title": "Ring Dips", "description": "3 sets of 5-8 reps", "type": "exercise" }, { "id": "l_ring_dips", - "title": "Level 3: Ring Dips in L-sit", + "level": "Level 3", + "title": "Ring Dips in L-sit", "description": "3 sets of 5-8 reps", "type": "exercise" }, { - "title": "Squat Progression 3x(5-8)", - "type": "section" + "title": "Squat Progression", + "description": "3x(5-8)", + "type": "section", + "mode": "levels" }, { "id": "assisted_squat", - "title": "Level 1: Assisted Bodyweight Squat", + "level": "Level 1", + "title": "Assisted Bodyweight Squat", "description": "3 sets of 5-8 reps", "type": "exercise" }, { "id": "bodyweight_squat", - "title": "Level 2: Bodyweight Squat", + "level": "Level 2", + "title": "Bodyweight Squat", "description": "3 sets of 5-8 reps", "type": "exercise" }, { "id": "deep_stepup", - "title": "Level 3: Deep Step-up", + "level": "Level 3", + "title": "Deep Step-up", "description": "3 sets of 5-8 reps", "type": "exercise" }, { - "title": "L-sit Progression 3x(10-30s)", - "type": "section" + "title": "L-sit Progression", + "description": "3x(10-30s)", + "type": "section", + "mode": "levels" }, { "id": "floor_l_sit", - "title": "Level 1: Foot-supported L-sit", + "level": "Level 1", + "title": "Foot-supported L-sit", "description": "3x(10-30s)", "type": "exercise" }, { "id": "one_foot_l_sit", - "title": "Level 2: One-Leg Foot Supported L-sit", + "level": "Level 2", + "title": "One-Leg Foot Supported L-sit", "description": "3x(10-30s)", "type": "exercise" }, { "id": "tuck_l_sit", - "title": "Level 3: Tuck L-sit", - "description": "3x(10-30s)", - "type": "exercise" - }, - { - "id": "l_sit", - "title": "Level 4: One-leg L-sit", + "level": "Level 3", + "title": "Tuck L-sit", "description": "3x(10-30s)", "type": "exercise" }, { "id": "l_sit", - "title": "Level 5: L-sit", + "level": "Level 4", + "title": "L-sit", "description": "3x(10-30s)", "type": "exercise" }, { - "title": "Pushing 3x(5-8)", - "type": "section" + "title": "Pushing", + "description": "3x(5-8)", + "type": "section", + "mode": "levels" }, { "id": "wall_pushup", - "title": "Level 1: Wall Pushup", + "level": "Level 1", + "title": "Wall Pushup", "description": "3 sets of 5-8 reps", "type": "exercise" }, { "id": "incline_pushup", - "title": "Level 2: Incline Pushup", + "level": "Level 2", + "title": "Incline Pushup", "description": "3 sets of 5-8 reps", "type": "exercise" }, { "id": "pushup", - "title": "Level 3: Pushup", + "level": "Level 3", + "title": "Pushup", "description": "3 sets of 5-8 reps", "type": "exercise" }, { "id": "diamond_pushup", - "title": "Level 4: Diamond Pushup", + "level": "Level 4", + "title": "Diamond Pushup", "description": "3 sets of 5-8 reps", "type": "exercise" }, { "id": "ring_pushup", - "title": "Level 5: Rings Push Ups", + "level": "Level 5", + "title": "Rings Push Ups", "description": "3 sets of 5-8 reps", "type": "exercise" }, { "id": "rings_turned_out_pushup", - "title": "Level 5: Rings Turned Out Push Up", + "level": "Level 6", + "title": "Rings Turned Out Push Up", "description": "3 sets of 5-8 reps", "type": "exercise" }, { "id": "pseudo_planche_pushup", - "title": "Level 7: Rings Turned Out PPPU", + "level": "Level 7", + "title": "Rings Turned Out PPPU", "description": "3 sets of 5-8 reps", "type": "exercise" }, { "id": "pseudo_planche", - "title": "Level 5-7 (No Rings): Pseudo Planche Push Ups", + "level": "Level 5-7 (No Rings)", + "title": "Pseudo Planche Push Ups", "description": "3 sets of 5-8 reps", "type": "exercise" }, { - "title": "Row Progression 3x(5-8)", - "type": "section" + "title": "Row Progression", + "description": "3x(5-8)", + "type": "section", + "mode": "levels" }, { "id": "vertical_row", - "title": "Level 1: Vertical Row", + "level": "Level 1", + "title": "Vertical Row", "description": "3 sets of 5-8 reps", "type": "exercise" }, { "id": "incline_row", - "title": "Level 2: Incline Row", + "level": "Level 2", + "title": "Incline Row", "description": "3 sets of 5-8 reps", "type": "exercise" }, { "id": "horizontal_row", - "title": "Level 3: Horizontal Row", + "level": "Level 3", + "title": "Horizontal Row", "description": "3 sets of 5-8 reps", "type": "exercise" }, { "id": "wide_row", - "title": "Level 4: Wide Row", + "level": "Level 4", + "title": "Wide Row", "description": "3 sets of 5-8 reps", "type": "exercise" }, { "id": "tuck_front_lever", - "title": "Level 5: Tuck Front Lever", + "level": "Level 5", + "title": "Tuck Front Lever", "description": "3 sets of 5-8 reps", "type": "exercise" }, { "id": "tuck_ice_cream_maker", - "title": "Level 6: Tuck Ice Cream Maker", + "level": "Level 6", + "title": "Tuck Ice Cream Maker", "description": "3 sets of 5-8 reps", "type": "exercise" }, { "id": "tuck_front_lever_row", - "title": "Level 7: Tuck Front Lever Row", + "level": "Level 7", + "title": "Tuck Front Lever Row", "description": "3 sets of 5-8 reps", "type": "exercise" }, { "id": "advanced_tuck_front_lever_row", - "title": "Level 8: Advanced Tuck Front Lever Row", + "level": "Level 8", + "title": "Advanced Tuck Front Lever Row", "description": "3 sets of 5-8 reps", "type": "exercise" } diff --git a/app/src/main/res/raw/finished.mp3 b/app/src/main/res/raw/finished.mp3 new file mode 100644 index 00000000..d9d03101 Binary files /dev/null and b/app/src/main/res/raw/finished.mp3 differ diff --git a/app/src/main/res/values-v21/styles.xml b/app/src/main/res/values-v21/styles.xml index 76fef9c4..22556540 100644 --- a/app/src/main/res/values-v21/styles.xml +++ b/app/src/main/res/values-v21/styles.xml @@ -11,6 +11,13 @@ @android:color/transparent + + + +