diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1997841..b8b1620 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -11,7 +11,6 @@ android:screenOrientation="portrait" android:supportsRtl="true" android:theme="@style/AppTheme"> - + + @@ -65,12 +66,11 @@ - + - + + \ No newline at end of file diff --git a/app/src/main/assets/exercises.sqlite b/app/src/main/assets/exercises.sqlite index bce08a5..6aa2396 100644 Binary files a/app/src/main/assets/exercises.sqlite and b/app/src/main/assets/exercises.sqlite differ diff --git a/app/src/main/java/org/secuso/privacyfriendlybreakreminder/activities/BreakActivity.java b/app/src/main/java/org/secuso/privacyfriendlybreakreminder/activities/BreakActivity.java index 1ce9f91..b9c82c9 100644 --- a/app/src/main/java/org/secuso/privacyfriendlybreakreminder/activities/BreakActivity.java +++ b/app/src/main/java/org/secuso/privacyfriendlybreakreminder/activities/BreakActivity.java @@ -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> allAvailableExercises; private List 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 list = dbHandler.getExercisesFromSection("de",exercises[currentExerciseSection]); + List 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(); diff --git a/app/src/main/java/org/secuso/privacyfriendlybreakreminder/activities/ExerciseActivity.java b/app/src/main/java/org/secuso/privacyfriendlybreakreminder/activities/ExerciseActivity.java new file mode 100644 index 0000000..4b938a9 --- /dev/null +++ b/app/src/main/java/org/secuso/privacyfriendlybreakreminder/activities/ExerciseActivity.java @@ -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); + } + } +} diff --git a/app/src/main/java/org/secuso/privacyfriendlybreakreminder/activities/ExerciseSetOverviewActivity.java b/app/src/main/java/org/secuso/privacyfriendlybreakreminder/activities/ExerciseSetOverviewActivity.java new file mode 100644 index 0000000..c2047e2 --- /dev/null +++ b/app/src/main/java/org/secuso/privacyfriendlybreakreminder/activities/ExerciseSetOverviewActivity.java @@ -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{ + + 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 onCreateLoader(int id, final Bundle args) { + return new AsyncTaskLoader(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 loader, Cursor cursor) { + cursor.moveToFirst(); + exerciseAdapter.changeCursor(cursor); + } + + @Override + public void onLoaderReset(Loader loader) { + ((ExerciseAdapter) exerciseList.getAdapter()).changeCursor(null); + } + + @Override + protected void onResume() { + super.onResume(); + + getSupportLoaderManager().restartLoader(0, null, this); + } +} diff --git a/app/src/main/java/org/secuso/privacyfriendlybreakreminder/activities/TimerActivity.java b/app/src/main/java/org/secuso/privacyfriendlybreakreminder/activities/TimerActivity.java index 8af888e..24b4abd 100644 --- a/app/src/main/java/org/secuso/privacyfriendlybreakreminder/activities/TimerActivity.java +++ b/app/src/main/java/org/secuso/privacyfriendlybreakreminder/activities/TimerActivity.java @@ -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; + } + } + } } diff --git a/app/src/main/java/org/secuso/privacyfriendlybreakreminder/activities/adapter/ExerciseAdapter.java b/app/src/main/java/org/secuso/privacyfriendlybreakreminder/activities/adapter/ExerciseAdapter.java new file mode 100644 index 0000000..79a7ec2 --- /dev/null +++ b/app/src/main/java/org/secuso/privacyfriendlybreakreminder/activities/adapter/ExerciseAdapter.java @@ -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 { + + + 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); + + } + } +} diff --git a/app/src/main/java/org/secuso/privacyfriendlybreakreminder/activities/helper/CursorFilter.java b/app/src/main/java/org/secuso/privacyfriendlybreakreminder/activities/helper/CursorFilter.java new file mode 100644 index 0000000..ac5e7d2 --- /dev/null +++ b/app/src/main/java/org/secuso/privacyfriendlybreakreminder/activities/helper/CursorFilter.java @@ -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); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/secuso/privacyfriendlybreakreminder/activities/helper/CursorRecyclerViewAdapter.java b/app/src/main/java/org/secuso/privacyfriendlybreakreminder/activities/helper/CursorRecyclerViewAdapter.java new file mode 100644 index 0000000..a927e28 --- /dev/null +++ b/app/src/main/java/org/secuso/privacyfriendlybreakreminder/activities/helper/CursorRecyclerViewAdapter.java @@ -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 {@inheritDoc} + * + * @see android.support.v7.widget.RecyclerView.Adapter + * @see android.widget.CursorAdapter + * @see Filterable + * @see CursorFilter.CursorFilterClient + */ +public abstract class CursorRecyclerViewAdapter extends RecyclerView.Adapter 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 not + * 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(); + } + + /** + *

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.

+ * + * @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 + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/secuso/privacyfriendlybreakreminder/database/DBHandler.java b/app/src/main/java/org/secuso/privacyfriendlybreakreminder/database/DBHandler.java deleted file mode 100644 index 015ac1d..0000000 --- a/app/src/main/java/org/secuso/privacyfriendlybreakreminder/database/DBHandler.java +++ /dev/null @@ -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 getExerciseList(String language) { - SQLiteDatabase database = getReadableDatabase(); - - Cursor c = database.rawQuery(buildQuery(false), new String[]{language}); - - List 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 getExercisesFromSection(String language, String section) { - SQLiteDatabase database = getReadableDatabase(); - - Cursor c = database.rawQuery(buildQuery(true), new String[]{language, "%"+section+"%"}); - - List 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(); - } -} diff --git a/app/src/main/java/org/secuso/privacyfriendlybreakreminder/database/SQLiteHelper.java b/app/src/main/java/org/secuso/privacyfriendlybreakreminder/database/SQLiteHelper.java new file mode 100644 index 0000000..284a5dd --- /dev/null +++ b/app/src/main/java/org/secuso/privacyfriendlybreakreminder/database/SQLiteHelper.java @@ -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 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 buildExerciseList(Cursor c) { + List 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 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"); + } + } + } +} diff --git a/app/src/main/java/org/secuso/privacyfriendlybreakreminder/database/columns/ExerciseColumns.java b/app/src/main/java/org/secuso/privacyfriendlybreakreminder/database/columns/ExerciseColumns.java index 759d872..ce5a19d 100644 --- a/app/src/main/java/org/secuso/privacyfriendlybreakreminder/database/columns/ExerciseColumns.java +++ b/app/src/main/java/org/secuso/privacyfriendlybreakreminder/database/columns/ExerciseColumns.java @@ -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() {} } diff --git a/app/src/main/java/org/secuso/privacyfriendlybreakreminder/database/columns/ExercisesLocalColumns.java b/app/src/main/java/org/secuso/privacyfriendlybreakreminder/database/columns/ExerciseLocalColumns.java similarity index 64% rename from app/src/main/java/org/secuso/privacyfriendlybreakreminder/database/columns/ExercisesLocalColumns.java rename to app/src/main/java/org/secuso/privacyfriendlybreakreminder/database/columns/ExerciseLocalColumns.java index 5721b89..c58f322 100644 --- a/app/src/main/java/org/secuso/privacyfriendlybreakreminder/database/columns/ExercisesLocalColumns.java +++ b/app/src/main/java/org/secuso/privacyfriendlybreakreminder/database/columns/ExerciseLocalColumns.java @@ -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() {} } diff --git a/app/src/main/java/org/secuso/privacyfriendlybreakreminder/database/columns/ExerciseSetColumns.java b/app/src/main/java/org/secuso/privacyfriendlybreakreminder/database/columns/ExerciseSetColumns.java new file mode 100644 index 0000000..e14306b --- /dev/null +++ b/app/src/main/java/org/secuso/privacyfriendlybreakreminder/database/columns/ExerciseSetColumns.java @@ -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() {} +} diff --git a/app/src/main/java/org/secuso/privacyfriendlybreakreminder/database/data/Exercise.java b/app/src/main/java/org/secuso/privacyfriendlybreakreminder/database/data/Exercise.java index bd023d9..d43b898 100644 --- a/app/src/main/java/org/secuso/privacyfriendlybreakreminder/database/data/Exercise.java +++ b/app/src/main/java/org/secuso/privacyfriendlybreakreminder/database/data/Exercise.java @@ -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); + } } diff --git a/app/src/main/java/org/secuso/privacyfriendlybreakreminder/database/data/ExerciseSet.java b/app/src/main/java/org/secuso/privacyfriendlybreakreminder/database/data/ExerciseSet.java new file mode 100644 index 0000000..acf1568 --- /dev/null +++ b/app/src/main/java/org/secuso/privacyfriendlybreakreminder/database/data/ExerciseSet.java @@ -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 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(); + } + +} diff --git a/app/src/main/java/org/secuso/privacyfriendlybreakreminder/service/TimerService.java b/app/src/main/java/org/secuso/privacyfriendlybreakreminder/service/TimerService.java index 052041e..a7920fb 100644 --- a/app/src/main/java/org/secuso/privacyfriendlybreakreminder/service/TimerService.java +++ b/app/src/main/java/org/secuso/privacyfriendlybreakreminder/service/TimerService.java @@ -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; diff --git a/app/src/main/res/drawable-hdpi/ic_play_arrow_black_48dp.png b/app/src/main/res/drawable-hdpi/ic_play_arrow_black_48dp.png new file mode 100644 index 0000000..5345ee3 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_play_arrow_black_48dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_play_arrow_black_48dp.png b/app/src/main/res/drawable-mdpi/ic_play_arrow_black_48dp.png new file mode 100644 index 0000000..f208795 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_play_arrow_black_48dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_play_arrow_black_48dp.png b/app/src/main/res/drawable-xhdpi/ic_play_arrow_black_48dp.png new file mode 100644 index 0000000..d12d495 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_play_arrow_black_48dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_play_arrow_black_48dp.png b/app/src/main/res/drawable-xxhdpi/ic_play_arrow_black_48dp.png new file mode 100644 index 0000000..1c57756 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_play_arrow_black_48dp.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_play_arrow_black_48dp.png b/app/src/main/res/drawable-xxxhdpi/ic_play_arrow_black_48dp.png new file mode 100644 index 0000000..904bbdb Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_play_arrow_black_48dp.png differ diff --git a/app/src/main/res/drawable/circular_small.xml b/app/src/main/res/drawable/circular_small.xml new file mode 100644 index 0000000..7e6da40 --- /dev/null +++ b/app/src/main/res/drawable/circular_small.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_exercise.xml b/app/src/main/res/layout/activity_exercise.xml new file mode 100644 index 0000000..00ec4a7 --- /dev/null +++ b/app/src/main/res/layout/activity_exercise.xml @@ -0,0 +1,152 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_exercise_set.xml b/app/src/main/res/layout/activity_exercise_set.xml new file mode 100644 index 0000000..31a213e --- /dev/null +++ b/app/src/main/res/layout/activity_exercise_set.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_timer.xml b/app/src/main/res/layout/activity_timer.xml index db5923d..5d1f4be 100644 --- a/app/src/main/res/layout/activity_timer.xml +++ b/app/src/main/res/layout/activity_timer.xml @@ -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"> + + + + + android:layout_marginEnd="8dp" + app:layout_constraintHorizontal_bias="0.0" + android:layout_marginTop="0dp" + app:layout_constraintTop_toBottomOf="@id/exercise"> + + + + + + + + + + + + + + + android:visibility="invisible" /> + android:textSize="36sp" + android:textStyle="bold" + android:visibility="invisible" /> + app:layout_constraintHorizontal_bias="0.33" + android:layout_marginStart="8dp" + android:layout_marginEnd="8dp" /> + app:layout_constraintHorizontal_bias="0.66" + android:layout_marginStart="8dp" + android:layout_marginEnd="8dp" /> diff --git a/app/src/main/res/layout/layout_exercise.xml b/app/src/main/res/layout/layout_exercise.xml new file mode 100644 index 0000000..0448b91 --- /dev/null +++ b/app/src/main/res/layout/layout_exercise.xml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_exercise_set.xml b/app/src/main/res/layout/layout_exercise_set.xml new file mode 100644 index 0000000..3ae59d2 --- /dev/null +++ b/app/src/main/res/layout/layout_exercise_set.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 545b9c6..4934cf3 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -13,6 +13,13 @@ true + +