Added a screen for the break and also introduced ExerciseSets. Database restructured and added methods to the SQLDatabaseHelper.

This commit is contained in:
Christopher Beckmann 2017-09-03 18:34:32 +02:00
commit 0d7b3a52fc
29 changed files with 1830 additions and 284 deletions

View file

@ -11,7 +11,6 @@
android:screenOrientation="portrait"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name=".activities.BreakReminder"
android:label="@string/app_name"
@ -19,6 +18,8 @@
android:screenOrientation="portrait"
android:theme="@style/AppTheme.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
@ -65,12 +66,11 @@
<activity
android:name=".activities.TimerActivity"
android:launchMode="singleInstance">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service
@ -78,6 +78,8 @@
android:enabled="true"
android:exported="false" />
<activity android:name=".activities.ExerciseSetOverviewActivity" />
<activity android:name=".activities.ExerciseActivity"></activity>
</application>
</manifest>

Binary file not shown.

View file

@ -12,7 +12,7 @@ import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import org.secuso.privacyfriendlybreakreminder.database.DBHandler;
import org.secuso.privacyfriendlybreakreminder.database.SQLiteHelper;
import org.secuso.privacyfriendlybreakreminder.database.data.*;
import org.secuso.privacyfriendlybreakreminder.R;
@ -35,7 +35,7 @@ public class BreakActivity extends AppCompatActivity implements View.OnClickList
private int currentExercise, breakTime = 0, currentExerciseSection;
private ImageView image;
private String[] exercises;
private DBHandler dbHandler;
private SQLiteHelper SQLiteHelper;
private List<List<Exercise>> allAvailableExercises;
private List<Integer> sections;
private Random random;
@ -86,7 +86,7 @@ public class BreakActivity extends AppCompatActivity implements View.OnClickList
ct_text.setText(bufferZeroMinute + mins + ":00");
ct_text.setOnClickListener(this);
dbHandler = new DBHandler(this);
SQLiteHelper = new SQLiteHelper(this);
random = new Random();
sections = new ArrayList<>();
setRandomExercises();
@ -253,7 +253,7 @@ public class BreakActivity extends AppCompatActivity implements View.OnClickList
while (notFoundYet) {
currentExerciseSection = random.nextInt(exercises.length);
if (!usedSectionsString.contains(exercises[currentExerciseSection])) {
List<Exercise> list = dbHandler.getExercisesFromSection("de",exercises[currentExerciseSection]);
List<Exercise> list = SQLiteHelper.getExercisesFromSection("de",exercises[currentExerciseSection]);
allAvailableExercises.add(list);
usedSectionsString += exercises[currentExerciseSection] + ".";
editor.putString("currently_done_exercises", usedSectionsString);
@ -452,7 +452,7 @@ public class BreakActivity extends AppCompatActivity implements View.OnClickList
//Close database connection
if (!noExercises)
dbHandler.close();
SQLiteHelper.close();
finish();
}
}.start();

View file

@ -0,0 +1,32 @@
package org.secuso.privacyfriendlybreakreminder.activities;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.ImageView;
import org.secuso.privacyfriendlybreakreminder.R;
public class ExerciseActivity extends AppCompatActivity {
private ImageView playButton;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_exercise);
initResources();
}
private void initResources() {
playButton = (ImageView) findViewById(R.id.button_playPause);
}
private void updatePlayButton(boolean isRunning) {
if(isRunning) {
playButton.setImageResource(R.drawable.ic_pause_black_48dp);
} else {
playButton.setImageResource(R.drawable.ic_play_arrow_black_48dp);
}
}
}

View file

@ -0,0 +1,77 @@
package org.secuso.privacyfriendlybreakreminder.activities;
import android.database.Cursor;
import android.support.v4.content.AsyncTaskLoader;
import android.support.v4.content.Loader;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.widget.TextView;
import org.secuso.privacyfriendlybreakreminder.R;
import org.secuso.privacyfriendlybreakreminder.activities.adapter.ExerciseAdapter;
import org.secuso.privacyfriendlybreakreminder.database.SQLiteHelper;
public class ExerciseSetOverviewActivity extends AppCompatActivity implements android.support.v4.app.LoaderManager.LoaderCallbacks<Cursor>{
private TextView exerciseSetName;
private RecyclerView exerciseList;
private ExerciseAdapter exerciseAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_exercise_set);
initResources();
}
private void initResources() {
exerciseSetName = (TextView) findViewById(R.id.exercise_set_name);
exerciseList = (RecyclerView) findViewById(R.id.exercise_list);
exerciseAdapter = new ExerciseAdapter(this, null);
exerciseList.setAdapter(exerciseAdapter);
exerciseList.setLayoutManager(new GridLayoutManager(this, 2));
getSupportLoaderManager().initLoader(0, null, this);
}
@Override
public Loader<Cursor> onCreateLoader(int id, final Bundle args) {
return new AsyncTaskLoader<Cursor>(this) {
@Override
public Cursor loadInBackground() {
SQLiteHelper helper = new SQLiteHelper(getContext());
return helper.getExerciseCursorForSet(2, "de"); // TODO; get correct subset list
}
@Override
protected void onStartLoading() {
forceLoad();
}
@Override
protected void onReset() {}
};
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
cursor.moveToFirst();
exerciseAdapter.changeCursor(cursor);
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
((ExerciseAdapter) exerciseList.getAdapter()).changeCursor(null);
}
@Override
protected void onResume() {
super.onResume();
getSupportLoaderManager().restartLoader(0, null, this);
}
}

View file

@ -1,5 +1,7 @@
package org.secuso.privacyfriendlybreakreminder.activities;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
@ -7,23 +9,61 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.os.IBinder;
import android.preference.PreferenceManager;
import android.support.annotation.ColorRes;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.LinearInterpolator;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.NumberPicker;
import android.widget.ProgressBar;
import android.widget.TextView;
import org.secuso.privacyfriendlybreakreminder.R;
import org.secuso.privacyfriendlybreakreminder.service.TimerService;
import java.util.Locale;
public class TimerActivity extends AppCompatActivity {
private static final String TAG = TimerActivity.class.getSimpleName();
private static final String PREF_PICKER_SECONDS = TAG + ".PREF_PICKER_SECONDS";
private static final String PREF_PICKER_MINUTES = TAG + ".PREF_PICKER_MINUTES";
private static final String PREF_PICKER_HOURS = TAG + ".PREF_PICKER_HOURS";
// UI
private ProgressBar progressBar;
private TextView timerText;
private ImageButton playButton;
private ImageButton resetButton;
private NumberPicker secondsPicker;
private NumberPicker minutesPicker;
private NumberPicker hoursPicker;
private LinearLayout pickerLayout;
// animation
private int mShortAnimationDuration;
private static final String[] SECONDS_MINUTES = new String[60];
private static final String[] HOURS = new String[24];
static {
for(int i = 0; i < SECONDS_MINUTES.length; ++i) {
SECONDS_MINUTES[i] = String.format(Locale.US, "%02d", i);
}
for(int i = 0; i < HOURS.length; ++i) {
HOURS[i] = String.format(Locale.US, "%02d", i);
}
}
// Service
private TimerService mTimerService = null;
@ -44,17 +84,22 @@ public class TimerActivity extends AppCompatActivity {
};
private void onServiceConnected() {
if(mTimerService.isRunning()) {
progressBar.setMax((int) mTimerService.getInitialDuration());
}
updateUI();
}
private final BroadcastReceiver timerReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
long millisUntilDone = intent.getLongExtra("onTickMillis", -1L);
long initialDuration = intent.getLongExtra("initialMillis", 0L);
boolean isRunning = intent.getBooleanExtra("isRunning", false);
boolean isPaused = intent.getBooleanExtra("isPaused", false);
updateProgress(millisUntilDone);
if(mTimerService != null) {
updateUI();
} else {
updateUI(isRunning, isPaused, initialDuration, millisUntilDone);
}
}
};
@ -90,6 +135,7 @@ public class TimerActivity extends AppCompatActivity {
if(mTimerService != null && !mTimerService.isRunning()) {
updateProgress(mTimerService.getInitialDuration());
}
updateUI();
}
@Override
@ -111,19 +157,39 @@ public class TimerActivity extends AppCompatActivity {
}
private void initResources() {
SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(this);
mShortAnimationDuration = getResources().getInteger(android.R.integer.config_shortAnimTime);
progressBar = (ProgressBar) findViewById(R.id.progressBar);
timerText = (TextView) findViewById(R.id.timerText);
playButton = (ImageButton) findViewById(R.id.button_playPause);
resetButton = (ImageButton) findViewById(R.id.button_reset);
timerText.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
int duration = 1000 * 60 * 1; // 1 minutes
if(mTimerService != null) {
mTimerService.startTimer(duration);
progressBar.setMax(duration);
}
}
});
secondsPicker = (NumberPicker) findViewById(R.id.seconds_picker);
minutesPicker = (NumberPicker) findViewById(R.id.minutes_picker);
hoursPicker = (NumberPicker) findViewById(R.id.hours_picker);
secondsPicker.setDisplayedValues(SECONDS_MINUTES);
secondsPicker.setMinValue(0);
secondsPicker.setMaxValue(SECONDS_MINUTES.length - 1);
secondsPicker.setValue(pref.getInt(PREF_PICKER_SECONDS, 0));
minutesPicker.setDisplayedValues(SECONDS_MINUTES);
minutesPicker.setMinValue(0);
minutesPicker.setMaxValue(SECONDS_MINUTES.length - 1);
minutesPicker.setValue(pref.getInt(PREF_PICKER_MINUTES, 30));
hoursPicker.setDisplayedValues(HOURS);
hoursPicker.setMinValue(0);
hoursPicker.setMaxValue(HOURS.length - 1);
hoursPicker.setValue(pref.getInt(PREF_PICKER_HOURS, 1));
setDividerColor(secondsPicker, R.color.transparent);
setDividerColor(minutesPicker, R.color.transparent);
setDividerColor(hoursPicker, R.color.transparent);
pickerLayout = (LinearLayout) findViewById(R.id.picker_layout);
}
private void updateProgress(long millisUntilFinished) {
@ -135,15 +201,8 @@ public class TimerActivity extends AppCompatActivity {
int seconds = secondsUntilFinished % 60;
int minutes = minutesUntilFinished % 60;
StringBuilder sb = new StringBuilder();
if(hours > 0) sb.append(hours).append(":");
if(minutes < 10) sb.append(0);
sb.append(minutes).append(":");
if(seconds < 10) sb.append(0);
sb.append(seconds);
timerText.setText(sb.toString());
String time = String.format(Locale.US, "%02d:%02d:%02d", hours, minutes, seconds);
timerText.setText(time);
//progressBar.setMax(1000);
//ObjectAnimator animation = ObjectAnimator.ofInt(progressBar, "progress", 0, 1000 * percentFinished); // see this max value coming back here, we animale towards that value
@ -154,8 +213,148 @@ public class TimerActivity extends AppCompatActivity {
}
public void onClick(View view) {
switch(view.getId()) {
case R.id.button_playPause:
case R.id.progressBar:
handlePlayPressed();
break;
case R.id.button_reset:
mTimerService.stopAndResetTimer();
break;
case R.id.button_chooseExercise:
startActivity(new Intent(this, ExerciseSetOverviewActivity.class));
break;
}
updateUI();
}
private void handlePlayPressed() {
if(mTimerService != null) {
if(mTimerService.isPaused()) {
mTimerService.resumeTimer();
}
else if(mTimerService.isRunning()){
mTimerService.pauseTimer();
} else {
long duration = getCurrentSetDuration();
saveCurrentSetDuration();
mTimerService.startTimer(duration);
progressBar.setMax((int)duration);
}
}
}
private void saveCurrentSetDuration() {
SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(this);
pref.edit().putInt(PREF_PICKER_SECONDS, secondsPicker.getValue())
.putInt(PREF_PICKER_MINUTES, minutesPicker.getValue())
.putInt(PREF_PICKER_HOURS, hoursPicker.getValue()).apply();
}
private long getCurrentSetDuration() {
long duration = secondsPicker.getValue() * 1000;
duration += minutesPicker.getValue() * 1000 * 60;
duration += hoursPicker.getValue() * 1000 * 60 * 60;
return duration;
}
private void updateUI() {
if(mTimerService != null) {
updateUI(mTimerService.isRunning(), mTimerService.isPaused(), mTimerService.getInitialDuration(), mTimerService.getRemainingDuration());
} else {
showPicker(true);
}
}
private void updateUI(boolean running, boolean isPaused, long initialDuration, long remainingDuration) {
updatePlayButton(running);
progressBar.setMax((int) initialDuration);
updateProgress(remainingDuration);
showPicker(!running && !isPaused);
}
private void showPicker(boolean showPicker) {
if(showPicker && pickerLayout.getVisibility() != View.VISIBLE) {
pickerLayout.setAlpha(0f);
pickerLayout.setVisibility(View.VISIBLE);
pickerLayout.animate()
.alpha(1f)
.setStartDelay(mShortAnimationDuration)
.setDuration(mShortAnimationDuration)
.setListener(null);
timerText.animate()
.alpha(0f)
.setDuration(mShortAnimationDuration)
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animator) {
timerText.setVisibility(View.INVISIBLE);
}
});
progressBar.animate()
.alpha(0f)
.setDuration(mShortAnimationDuration)
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animator) {
progressBar.setVisibility(View.INVISIBLE);
}
});
} else if(!showPicker && pickerLayout.getVisibility() == View.VISIBLE) {
pickerLayout.animate()
.alpha(0f)
.setDuration(mShortAnimationDuration)
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animator) {
pickerLayout.setVisibility(View.INVISIBLE);
}
});
timerText.setAlpha(0f);
timerText.setVisibility(View.VISIBLE);
timerText.animate()
.alpha(1f)
.setDuration(mShortAnimationDuration)
.setListener(null);
progressBar.setAlpha(0f);
progressBar.setVisibility(View.VISIBLE);
progressBar.animate()
.alpha(1f)
.setDuration(mShortAnimationDuration)
.setStartDelay(mShortAnimationDuration)
.setListener(null);
}
}
private void updatePlayButton(boolean isRunning) {
if(isRunning) {
playButton.setImageResource(R.drawable.ic_pause_black_48dp);
} else {
playButton.setImageResource(R.drawable.ic_play_arrow_black_48dp);
}
}
private void setDividerColor(NumberPicker picker, @ColorRes int color) {
java.lang.reflect.Field[] pickerFields = NumberPicker.class.getDeclaredFields();
for (java.lang.reflect.Field pf : pickerFields) {
if (pf.getName().equals("mSelectionDivider")) {
pf.setAccessible(true);
try {
ColorDrawable colorDrawable = new ColorDrawable(ContextCompat.getColor(this, color));
pf.set(picker, colorDrawable);
} catch (IllegalArgumentException | Resources.NotFoundException | IllegalAccessException e) {
Log.e(TAG, e.getMessage(), e);
}
break;
}
}
}
}

View file

@ -0,0 +1,76 @@
package org.secuso.privacyfriendlybreakreminder.activities.adapter;
import android.content.Context;
import android.database.Cursor;
import android.support.v7.widget.RecyclerView.ViewHolder;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import org.secuso.privacyfriendlybreakreminder.R;
import org.secuso.privacyfriendlybreakreminder.activities.helper.CursorRecyclerViewAdapter;
import org.secuso.privacyfriendlybreakreminder.database.columns.ExerciseColumns;
import org.secuso.privacyfriendlybreakreminder.database.data.Exercise;
/**
* Created by Christopher Beckmann on 30.08.2017.
*/
public class ExerciseAdapter extends CursorRecyclerViewAdapter<ViewHolder> {
public ExerciseAdapter(Context context, Cursor cursor) {
super(context, cursor, ExerciseColumns._ID);
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.layout_exercise, parent, false);
return new ExerciseViewHolder(itemView);
}
@Override
public void onBindViewHolder(ViewHolder viewHolder, Cursor cursor) {
final Exercise exercise = ExerciseColumns.fromCursor(cursor);
ExerciseViewHolder vh = (ExerciseViewHolder) viewHolder;
String imageID = exercise.getImageID();
String[] imageIDSplit = imageID.split(",");
if(imageIDSplit.length > 1) {
imageID = imageIDSplit[0]; // only take the first image as a display image
}
int imageResID = mContext.getResources().getIdentifier(
"exercise_" + imageID,
"drawable",
mContext.getPackageName());
vh.image.setImageResource(imageResID);
vh.name.setText(exercise.getName());
vh.executionText.setText(exercise.getExecution());
vh.descriptionText.setText(exercise.getDescription());
vh.section.setText(exercise.getSection());
}
public class ExerciseViewHolder extends ViewHolder {
ImageView image;
TextView name;
TextView executionText;
TextView descriptionText;
TextView section;
public ExerciseViewHolder(View itemView) {
super(itemView);
name = (TextView) itemView.findViewById(R.id.exercise_name);
image = (ImageView) itemView.findViewById(R.id.exercise_image);
executionText = (TextView) itemView.findViewById(R.id.exercise_execution);
descriptionText = (TextView) itemView.findViewById(R.id.exercise_description);
section = (TextView) itemView.findViewById(R.id.exercise_section);
}
}
}

View file

@ -0,0 +1,75 @@
package org.secuso.privacyfriendlybreakreminder.activities.helper;
/*
* The MIT License (MIT)
*
* Copyright (c) 2014 Matthieu Harlé
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import android.database.Cursor;
import android.widget.Filter;
/**
* Taken from: https://gist.github.com/Shywim/127f207e7248fe48400b
*/
public class CursorFilter extends Filter {
CursorFilterClient mClient;
interface CursorFilterClient {
CharSequence convertToString(Cursor cursor);
Cursor runQueryOnBackgroundThread(CharSequence constraint);
Cursor getCursor();
void changeCursor(Cursor cursor);
}
CursorFilter(CursorFilterClient client) {
mClient = client;
}
@Override
public CharSequence convertResultToString(Object resultValue) {
return mClient.convertToString((Cursor) resultValue);
}
@Override
protected FilterResults performFiltering(CharSequence constraint) {
Cursor cursor = mClient.runQueryOnBackgroundThread(constraint);
FilterResults results = new FilterResults();
if (cursor != null) {
results.count = cursor.getCount();
results.values = cursor;
} else {
results.count = 0;
results.values = null;
}
return results;
}
@Override
protected void publishResults(CharSequence constraint, FilterResults results) {
Cursor oldCursor = mClient.getCursor();
if (results.values != null && results.values != oldCursor) {
mClient.changeCursor((Cursor) results.values);
}
}
}

View file

@ -0,0 +1,376 @@
package org.secuso.privacyfriendlybreakreminder.activities.helper;
/*
* Copyright (C) 2014 skyfish.jy@gmail.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
/*
* The MIT License (MIT)
*
* Copyright (c) 2014 Matthieu Harlé
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import android.content.Context;
import android.database.ContentObserver;
import android.database.Cursor;
import android.database.CursorJoiner;
import android.database.DataSetObserver;
import android.os.Handler;
import android.provider.BaseColumns;
import android.support.v7.widget.RecyclerView;
import android.text.TextUtils;
import android.widget.Filter;
import android.widget.FilterQueryProvider;
import android.widget.Filterable;
/**
* Provide a {@link android.support.v7.widget.RecyclerView.Adapter} implementation with cursor
* support.
*
* Child classes only need to implement {@link #onCreateViewHolder(android.view.ViewGroup, int)} and
* {@link #onBindViewHolder(RecyclerView.ViewHolder, Cursor)}.
*
* This class does not implement deprecated fields and methods from CursorAdapter! Incidentally,
* only {@link android.widget.CursorAdapter#FLAG_REGISTER_CONTENT_OBSERVER} is available, so the
* flag is implied, and only the Adapter behavior using this flag has been ported.
*
* @param <VH> {@inheritDoc}
*
* @see android.support.v7.widget.RecyclerView.Adapter
* @see android.widget.CursorAdapter
* @see Filterable
* @see CursorFilter.CursorFilterClient
*/
public abstract class CursorRecyclerViewAdapter<VH extends RecyclerView.ViewHolder> extends RecyclerView.Adapter<VH> implements CursorFilter.CursorFilterClient, Filterable {
protected Context mContext;
private Cursor mCursor;
private boolean mDataValid;
private int mRowIdColumn;
private DataSetObserver mDataSetObserver;
private ChangeObserver mChangeObserver;
private CursorFilter mCursorFilter;
private FilterQueryProvider mFilterQueryProvider;
private boolean mUseIndividualNotifies;
private String mIdIdentifier = "_id";
public CursorRecyclerViewAdapter(Context context, Cursor cursor, String idIdentifier) {
this(context, null);
this.setIdIdentifier(idIdentifier);
this.changeCursor(cursor);
}
public CursorRecyclerViewAdapter(Context context, Cursor cursor) {
mContext = context;
mCursor = cursor;
mDataValid = cursor != null;
mRowIdColumn = mDataValid ? mCursor.getColumnIndex(mIdIdentifier) : -1;
mDataSetObserver = new NotifyingDataSetObserver();
mChangeObserver = new ChangeObserver();
if (mDataValid) {
if (mChangeObserver != null) cursor.registerContentObserver(mChangeObserver);
if (mDataSetObserver != null) cursor.registerDataSetObserver(mDataSetObserver);
}
}
public void setIdIdentifier(String identifier) {
mIdIdentifier = TextUtils.isEmpty(identifier) ?
"_id" :
identifier;
}
public Cursor getCursor() {
return mCursor;
}
@Override
public int getItemCount() {
if (mDataValid && mCursor != null) {
return mCursor.getCount();
}
return 0;
}
@Override
public long getItemId(int position) {
if (mDataValid && mCursor != null && mCursor.moveToPosition(position)) {
return mCursor.getLong(mRowIdColumn);
}
return 0;
}
@Override
public void setHasStableIds(boolean hasStableIds) {
super.setHasStableIds(hasStableIds);
}
public abstract void onBindViewHolder(VH viewHolder, Cursor cursor);
@Override
public void onBindViewHolder(VH viewHolder, int position) {
if (!mDataValid) {
throw new IllegalStateException("this should only be called when the cursor is valid");
}
if (!mCursor.moveToPosition(position)) {
throw new IllegalStateException("couldn't move cursor to position " + position);
}
onBindViewHolder(viewHolder, mCursor);
}
/**
* Change the underlying cursor to a new cursor. If there is an existing cursor it will be
* closed.
*/
public void changeCursor(Cursor cursor) {
Cursor old = swapCursor(cursor);
if (old != null) {
old.close();
}
}
/**
* Swap in a new Cursor, returning the old Cursor. Unlike
* {@link #changeCursor(Cursor)}, the returned old Cursor is <em>not</em>
* closed.
*/
public Cursor swapCursor(Cursor newCursor) {
if (newCursor == mCursor) {
return null;
}
// unregister observers on old cursor
final Cursor oldCursor = mCursor;
if (oldCursor != null) {
if (mChangeObserver != null) oldCursor.unregisterContentObserver(mChangeObserver);
if (mDataSetObserver != null) oldCursor.unregisterDataSetObserver(mDataSetObserver);
}
// register observers on the new cursor
mCursor = newCursor;
if (mCursor != null) {
if (mChangeObserver != null) newCursor.registerContentObserver(mChangeObserver);
if (mDataSetObserver != null) newCursor.registerDataSetObserver(mDataSetObserver);
mRowIdColumn = newCursor.getColumnIndexOrThrow(mIdIdentifier);
mDataValid = true;
} else {
mRowIdColumn = -1;
mDataValid = false;
}
// notify that Dataset has Changed
boolean notifyDataChanged = true;
// check if we can get better notifies
if(mUseIndividualNotifies && oldCursor != null && newCursor != null && !oldCursor.isClosed() && !newCursor.isClosed()) {
notifyDataChanged = notifyItems(oldCursor, newCursor);
}
if(notifyDataChanged) {
notifyDataSetChanged();
}
return oldCursor;
}
public void setUseIndividualNotifies(boolean use) {
mUseIndividualNotifies = use;
}
/**
* Compares the two cursors and calls notifyChanged on every item that changed.
* @param oldCursor
* @param newCursor
* @return
*/
private boolean notifyItems(Cursor oldCursor, Cursor newCursor) {
String[] columns = new String[] { BaseColumns._ID };
CursorJoiner joiner = new CursorJoiner(oldCursor, columns, newCursor, columns);
for (CursorJoiner.Result res : joiner) {
switch (res) {
case LEFT:
notifyItemRemoved(newCursor.getPosition());
break;
case RIGHT:
notifyItemInserted(newCursor.getPosition());
break;
case BOTH:
// for(int i = 0; i < newCursor.getColumnCount(); i++) {
// if(!oldCursor.getString(i).equals(newCursor.getString(i))) {
// notifyItemChanged(newCursor.getPosition());
// }
// }
if (getRowHash(oldCursor) != getRowHash(newCursor)) {
notifyItemChanged(newCursor.getPosition());
}
break;
}
}
return false;
}
private int getRowHash(Cursor cursor) {
StringBuilder result = new StringBuilder("row");
for (int i = 0; i < cursor.getColumnCount(); i++) {
result.append(cursor.getString(i));
}
return result.toString().hashCode();
}
/**
* <p>Converts the cursor into a CharSequence. Subclasses should override this
* method to convert their results. The default implementation returns an
* empty String for null values or the default String representation of
* the value.</p>
*
* @param cursor the cursor to convert to a CharSequence
* @return a CharSequence representing the value
*/
public CharSequence convertToString(Cursor cursor) {
return cursor == null ? "" : cursor.toString();
}
/**
* Runs a query with the specified constraint. This query is requested
* by the filter attached to this adapter.
*
* The query is provided by a
* {@link FilterQueryProvider}.
* If no provider is specified, the current cursor is not filtered and returned.
*
* After this method returns the resulting cursor is passed to {@link #changeCursor(Cursor)}
* and the previous cursor is closed.
*
* This method is always executed on a background thread, not on the
* application's main thread (or UI thread.)
*
* Contract: when constraint is null or empty, the original results,
* prior to any filtering, must be returned.
*
* @param constraint the constraint with which the query must be filtered
*
* @return a Cursor representing the results of the new query
*
* @see #getFilter()
* @see #getFilterQueryProvider()
* @see #setFilterQueryProvider(FilterQueryProvider)
*/
public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
if (mFilterQueryProvider != null) {
return mFilterQueryProvider.runQuery(constraint);
}
return mCursor;
}
public Filter getFilter() {
if (mCursorFilter == null) {
mCursorFilter = new CursorFilter(this);
}
return mCursorFilter;
}
/**
* Returns the query filter provider used for filtering. When the
* provider is null, no filtering occurs.
*
* @return the current filter query provider or null if it does not exist
*
* @see #setFilterQueryProvider(FilterQueryProvider)
* @see #runQueryOnBackgroundThread(CharSequence)
*/
public FilterQueryProvider getFilterQueryProvider() {
return mFilterQueryProvider;
}
/**
* Sets the query filter provider used to filter the current Cursor.
* The provider's
* {@link FilterQueryProvider#runQuery(CharSequence)}
* method is invoked when filtering is requested by a client of
* this adapter.
*
* @param filterQueryProvider the filter query provider or null to remove it
*
* @see #getFilterQueryProvider()
* @see #runQueryOnBackgroundThread(CharSequence)
*/
public void setFilterQueryProvider(FilterQueryProvider filterQueryProvider) {
mFilterQueryProvider = filterQueryProvider;
}
/**
* Called when the {@link ContentObserver} on the cursor receives a change notification.
* Can be implemented by sub-class.
*
* @see ContentObserver#onChange(boolean)
*/
protected void onContentChanged() {
}
private class ChangeObserver extends ContentObserver {
public ChangeObserver() {
super(new Handler());
}
@Override
public boolean deliverSelfNotifications() {
return true;
}
@Override
public void onChange(boolean selfChange) {
onContentChanged();
}
}
private class NotifyingDataSetObserver extends DataSetObserver {
private final String TAG = NotifyingDataSetObserver.class.getSimpleName();
@Override
public void onChanged() {
super.onChanged();
mDataValid = true;
notifyDataSetChanged();
}
@Override
public void onInvalidated() {
super.onInvalidated();
mDataValid = false;
notifyDataSetChanged();
//There is no notifyDataSetInvalidated() method in RecyclerView.Adapter
}
}
}

View file

@ -1,192 +0,0 @@
package org.secuso.privacyfriendlybreakreminder.database;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;
import org.secuso.privacyfriendlybreakreminder.database.data.Exercise;
import org.secuso.privacyfriendlybreakreminder.database.columns.ExerciseColumns;
import org.secuso.privacyfriendlybreakreminder.database.columns.ExercisesLocalColumns;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
public class DBHandler extends SQLiteOpenHelper {
private SQLiteDatabase dataBase;
private static final String DATABASE_NAME = "exercises.sqlite";
private static final String DATABASE_PATH = "/data/data/org.secuso.privacyfriendlybreakreminder/databases/";
private static final int DATABASE_VERSION = 3;
private static final String[] deleteQueryList = {
ExerciseColumns.SQL_DELETE_ENTRIES,
ExercisesLocalColumns.SQL_DELETE_ENTRIES};
public DBHandler(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
//Check if database exists
File databaseFile = context.getDatabasePath(DATABASE_NAME);
if (!databaseFile.exists()) {
this.getReadableDatabase();
try {
copyDataBase(context);
this.close();
} catch (Exception e) {
Log.v("db log", "Copying data didn´t work!!");
}
}
}
@Override
public void onCreate(SQLiteDatabase db) {
}
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
execSQLList(db, deleteQueryList);
}
public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
onUpgrade(db, oldVersion, newVersion);
}
private void execSQLList(SQLiteDatabase db, String[] queryList) {
for (String query : queryList) {
db.execSQL(query);
}
}
public Cursor getExerciseCursor(String language) {
SQLiteDatabase database = getReadableDatabase();
return database.rawQuery(buildQuery(false), new String[]{language});
}
public List<Exercise> getExerciseList(String language) {
SQLiteDatabase database = getReadableDatabase();
Cursor c = database.rawQuery(buildQuery(false), new String[]{language});
List<Exercise> result = new ArrayList<>();
if(c != null) {
while(!c.isAfterLast()) {
result.add(ExerciseColumns.getExercise(c));
c.moveToNext();
}
c.close();
}
return result;
}
/**
* SELECT
* E._id,
* E.section,
* E.image_id,
* L.local_id,
* L.language,
* L.exercise_id,
* L.name,
* L.description,
* L.execution
* FROM exercises E LEFT OUTER JOIN exercises_local L
* ON E._id = L.exercise_id
* WHERE L.language = "de" [AND E.section LIKE %?%]
* ORDER BY E._id ASC
*
* @return the sql query without the ; at the end.
*/
private String buildQuery(boolean addSectionCheck) {
StringBuilder sqlQuery = new StringBuilder();
sqlQuery.append("SELECT ");
for(String field : ExerciseColumns.PROJECTION) {
sqlQuery.append("E.").append(field).append(", ");
}
for(String field : ExercisesLocalColumns.PROJECTION) {
sqlQuery.append("L.").append(field).append(", ");
}
// delete the last comma
sqlQuery.setLength(sqlQuery.length()-2);
sqlQuery.append(" FROM ");
sqlQuery.append(ExerciseColumns.TABLE_NAME);
sqlQuery.append(" E LEFT OUTER JOIN ");
sqlQuery.append(ExercisesLocalColumns.TABLE_NAME);
sqlQuery.append(" L");
sqlQuery.append("ON E.");
sqlQuery.append(ExerciseColumns._ID);
sqlQuery.append(" = L.");
sqlQuery.append(ExercisesLocalColumns.EXERCISE_ID);
sqlQuery.append("WHERE ");
sqlQuery.append("L.");
sqlQuery.append(ExercisesLocalColumns.LANGUAGE);
sqlQuery.append("= ? ");
if(addSectionCheck) {
sqlQuery.append("AND E.");
sqlQuery.append(ExerciseColumns.SECTION);
sqlQuery.append("LIKE ? ");
}
sqlQuery.append("ORDER BY E.");
sqlQuery.append(ExerciseColumns._ID);
sqlQuery.append(" ASC");
return sqlQuery.toString();
}
public List<Exercise> getExercisesFromSection(String language, String section) {
SQLiteDatabase database = getReadableDatabase();
Cursor c = database.rawQuery(buildQuery(true), new String[]{language, "%"+section+"%"});
List<Exercise> result = new ArrayList<>();
if(c != null) {
while(!c.isAfterLast()) {
result.add(ExerciseColumns.getExercise(c));
c.moveToNext();
}
c.close();
}
return result;
}
private void copyDataBase(Context context) throws IOException {
InputStream myInput = context.getAssets().open(DATABASE_NAME);
String outFileName = DATABASE_PATH + DATABASE_NAME;
OutputStream myOutput = new FileOutputStream(outFileName);
byte[] buffer = new byte[1024];
int length;
while ((length = myInput.read(buffer)) > 0) {
myOutput.write(buffer, 0, length);
}
myOutput.flush();
myOutput.close();
myInput.close();
}
}

View file

@ -0,0 +1,291 @@
package org.secuso.privacyfriendlybreakreminder.database;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;
import org.secuso.privacyfriendlybreakreminder.database.columns.ExerciseSetColumns;
import org.secuso.privacyfriendlybreakreminder.database.data.Exercise;
import org.secuso.privacyfriendlybreakreminder.database.columns.ExerciseColumns;
import org.secuso.privacyfriendlybreakreminder.database.columns.ExerciseLocalColumns;
import org.secuso.privacyfriendlybreakreminder.database.data.ExerciseSet;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
public class SQLiteHelper extends SQLiteOpenHelper {
private static final String TAG = SQLiteHelper.class.getSimpleName();
private Context mContext;
private static final String DATABASE_NAME = "exercises.sqlite";
private static final String DATABASE_PATH = "/data/data/org.secuso.privacyfriendlybreakreminder/databases/";
private static final int DATABASE_VERSION = 4;
private static final String[] deleteQueryList = {
ExerciseColumns.SQL_DELETE_ENTRIES,
ExerciseLocalColumns.SQL_DELETE_ENTRIES,
ExerciseSetColumns.SQL_DELETE_ENTRIES};
private boolean onCreate;
private boolean onUpgrade;
public SQLiteHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
mContext = context;
}
@Override
public void onOpen(SQLiteDatabase db) {
if (onCreate || onUpgrade) {
onCreate = onUpgrade = false;
copyDatabaseFromAssets(db);
}
}
@Override
public void onCreate(SQLiteDatabase db) {
onCreate = true;
}
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
onUpgrade = true;
}
public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
onUpgrade(db, oldVersion, newVersion);
}
public synchronized Cursor getExerciseCursor(String language) {
SQLiteDatabase database = getReadableDatabase();
return database.rawQuery(buildQuery(false), new String[]{language});
}
public synchronized List<Exercise> getExerciseList(String language) {
Cursor c = getExerciseCursor(language);
return buildExerciseList(c);
}
public synchronized Cursor getExerciseCursorForSet(int setId, String language) {
SQLiteDatabase database = getReadableDatabase();
String sql = "SELECT *\n" +
"FROM exercise_set ES LEFT OUTER JOIN exercise_set_exercises ESE\n" +
"\tON ES.exercise_set_id = ESE.exercise_set_id\n" +
"LEFT OUTER JOIN exercises E\n" +
"\tON ESE.exercise_id = E.exercise_id\n" +
"LEFT OUTER JOIN exercises_local L\n" +
"\tON E.exercise_id = L.exercise_id\n" +
"WHERE ES.exercise_set_id = ? AND L.language = ?\n" +
"ORDER BY ESE.exercise_id ASC";
String sql2 = "SELECT *\n" +
"\tFROM (SELECT * \n" +
"\t\t\tFROM (SELECT *\n" +
"\t\t\t\tFROM "+ExerciseSetColumns.TABLE_NAME+" ES LEFT OUTER JOIN exercise_set_exercises ESE\n" +
"\t\t\t\tON ES."+ExerciseSetColumns._ID+" = ESE."+ExerciseSetColumns._ID+"\n" +
"\t\t\t\tWHERE ES."+ExerciseSetColumns._ID+" = ?\n" +
"\t\t\t\tORDER BY ESE."+ExerciseColumns._ID+" ASC) ES_ESE \n" +
"\t\t\tLEFT OUTER JOIN "+ExerciseColumns.TABLE_NAME+" E\n" +
"\t\t\tON ES_ESE."+ExerciseColumns._ID+" = E."+ExerciseColumns._ID+") ES_ESE_E \n" +
"\t\tLEFT OUTER JOIN "+ExerciseLocalColumns.TABLE_NAME+" L\n" +
"\t\tON ES_ESE_E."+ExerciseColumns._ID+" = L."+ExerciseLocalColumns.EXERCISE_ID+"\n" +
"\t\tWHERE L."+ExerciseLocalColumns.LANGUAGE+" = ?";
return database.rawQuery(sql, new String[]{String.valueOf(setId), language});
}
public synchronized ExerciseSet getExerciseListForSet(int setId, String language) {
Cursor c = getExerciseCursorForSet(setId, language);
ExerciseSet result = null;
if(c != null) {
c.moveToFirst();
result = ExerciseSetColumns.fromCursor(c);
while(!c.isAfterLast()) {
result.add(ExerciseColumns.fromCursor(c));
c.moveToNext();
}
c.close();
}
return result;
}
private List<Exercise> buildExerciseList(Cursor c) {
List<Exercise> result = new ArrayList<>();
if(c != null) {
c.moveToFirst();
while(!c.isAfterLast()) {
result.add(ExerciseColumns.fromCursor(c));
c.moveToNext();
}
c.close();
}
return result;
}
private String buildQuery(boolean addSectionCheck) {
return buildQuery(addSectionCheck, "");
}
public synchronized List<Exercise> getExercisesFromSection(String language, String section) { // TODO: Rename after old activities are deleted
SQLiteDatabase database = getReadableDatabase();
Cursor c = database.rawQuery(buildQuery(true), new String[]{language, "%"+section+"%"});
return buildExerciseList(c);
}
/**
* SELECT
* E._id,
* E.section,
* E.image_id,
* L.local_id,
* L.language,
* L.exercise_id,
* L.name,
* L.description,
* L.execution
* FROM exercises E LEFT OUTER JOIN exercises_local L
* ON E._id = L.exercise_id
* WHERE L.language = "de" [AND E.section LIKE %?%]
* ORDER BY E._id ASC
*
* @return the sql query without the ; at the end.
*/
private String buildQuery(boolean addSectionCheck, String customWhereClause) {
StringBuilder sqlQuery = new StringBuilder();
sqlQuery.append("SELECT ");
for(String field : ExerciseColumns.PROJECTION) {
sqlQuery.append("E.").append(field).append(", ");
}
for(String field : ExerciseLocalColumns.PROJECTION) {
sqlQuery.append("L.").append(field).append(", ");
}
// delete the last comma
sqlQuery.setLength(sqlQuery.length()-2);
sqlQuery.append(" FROM ");
sqlQuery.append(ExerciseColumns.TABLE_NAME);
sqlQuery.append(" E LEFT OUTER JOIN ");
sqlQuery.append(ExerciseLocalColumns.TABLE_NAME);
sqlQuery.append(" L ");
sqlQuery.append("ON E.");
sqlQuery.append(ExerciseColumns._ID);
sqlQuery.append(" = L.");
sqlQuery.append(ExerciseLocalColumns.EXERCISE_ID);
sqlQuery.append(" ");
sqlQuery.append("WHERE ");
sqlQuery.append("L.");
sqlQuery.append(ExerciseLocalColumns.LANGUAGE);
sqlQuery.append(" = ? ");
if(addSectionCheck) {
sqlQuery.append("AND E.");
sqlQuery.append(ExerciseColumns.SECTION);
sqlQuery.append(" LIKE ? ");
}
sqlQuery.append("ORDER BY E.");
sqlQuery.append(ExerciseColumns._ID);
sqlQuery.append(" ASC");
return sqlQuery.toString();
}
private void copyDataBase(Context context) throws IOException {
InputStream myInput = context.getAssets().open(DATABASE_NAME);
String outFileName = DATABASE_PATH + DATABASE_NAME;
OutputStream myOutput = new FileOutputStream(outFileName);
byte[] buffer = new byte[1024];
int length;
while ((length = myInput.read(buffer)) > 0) {
myOutput.write(buffer, 0, length);
}
myOutput.flush();
myOutput.close();
myInput.close();
SQLiteDatabase copiedDb = context.openOrCreateDatabase(DATABASE_NAME, 0, null);
copiedDb.execSQL("PRAGMA user_version = " + DATABASE_VERSION);
copiedDb.close();
}
/**
* Copy packaged database from assets folder to the database created in the
* application package context.
*
* @param db
* The target database in the application package context.
*/
private void copyDatabaseFromAssets(SQLiteDatabase db) {
Log.i(TAG, "copyDatabase");
InputStream myInput = null;
OutputStream myOutput = null;
try {
// Open db packaged as asset as the input stream
myInput = mContext.getAssets().open(DATABASE_NAME);
// Open the db in the application package context:
myOutput = new FileOutputStream(db.getPath());
// Transfer db file contents:
byte[] buffer = new byte[1024];
int length;
while ((length = myInput.read(buffer)) > 0) {
myOutput.write(buffer, 0, length);
}
myOutput.flush();
// Set the version of the copied database to the current
// version:
SQLiteDatabase copiedDb = mContext.openOrCreateDatabase(DATABASE_NAME, 0, null);
copiedDb.execSQL("PRAGMA user_version = " + DATABASE_VERSION);
copiedDb.close();
} catch (IOException e) {
e.printStackTrace();
throw new Error(TAG + " Error copying database");
} finally {
// Close the streams
try {
if (myOutput != null) {
myOutput.close();
}
if (myInput != null) {
myInput.close();
}
} catch (IOException e) {
e.printStackTrace();
throw new Error(TAG + " Error closing streams");
}
}
}
}

View file

@ -2,7 +2,6 @@ package org.secuso.privacyfriendlybreakreminder.database.columns;
import android.content.ContentValues;
import android.database.Cursor;
import android.provider.BaseColumns;
import org.secuso.privacyfriendlybreakreminder.database.data.Exercise;
@ -10,10 +9,11 @@ import org.secuso.privacyfriendlybreakreminder.database.data.Exercise;
* Created by Christopher Beckmann on 23.08.2017.
*/
public class ExerciseColumns implements BaseColumns {
public final class ExerciseColumns {
public static final String TABLE_NAME = "exercises";
public static final String _ID = "exercise_id";
public static final String SECTION = "section";
public static final String IMAGE_ID = "image_id";
@ -24,8 +24,8 @@ public class ExerciseColumns implements BaseColumns {
};
public static final String SQL_DELETE_ENTRIES = "DROP TABLE IF EXISTS " + TABLE_NAME;
public static Exercise getExercise(Cursor c) {
Exercise e = ExercisesLocalColumns.getExercise(c);
public static Exercise fromCursor(Cursor c) {
Exercise e = ExerciseLocalColumns.fromCursor(c);
e.setId(c.getInt(c.getColumnIndexOrThrow(ExerciseColumns._ID)));
e.setSection(c.getString(c.getColumnIndexOrThrow(ExerciseColumns.SECTION)));
@ -42,8 +42,10 @@ public class ExerciseColumns implements BaseColumns {
values.put(ExerciseColumns.SECTION, record.getSection());
values.put(ExerciseColumns.IMAGE_ID, record.getImageID());
values.putAll(ExercisesLocalColumns.getValues(record));
values.putAll(ExerciseLocalColumns.getValues(record));
return values;
}
private ExerciseColumns() {}
}

View file

@ -2,7 +2,6 @@ package org.secuso.privacyfriendlybreakreminder.database.columns;
import android.content.ContentValues;
import android.database.Cursor;
import android.provider.BaseColumns;
import org.secuso.privacyfriendlybreakreminder.database.data.Exercise;
@ -10,7 +9,7 @@ import org.secuso.privacyfriendlybreakreminder.database.data.Exercise;
* Created by Christopher Beckmann on 25.08.2017.
*/
public class ExercisesLocalColumns implements BaseColumns {
public final class ExerciseLocalColumns {
public static final String TABLE_NAME = "exercises_local";
@ -31,14 +30,14 @@ public class ExercisesLocalColumns implements BaseColumns {
};
public static final String SQL_DELETE_ENTRIES = "DROP TABLE IF EXISTS " + TABLE_NAME;
public static Exercise getExercise(Cursor c) {
public static Exercise fromCursor(Cursor c) {
Exercise e = new Exercise();
e.setLocalId(c.getInt(c.getColumnIndexOrThrow(ExercisesLocalColumns._ID)));
e.setLanguage(c.getString(c.getColumnIndexOrThrow(ExercisesLocalColumns.LANGUAGE)));
e.setDescription(c.getString(c.getColumnIndexOrThrow(ExercisesLocalColumns.DESCRIPTION)));
e.setExecution(c.getString(c.getColumnIndexOrThrow(ExercisesLocalColumns.EXECUTION)));
e.setName(c.getString(c.getColumnIndexOrThrow(ExercisesLocalColumns.NAME)));
e.setLocalId(c.getInt(c.getColumnIndexOrThrow(ExerciseLocalColumns._ID)));
e.setLanguage(c.getString(c.getColumnIndexOrThrow(ExerciseLocalColumns.LANGUAGE)));
e.setDescription(c.getString(c.getColumnIndexOrThrow(ExerciseLocalColumns.DESCRIPTION)));
e.setExecution(c.getString(c.getColumnIndexOrThrow(ExerciseLocalColumns.EXECUTION)));
e.setName(c.getString(c.getColumnIndexOrThrow(ExerciseLocalColumns.NAME)));
return e;
}
@ -47,13 +46,15 @@ public class ExercisesLocalColumns implements BaseColumns {
ContentValues values = new ContentValues();
if(record.getLocalId() != -1) {
values.put(ExercisesLocalColumns._ID, record.getLocalId());
values.put(ExerciseLocalColumns._ID, record.getLocalId());
}
values.put(ExercisesLocalColumns.LANGUAGE, record.getLanguage());
values.put(ExercisesLocalColumns.DESCRIPTION, record.getDescription());
values.put(ExercisesLocalColumns.EXECUTION, record.getExecution());
values.put(ExercisesLocalColumns.NAME, record.getName());
values.put(ExerciseLocalColumns.LANGUAGE, record.getLanguage());
values.put(ExerciseLocalColumns.DESCRIPTION, record.getDescription());
values.put(ExerciseLocalColumns.EXECUTION, record.getExecution());
values.put(ExerciseLocalColumns.NAME, record.getName());
return values;
}
private ExerciseLocalColumns() {}
}

View file

@ -0,0 +1,49 @@
package org.secuso.privacyfriendlybreakreminder.database.columns;
import android.content.ContentValues;
import android.database.Cursor;
import android.provider.BaseColumns;
import org.secuso.privacyfriendlybreakreminder.database.data.Exercise;
import org.secuso.privacyfriendlybreakreminder.database.data.ExerciseSet;
/**
* Created by Christopher Beckmann on 03.09.2017.
*/
public final class ExerciseSetColumns {
public static final String TABLE_NAME = "exercise_set";
public static final String _ID = "exercise_set_id";
public static final String NAME = "name";
public static final String[] PROJECTION = {
_ID,
NAME
};
public static final String SQL_DELETE_ENTRIES = "DROP TABLE IF EXISTS " + TABLE_NAME;
public static ExerciseSet fromCursor(Cursor c) {
ExerciseSet e = new ExerciseSet();
e.setId(c.getInt(c.getColumnIndexOrThrow(ExerciseSetColumns._ID)));
e.setName(c.getString(c.getColumnIndexOrThrow(ExerciseSetColumns.NAME)));
return e;
}
public static ContentValues getValues(ExerciseSet record) {
ContentValues values = new ContentValues();
if(record.getId() != -1) {
values.put(ExerciseSetColumns._ID, record.getId());
}
values.put(ExerciseSetColumns.NAME, record.getName());
return values;
}
private ExerciseSetColumns() {}
}

View file

@ -4,12 +4,12 @@ package org.secuso.privacyfriendlybreakreminder.database.data;
public class Exercise {
private int id;
private int localId;
private String section;
private String execution;
private String description;
private String name;
private String section = "";
private String execution = "";
private String description = "";
private String name = "";
private String imageID;
private String language;
private String language = "";
public Exercise() {
this.localId = -1;
@ -60,4 +60,35 @@ public class Exercise {
public void setDescription(String description) {
this.description = description;
}
@Override
public int hashCode() {
StringBuilder sb = new StringBuilder();
sb.append(id)
.append(localId)
.append(section)
.append(execution)
.append(description)
.append(name)
.append(imageID)
.append(language);
return sb.toString().hashCode();
}
@Override
public boolean equals(Object object) {
if(!(object instanceof Exercise)) return false;
Exercise other = (Exercise) object;
return this.id != other.id
|| this.localId != other.localId
|| !this.section.equals(other.section)
|| !this.execution.equals(other.execution)
|| !this.description.equals(other.description)
|| !this.name.equals(other.name)
|| !this.imageID.equals(other.imageID)
|| !this.language.equals(other.language);
}
}

View file

@ -0,0 +1,53 @@
package org.secuso.privacyfriendlybreakreminder.database.data;
import java.util.ArrayList;
import java.util.List;
/**
* Created by Christopher Beckmann on 03.09.2017.
*/
public class ExerciseSet {
private int id = -1;
private String name = null;
private List<Exercise> exercises = new ArrayList<>();
public ExerciseSet() {}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public void add(Exercise exercise) {
if(!exercises.contains(exercise)) {
exercises.add(exercise);
}
}
public void remove(Exercise exercise) {
if(exercises.contains(exercise)) {
exercises.remove(exercise);
}
}
public void get(int index) {
exercises.get(index);
}
public int size() {
return exercises.size();
}
}

View file

@ -22,6 +22,7 @@ import org.secuso.privacyfriendlybreakreminder.R;
import org.secuso.privacyfriendlybreakreminder.activities.TimerActivity;
import java.io.FileDescriptor;
import java.util.Locale;
import java.util.Timer;
import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
@ -74,6 +75,7 @@ public class TimerService extends Service {
mTimer = createTimer(duration);
mTimer.start();
isRunning = true;
sendBroadcast(buildBroadcast());
}
}
@ -81,6 +83,7 @@ public class TimerService extends Service {
if(isRunning) {
mTimer.cancel();
isRunning = false;
sendBroadcast(buildBroadcast());
}
}
@ -89,17 +92,29 @@ public class TimerService extends Service {
mTimer = createTimer(remainingDuration);
mTimer.start();
isRunning = true;
sendBroadcast(buildBroadcast());
}
}
public synchronized void resetTimer() {
if(isRunning) {
mTimer.cancel();
isRunning = false;
remainingDuration = 0;
mTimer = createTimer(initialDuration);
mTimer.start();
}
remainingDuration = initialDuration;
sendBroadcast(buildBroadcast());
}
public synchronized void stopAndResetTimer() {
if(isRunning) mTimer.cancel();
isRunning = false;
remainingDuration = initialDuration;
sendBroadcast(buildBroadcast());
}
public synchronized boolean isPaused() { return !isRunning && remainingDuration > 0 && remainingDuration != initialDuration; }
public synchronized boolean isRunning() {
return isRunning;
}
@ -111,29 +126,36 @@ public class TimerService extends Service {
@Override
public void onTick(long millisUntilFinished) {
int secondsUntilFinished = (int) Math.ceil(millisUntilFinished / 1000.0);
synchronized (TimerService.this) {
remainingDuration = millisUntilFinished;
}
remainingDuration = millisUntilFinished;
Intent broadcast = new Intent(TIMER_BROADCAST);
broadcast.putExtra("onTickMillis", millisUntilFinished);
broadcast.putExtra("countdown_seconds", secondsUntilFinished);
sendBroadcast(broadcast);
sendBroadcast(buildBroadcast());
}
@Override
public void onFinish() {
// TODO: finish broadcast
Intent broadcast = new Intent(TIMER_BROADCAST);
Intent broadcast = buildBroadcast();
broadcast.putExtra("done", true);
broadcast.putExtra("onTickMillis", 0);
broadcast.putExtra("countdown_seconds", 0);
sendBroadcast(broadcast);
resetTimer();
sendBroadcast(buildBroadcast());
stopAndResetTimer();
}
};
}
private synchronized Intent buildBroadcast() {
int secondsUntilFinished = (int) Math.ceil(remainingDuration / 1000.0);
Intent broadcast = new Intent(TIMER_BROADCAST);
broadcast.putExtra("onTickMillis", remainingDuration);
broadcast.putExtra("initialMillis", initialDuration);
broadcast.putExtra("countdown_seconds", secondsUntilFinished);
broadcast.putExtra("isRunning", isRunning());
broadcast.putExtra("isPaused", isPaused());
return (broadcast);
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
super.onStartCommand(intent, flags, startId);
@ -157,15 +179,9 @@ public class TimerService extends Service {
int seconds = secondsUntilFinished % 60;
int minutes = minutesUntilFinished % 60;
StringBuilder sb = new StringBuilder();
String time = String.format(Locale.US, "%02d:%02d:%02d", hours, minutes, seconds);
if(hours > 0) sb.append(hours).append(":");
if(minutes < 10) sb.append(0);
sb.append(minutes).append(":");
if(seconds < 10) sb.append(0);
sb.append(seconds);
builder.setContentText(sb.toString());
builder.setContentText(time);
builder.setContentIntent(PendingIntent.getActivity(this, 0, new Intent(this, TimerActivity.class), FLAG_UPDATE_CURRENT));
builder.setColor(ContextCompat.getColor(this, R.color.colorAccent));
builder.setPriority(NotificationCompat.PRIORITY_HIGH);
@ -189,6 +205,10 @@ public class TimerService extends Service {
context.startService(new Intent(context.getApplicationContext(), TimerService.class));
}
public synchronized long getRemainingDuration() {
return remainingDuration;
}
public class TimerServiceBinder extends Binder {
public TimerService getService() {
return TimerService.this;

Binary file not shown.

After

Width:  |  Height:  |  Size: 265 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 320 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 394 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 515 B

View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8" ?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@android:id/background">
<shape
android:innerRadiusRatio="2"
android:shape="ring"
android:useLevel="false"
android:thicknessRatio="6">
<solid android:color="@color/middlegrey"/>
</shape>
</item>
<item android:id="@android:id/progress">
<shape
android:innerRadiusRatio="2"
android:useLevel="true"
android:shape="ring"
android:thicknessRatio="6">
<solid android:color="@color/lightblue"/>
<corners android:radius="2dp"/>
</shape>
</item>
</layer-list>

View file

@ -0,0 +1,152 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="org.secuso.privacyfriendlybreakreminder.activities.ExerciseActivity">
<ImageButton
android:id="@+id/button_playPause"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="32dp"
android:layout_marginEnd="8dp"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
android:layout_marginStart="8dp"
android:background="?android:selectableItemBackgroundBorderless"
android:hapticFeedbackEnabled="true"
android:onClick="onClick"
android:scaleType="fitXY"
android:tint="@color/darkblue"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:srcCompat="@drawable/ic_play_arrow_black_48dp" />
<RelativeLayout
android:id="@+id/relativeLayout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:layout_marginStart="8dp"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:layout_marginTop="8dp">
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="125dp"
android:layout_height="125dp"
android:layout_alignTop="@+id/timerText"
android:layout_centerHorizontal="true"
android:max="100"
android:padding="16dp"
android:progress="66"
android:progressDrawable="@drawable/circular_small"
android:rotation="270" />
<TextView
android:id="@+id/timerText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:gravity="center"
android:text="00:00"
android:textAppearance="@style/TextAppearance.AppCompat.Large"
android:textColor="@color/colorPrimaryDark"
android:textSize="20sp"
android:textStyle="bold" />
</RelativeLayout>
<ImageView
android:id="@+id/exercise_image"
android:layout_width="150dp"
android:layout_height="150dp"
app:srcCompat="@drawable/exercise_0"
android:layout_marginTop="8dp"
app:layout_constraintTop_toBottomOf="@+id/relativeLayout"
android:layout_marginRight="8dp"
app:layout_constraintRight_toRightOf="parent"
android:layout_marginLeft="8dp"
app:layout_constraintLeft_toLeftOf="parent"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp" />
<TextView
android:id="@+id/execution_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="0dp"
android:text="Execution"
android:textStyle="bold"
app:layout_constraintLeft_toRightOf="@+id/relativeLayout"
app:layout_constraintTop_toBottomOf="@+id/section_card" />
<TextView
android:id="@+id/execution"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="5dp"
android:text="sitting or standing"
app:layout_constraintLeft_toRightOf="@+id/relativeLayout"
app:layout_constraintTop_toBottomOf="@+id/execution_title" />
<TextView
android:id="@+id/description_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:text="Description"
android:textStyle="bold"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@+id/exercise_image" />
<TextView
android:id="@+id/description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="- do something\n- do something else\n- then do the first something again"
app:layout_constraintLeft_toLeftOf="@+id/description_title"
app:layout_constraintTop_toBottomOf="@+id/description_title" />
<android.support.v7.widget.CardView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:cardBackgroundColor="@color/middleblue"
android:layout_marginEnd="8dp"
app:cardCornerRadius="12dp"
app:layout_constraintRight_toRightOf="parent"
android:id="@+id/section_card"
android:layout_marginTop="8dp"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/section"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginBottom="4dp"
android:layout_marginEnd="12dp"
android:layout_marginStart="12dp"
android:layout_marginTop="4dp"
android:gravity="center"
android:text="Neck, Arms"
android:textColor="@color/white" />
</android.support.v7.widget.CardView>
</android.support.constraint.ConstraintLayout>

View file

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="org.secuso.privacyfriendlybreakreminder.activities.ExerciseSetOverviewActivity">
<TextView
android:id="@+id/exercise_set_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:layout_marginTop="8dp"
android:text="TextView"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
android:layout_marginStart="8dp" />
<android.support.v7.widget.RecyclerView
android:id="@+id/exercise_list"
android:layout_width="368dp"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:layout_marginTop="8dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@+id/exercise_set_name"
android:layout_marginStart="8dp"
android:layout_marginRight="8dp"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintHorizontal_bias="0.5">
</android.support.v7.widget.RecyclerView>
</android.support.constraint.ConstraintLayout>

View file

@ -4,33 +4,112 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="@dimen/activity_horizontal_margin"
android:padding="0dp"
android:layout_margin="0dp"
tools:context="org.secuso.privacyfriendlybreakreminder.activities.TimerActivity">
<include android:id="@+id/exercise" layout="@layout/layout_exercise_set"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintHorizontal_bias="0.5" />
<android.support.design.widget.FloatingActionButton
android:id="@+id/button_chooseExercise"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="20dp"
android:layout_marginTop="16dp"
android:clickable="true"
app:fabSize="mini"
android:onClick="onClick"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="@+id/exercise"
android:layout_marginBottom="8dp"
app:layout_constraintBottom_toTopOf="@+id/relativeLayout"
app:layout_constraintVertical_bias="0.25" />
<RelativeLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:layout_marginTop="8dp"
android:layout_marginStart="8dp"
android:layout_marginRight="8dp"
app:layout_constraintRight_toRightOf="parent"
android:id="@+id/relativeLayout"
android:layout_marginEnd="8dp">
android:layout_marginEnd="8dp"
app:layout_constraintHorizontal_bias="0.0"
android:layout_marginTop="0dp"
app:layout_constraintTop_toBottomOf="@id/exercise">
<LinearLayout
android:id="@+id/picker_layout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:divider="@color/colorAccent"
android:dividerPadding="4dp"
android:gravity="center"
android:orientation="horizontal"
android:visibility="visible">
<NumberPicker
android:id="@+id/hours_picker"
android:layout_width="wrap_content"
android:layout_height="135dp"
android:clickable="false"
android:focusable="false"
android:theme="@style/AppTheme.NumberPicker" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=":"
android:textColor="@color/colorPrimary"
android:textSize="24sp"
android:textStyle="bold"
android:theme="@style/AppTheme.NumberPicker" />
<NumberPicker
android:id="@+id/minutes_picker"
android:layout_width="wrap_content"
android:layout_height="135dp"
android:theme="@style/AppTheme.NumberPicker" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=":"
android:textColor="@color/colorPrimary"
android:textSize="24sp"
android:textStyle="bold"
android:theme="@style/AppTheme.NumberPicker" />
<NumberPicker
android:id="@+id/seconds_picker"
android:layout_width="wrap_content"
android:layout_height="135dp"
android:clipChildren="false"
android:theme="@style/AppTheme.NumberPicker" />
</LinearLayout>
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="250dp"
android:layout_alignTop="@+id/timerText"
android:layout_centerHorizontal="true"
android:max="100"
android:onClick="onClick"
android:padding="16dp"
android:progress="66"
android:progressDrawable="@drawable/circular"
android:rotation="270"
android:max="100"
android:progress="66"
android:layout_alignTop="@+id/timerText" />
android:visibility="invisible" />
<TextView
android:id="@+id/timerText"
@ -39,21 +118,24 @@
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:gravity="center"
android:text="00:00"
android:text="00:00:00"
android:textAppearance="@style/TextAppearance.AppCompat.Large"
android:textColor="@color/colorPrimaryDark"
android:textSize="40sp"
android:textStyle="bold" />
android:textSize="36sp"
android:textStyle="bold"
android:visibility="invisible" />
</RelativeLayout>
<ImageButton
android:id="@+id/imageButton3"
android:id="@+id/button_reset"
android:layout_width="wrap_content"
android:onClick="onClick"
android:layout_height="wrap_content"
app:srcCompat="@drawable/ic_replay_black_48dp"
android:hapticFeedbackEnabled="true"
android:tint="@color/darkblue"
android:background="?android:selectableItemBackgroundBorderless"
android:layout_marginTop="8dp"
app:layout_constraintTop_toBottomOf="@+id/relativeLayout"
@ -61,15 +143,19 @@
app:layout_constraintRight_toRightOf="@+id/relativeLayout"
android:layout_marginLeft="8dp"
app:layout_constraintLeft_toLeftOf="@+id/relativeLayout"
app:layout_constraintHorizontal_bias="0.33" />
app:layout_constraintHorizontal_bias="0.33"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp" />
<ImageButton
android:id="@+id/imageButton4"
android:id="@+id/button_playPause"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="onClick"
android:background="?android:selectableItemBackgroundBorderless"
android:hapticFeedbackEnabled="true"
android:scaleType="fitXY"
android:tint="@color/darkblue"
app:srcCompat="@drawable/ic_play_arrow_black_48dp"
android:layout_marginRight="8dp"
app:layout_constraintRight_toRightOf="@+id/relativeLayout"
@ -77,6 +163,8 @@
app:layout_constraintTop_toBottomOf="@+id/relativeLayout"
android:layout_marginLeft="8dp"
app:layout_constraintLeft_toLeftOf="@+id/relativeLayout"
app:layout_constraintHorizontal_bias="0.66" />
app:layout_constraintHorizontal_bias="0.66"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp" />
</android.support.constraint.ConstraintLayout>

View file

@ -0,0 +1,81 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="0dp"
android:padding="0dp">
<TextView
android:id="@+id/exercise_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:layout_marginTop="0dp"
android:text="Name"
android:textAlignment="viewStart"
android:textStyle="bold"
app:layout_constraintLeft_toRightOf="@+id/cardView2"
app:layout_constraintTop_toTopOf="@+id/cardView2" />
<android.support.v7.widget.CardView
android:id="@+id/cardView2"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginLeft="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:innerRadius="0dp"
android:shape="ring"
android:thicknessRatio="1.9"
app:cardCornerRadius="24dp"
app:cardElevation="0dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginBottom="8dp">
<ImageView
android:id="@+id/exercise_image"
android:layout_width="48dp"
android:layout_height="48dp"
android:scaleType="fitCenter"
app:srcCompat="@drawable/exercise_0" />
</android.support.v7.widget.CardView>
<TextView
android:id="@+id/exercise_description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="8dp"
android:layout_marginTop="8dp"
android:text="Description"
android:textAlignment="textEnd"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/exercise_execution"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="0dp"
android:layout_marginTop="8dp"
android:text="Execution"
android:textAlignment="viewStart"
app:layout_constraintLeft_toLeftOf="@+id/exercise_name"
app:layout_constraintTop_toBottomOf="@+id/exercise_name" />
<TextView
android:id="@+id/exercise_section"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="0dp"
android:layout_marginTop="8dp"
android:text="Section"
android:textAlignment="textEnd"
app:layout_constraintRight_toRightOf="@+id/exercise_description"
app:layout_constraintTop_toBottomOf="@+id/exercise_description" />
</android.support.constraint.ConstraintLayout>

View file

@ -0,0 +1,68 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="132dp"
android:padding="0dp"
android:layout_margin="0dp"
xmlns:app="http://schemas.android.com/apk/res-auto">
<android.support.v7.widget.CardView
android:id="@+id/exercise_card"
android:layout_width="0dp"
android:layout_height="100dp"
android:layout_marginLeft="40dp"
android:layout_marginRight="40dp"
android:padding="0dp"
android:layout_marginTop="8dp"
android:visibility="visible"
app:cardCornerRadius="2dp"
app:cardElevation="4dp"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent">
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/textView9"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:layout_marginTop="0dp"
android:text="TextView"
app:layout_constraintLeft_toRightOf="@+id/cardView2"
app:layout_constraintTop_toTopOf="@+id/cardView2" />
<android.support.v7.widget.CardView
android:id="@+id/cardView2"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginLeft="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:innerRadius="0dp"
android:shape="ring"
android:thicknessRatio="1.9"
app:cardCornerRadius="24dp"
app:cardElevation="0dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ImageView
android:id="@+id/exercise_image"
android:layout_width="48dp"
android:layout_height="48dp"
android:scaleType="fitCenter"
app:srcCompat="@drawable/exercise_0" />
</android.support.v7.widget.CardView>
</android.support.constraint.ConstraintLayout>
</android.support.v7.widget.CardView>
</android.support.constraint.ConstraintLayout>

View file

@ -13,6 +13,13 @@
<item name="windowNoTitle">true</item>
</style>
<style name="AppTheme.NumberPicker" parent="AppTheme">
<item name="android:textColorPrimary">@color/colorPrimary</item>
<item name="android:textColorSecondary">@color/colorAccent</item>
<item name="android:textSize">28sp</item>
<item name="android:textStyle">bold</item>
</style>
<style name="AppTheme.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar" />
<style name="AppTheme.PopupOverlay" parent="ThemeOverlay.AppCompat.Light" />