From e80596060ba79a597e1de456d40c6e3bc0963e7f Mon Sep 17 00:00:00 2001 From: Phillip Hsu Date: Tue, 31 May 2016 19:39:00 -0700 Subject: [PATCH] Repository created --- .../java/com/philliphsu/clock2/Alarm.java | 27 ++-- .../clock2/alarms/AlarmsFragment.java | 29 +++- .../clock2/editalarm/EditAlarmActivity.java | 8 ++ .../clock2/model/AlarmIoHelper.java | 24 ++++ .../clock2/model/AlarmsRepository.java | 36 +++++ .../clock2/model/BaseRepository.java | 134 ++++++++++++++++++ .../philliphsu/clock2/model/JsonIoHelper.java | 93 ++++++++++++ .../clock2/model/JsonSerializable.java | 16 +++ .../philliphsu/clock2/model/Repository.java | 19 +++ 9 files changed, 372 insertions(+), 14 deletions(-) create mode 100644 app/src/main/java/com/philliphsu/clock2/model/AlarmIoHelper.java create mode 100644 app/src/main/java/com/philliphsu/clock2/model/AlarmsRepository.java create mode 100644 app/src/main/java/com/philliphsu/clock2/model/BaseRepository.java create mode 100644 app/src/main/java/com/philliphsu/clock2/model/JsonIoHelper.java create mode 100644 app/src/main/java/com/philliphsu/clock2/model/JsonSerializable.java create mode 100644 app/src/main/java/com/philliphsu/clock2/model/Repository.java diff --git a/app/src/main/java/com/philliphsu/clock2/Alarm.java b/app/src/main/java/com/philliphsu/clock2/Alarm.java index bdce061..46e65a9 100644 --- a/app/src/main/java/com/philliphsu/clock2/Alarm.java +++ b/app/src/main/java/com/philliphsu/clock2/Alarm.java @@ -1,6 +1,9 @@ package com.philliphsu.clock2; +import android.support.annotation.NonNull; + import com.google.auto.value.AutoValue; +import com.philliphsu.clock2.model.JsonSerializable; import org.json.JSONArray; import org.json.JSONException; @@ -16,12 +19,12 @@ import static com.philliphsu.clock2.DaysOfWeek.SUNDAY; * Created by Phillip Hsu on 5/26/2016. */ @AutoValue -public abstract class Alarm { +public abstract class Alarm implements JsonSerializable { private static final int MAX_MINUTES_CAN_SNOOZE = 30; // TODO: Delete this along with all snooze stuff. // JSON property names private static final String KEY_ENABLED = "enabled"; - private static final String KEY_ID = "id"; + //private static final String KEY_ID = "id"; // Defined in JsonSerializable private static final String KEY_HOUR = "hour"; private static final String KEY_MINUTES = "minutes"; private static final String KEY_RECURRING_DAYS = "recurring_days"; @@ -34,7 +37,7 @@ public abstract class Alarm { private boolean enabled; // ================================ - public abstract long id(); // TODO: Counter in the repository. Set this field as the repo creates instances. + //public abstract long id(); public abstract int hour(); public abstract int minutes(); @SuppressWarnings("mutable") @@ -48,11 +51,16 @@ public abstract class Alarm { public static Alarm create(JSONObject jsonObject) { try { + JSONArray a = (JSONArray) jsonObject.get(KEY_RECURRING_DAYS); + boolean[] recurringDays = new boolean[a.length()]; + for (int i = 0; i < recurringDays.length; i++) { + recurringDays[i] = a.getBoolean(i); + } Alarm alarm = new AutoValue_Alarm.Builder() .id(jsonObject.getLong(KEY_ID)) .hour(jsonObject.getInt(KEY_HOUR)) .minutes(jsonObject.getInt(KEY_MINUTES)) - .recurringDays((boolean[]) jsonObject.get(KEY_RECURRING_DAYS)) + .recurringDays(recurringDays) .label(jsonObject.getString(KEY_LABEL)) .ringtone(jsonObject.getString(KEY_RINGTONE)) .vibrates(jsonObject.getBoolean(KEY_VIBRATES)) @@ -69,7 +77,7 @@ public abstract class Alarm { // Fields that were not set when build() is called will throw an exception. // TODO: How can QualityMatters get away with not setting defaults????? return new AutoValue_Alarm.Builder() - .id(-1) + //.id(-1) // Set when build() is called .hour(0) .minutes(0) .recurringDays(new boolean[DaysOfWeek.NUM_DAYS]) @@ -78,9 +86,6 @@ public abstract class Alarm { .vibrates(false); } - // -------------------------------------------------------------- - // TODO: Snoozing functionality not necessary. Delete methods. - public void snooze(int minutes) { if (minutes <= 0 || minutes > MAX_MINUTES_CAN_SNOOZE) throw new IllegalArgumentException("Cannot snooze for "+minutes+" minutes"); @@ -99,8 +104,6 @@ public abstract class Alarm { return true; } - // -------------------------------------------------------------- - public void setEnabled(boolean enabled) { this.enabled = enabled; } @@ -169,6 +172,8 @@ public abstract class Alarm { return ringsIn() <= hours * 3600000; } + @Override + @NonNull public JSONObject toJsonObject() { try { return new JSONObject() @@ -187,6 +192,7 @@ public abstract class Alarm { @AutoValue.Builder public abstract static class Builder { + private static long idCount = 0; // Builder is mutable, so these are inherently setter methods. // By omitting the set- prefix, we reduce the number of changes required to define the Builder // class after copying and pasting the accessor fields here. @@ -212,6 +218,7 @@ public abstract class Alarm { /*not public*/abstract Alarm autoBuild(); public Alarm build() { + this.id(idCount++); Alarm alarm = autoBuild(); checkTime(alarm.hour(), alarm.minutes()); return alarm; diff --git a/app/src/main/java/com/philliphsu/clock2/alarms/AlarmsFragment.java b/app/src/main/java/com/philliphsu/clock2/alarms/AlarmsFragment.java index 7411fb4..cc876e0 100644 --- a/app/src/main/java/com/philliphsu/clock2/alarms/AlarmsFragment.java +++ b/app/src/main/java/com/philliphsu/clock2/alarms/AlarmsFragment.java @@ -13,7 +13,8 @@ import android.view.ViewGroup; import com.philliphsu.clock2.Alarm; import com.philliphsu.clock2.OnListItemInteractionListener; import com.philliphsu.clock2.R; -import com.philliphsu.clock2.alarms.dummy.DummyContent; +import com.philliphsu.clock2.model.AlarmsRepository; +import com.philliphsu.clock2.model.BaseRepository; /** * A fragment representing a list of Items. @@ -21,7 +22,7 @@ import com.philliphsu.clock2.alarms.dummy.DummyContent; * Activities containing this fragment MUST implement the {@link OnAlarmInteractionListener} * interface. */ -public class AlarmsFragment extends Fragment { +public class AlarmsFragment extends Fragment implements BaseRepository.DataObserver { // TODO: Customize parameter argument names private static final String ARG_COLUMN_COUNT = "column-count"; @@ -29,6 +30,9 @@ public class AlarmsFragment extends Fragment { private int mColumnCount = 1; private OnAlarmInteractionListener mListener; + private AlarmsAdapter mAdapter; + private AlarmsRepository mRepo; + /** * Mandatory empty constructor for the fragment manager to instantiate the * fragment (e.g. upon screen orientation changes). @@ -68,17 +72,19 @@ public class AlarmsFragment extends Fragment { } else { recyclerView.setLayoutManager(new GridLayoutManager(context, mColumnCount)); } - recyclerView.setAdapter(new AlarmsAdapter(DummyContent.ITEMS, mListener)); + mAdapter = new AlarmsAdapter(mRepo.getItems(), mListener); + recyclerView.setAdapter(mAdapter); } return view; } - @Override public void onAttach(Context context) { super.onAttach(context); if (context instanceof OnAlarmInteractionListener) { mListener = (OnAlarmInteractionListener) context; + mRepo = AlarmsRepository.getInstance(context); + mRepo.registerDataObserver(this); } else { throw new RuntimeException(context.toString() + " must implement OnAlarmInteractionListener"); @@ -91,6 +97,21 @@ public class AlarmsFragment extends Fragment { mListener = null; } + @Override + public void onItemAdded(Alarm item) { + mAdapter.addItem(item); + } + + @Override + public void onItemDeleted(Alarm item) { + mAdapter.removeItem(item); + } + + @Override + public void onItemUpdated(Alarm oldItem, Alarm newItem) { + mAdapter.updateItem(oldItem, newItem); + } + /** * This interface must be implemented by activities that contain this * fragment to allow an interaction in this fragment to be communicated diff --git a/app/src/main/java/com/philliphsu/clock2/editalarm/EditAlarmActivity.java b/app/src/main/java/com/philliphsu/clock2/editalarm/EditAlarmActivity.java index cc9e7bb..b049d10 100644 --- a/app/src/main/java/com/philliphsu/clock2/editalarm/EditAlarmActivity.java +++ b/app/src/main/java/com/philliphsu/clock2/editalarm/EditAlarmActivity.java @@ -7,11 +7,14 @@ import android.widget.CheckBox; import android.widget.EditText; import android.widget.ToggleButton; +import com.philliphsu.clock2.Alarm; import com.philliphsu.clock2.BaseActivity; import com.philliphsu.clock2.DaysOfWeek; import com.philliphsu.clock2.R; +import com.philliphsu.clock2.model.AlarmsRepository; import butterknife.Bind; +import butterknife.OnClick; public class EditAlarmActivity extends BaseActivity { @@ -47,6 +50,11 @@ public class EditAlarmActivity extends BaseActivity { return true; } + @OnClick(R.id.save) + void save() { + AlarmsRepository.getInstance(this).addItem(Alarm.builder().build()); + } + private void setWeekDaysText() { for (int i = 0; i < mDays.length; i++) { int weekDay = DaysOfWeek.getInstance(this).weekDay(i); diff --git a/app/src/main/java/com/philliphsu/clock2/model/AlarmIoHelper.java b/app/src/main/java/com/philliphsu/clock2/model/AlarmIoHelper.java new file mode 100644 index 0000000..7c96927 --- /dev/null +++ b/app/src/main/java/com/philliphsu/clock2/model/AlarmIoHelper.java @@ -0,0 +1,24 @@ +package com.philliphsu.clock2.model; + +import android.content.Context; +import android.support.annotation.NonNull; + +import com.philliphsu.clock2.Alarm; + +import org.json.JSONObject; + +/** + * Created by Phillip Hsu on 5/31/2016. + */ +public class AlarmIoHelper extends JsonIoHelper { + private static final String FILENAME = "alarms.json"; + + public AlarmIoHelper(@NonNull Context context) { + super(context, FILENAME); + } + + @Override + protected Alarm newItem(@NonNull JSONObject jsonObject) { + return Alarm.create(jsonObject); + } +} diff --git a/app/src/main/java/com/philliphsu/clock2/model/AlarmsRepository.java b/app/src/main/java/com/philliphsu/clock2/model/AlarmsRepository.java new file mode 100644 index 0000000..3c2fb4b --- /dev/null +++ b/app/src/main/java/com/philliphsu/clock2/model/AlarmsRepository.java @@ -0,0 +1,36 @@ +package com.philliphsu.clock2.model; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.util.Log; + +import com.philliphsu.clock2.Alarm; + +/** + * Created by Phillip Hsu on 5/31/2016. + */ +public class AlarmsRepository extends BaseRepository { + private static final String TAG = "AlarmsRepository"; + // Singleton, so this is the sole instance for the lifetime + // of the application; thus, instance fields do not need to + // be declared static because they are already associated with + // this single instance. Since no other instance can exist, + // any member fields are effectively class fields. + // ** + // Can't be final, otherwise you'd need to instantiate inline + // or in static initializer, but ctor requires Context so you + // can't do that either. + private static AlarmsRepository sRepo; + + private AlarmsRepository(@NonNull Context context) { + super(context, new AlarmIoHelper(context)); + } + + public static AlarmsRepository getInstance(@NonNull Context context) { + if (null == sRepo) { + Log.d(TAG, "Loading AlarmsRepository for the first time"); + sRepo = new AlarmsRepository(context); + } + return sRepo; + } +} diff --git a/app/src/main/java/com/philliphsu/clock2/model/BaseRepository.java b/app/src/main/java/com/philliphsu/clock2/model/BaseRepository.java new file mode 100644 index 0000000..324f98a --- /dev/null +++ b/app/src/main/java/com/philliphsu/clock2/model/BaseRepository.java @@ -0,0 +1,134 @@ +package com.philliphsu.clock2.model; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.util.Log; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Created by Phillip Hsu on 5/31/2016. + */ +public abstract class BaseRepository implements Repository { + private static final String TAG = "BaseRepository"; + // Cannot do this! Multiple classes will extend from this, + // so this "singleton" would be a class property for all of them. + //private static BaseRepositoryV2 sInstance; + + // Never used since subclasses provide the ioHelper, but I think + // the intention is that we hold onto the global context so this + // never gets GCed for the lifetime of the app. + @NonNull private final Context mContext; + @NonNull private final List mItems; + @NonNull private final JsonIoHelper mIoHelper; + + // TODO: Test that the callbacks work. + private DataObserver mDataObserver; + + // We could use T but since it's already defined, we should avoid + // the needless confusion and use a different type param. You won't + // be able to refer to the type that T resolves to anyway. + public interface DataObserver { + void onItemAdded(T2 item); + void onItemDeleted(T2 item); + void onItemUpdated(T2 oldItem, T2 newItem); + } + + /*package-private*/ BaseRepository(@NonNull Context context, + @NonNull JsonIoHelper ioHelper) { + Log.d(TAG, "BaseRepositoryV2 initialized"); + mContext = context.getApplicationContext(); + mIoHelper = ioHelper; // MUST precede loading items + mItems = loadItems(); // TOneverDO: move this elsewhere + } + + @Override @NonNull + public List getItems() { + return Collections.unmodifiableList(mItems); + } + + @Nullable + @Override + public T getItem(long id) { + for (T item : getItems()) + if (item.id() == id) + return item; + return null; + } + + @Override + public final void addItem(@NonNull T item) { + Log.d(TAG, "New item added"); + mItems.add(item); + mDataObserver.onItemAdded(item); // TODO: Set StopwatchView as DataObserver + saveItems(); + } + + @Override + public final void deleteItem(@NonNull T item) { + if (!mItems.remove(item)) { + Log.e(TAG, "Cannot remove an item that is not in the list"); + } else { + mDataObserver.onItemDeleted(item); // TODO: Set StopwatchView as DataObserver + saveItems(); + } + } + + @Override + public final void updateItem(@NonNull T item1, @NonNull T item2) { + // TODO: Won't work unless objects are immutable, so item1 + // can't change and thus its index will never change + // ** + // Actually, since the items come from this list, + // modifications to items will directly "propagate". + // In the process, the index of that modified item + // has not changed. If that's the case, there really + // isn't any point for an update method, especially + // since item2 would be unnecessary and won't even need + // to be used. + mItems.set(mItems.indexOf(item1), item2); + mDataObserver.onItemUpdated(item1, item2); // TODO: Set StopwatchView as DataObserver + saveItems(); + } + + @Override + public final boolean saveItems() { + try { + mIoHelper.saveItems(mItems); + Log.d(TAG, "Saved items to file"); + return true; + } catch (IOException e) { + Log.e(TAG, "Error writing items to file: " + e); + return false; + } + } + + @Override + public final void clear() { + mItems.clear(); + saveItems(); + } + + public final void registerDataObserver(@NonNull DataObserver observer) { + mDataObserver = observer; + } + + // TODO: Do we need to call this? + public final void unregisterDataObserver() { + mDataObserver = null; + } + + @NonNull + private List loadItems() { + try { + return mIoHelper.loadItems(); + } catch (IOException e) { + Log.e(TAG, "Error loading items from file: " + e); + return new ArrayList<>(); + } + } +} diff --git a/app/src/main/java/com/philliphsu/clock2/model/JsonIoHelper.java b/app/src/main/java/com/philliphsu/clock2/model/JsonIoHelper.java new file mode 100644 index 0000000..c43b522 --- /dev/null +++ b/app/src/main/java/com/philliphsu/clock2/model/JsonIoHelper.java @@ -0,0 +1,93 @@ +package com.philliphsu.clock2.model; + +import android.content.Context; +import android.support.annotation.NonNull; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.json.JSONTokener; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.util.ArrayList; +import java.util.List; + +/** + * Created by Phillip Hsu on 5/31/2016. + */ +public abstract class JsonIoHelper { + @NonNull private final Context mContext; + @NonNull private final String mFilename; + + public JsonIoHelper(@NonNull Context context, + @NonNull String filename) { + mContext = context.getApplicationContext(); + mFilename = filename; + } + + protected abstract T newItem(@NonNull JSONObject jsonObject); + + public final List loadItems() throws IOException { + ArrayList items = new ArrayList<>(); + BufferedReader reader = null; + try { + // Opens the file in a FileInputStream for byte-reading + InputStream in = mContext.openFileInput(mFilename); + // Use an InputStreamReader to convert bytes to characters. A BufferedReader wraps the + // existing Reader and provides a buffer (a cache) for storing the characters. + // From https://docs.oracle.com/javase/7/docs/api/java/io/BufferedReader.html: + // "In general, each read request made of a Reader causes a corresponding read request + // to be made of the underlying character or byte stream. It is therefore advisable to + // wrap a BufferedReader around any Reader whose read() operations may be costly, such as + // FileReaders and InputStreamReaders." + reader = new BufferedReader(new InputStreamReader(in)); + StringBuilder jsonString = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + // Line breaks are omitted and irrelevant + jsonString.append(line); + } + // JSONTokener parses a String in JSON "notation" into a "JSON-compatible" object. + // JSON objects are instances of JSONObject and JSONArray. You actually have to call + // nextValue() on the returned Tokener to get the corresponding JSON object. + JSONArray array = (JSONArray) new JSONTokener(jsonString.toString()).nextValue(); + for (int i = 0; i < array.length(); i++) { + items.add(newItem(array.getJSONObject(i))); + } + } catch (JSONException e) { + throw new RuntimeException(e); + } finally { + if (reader != null) + reader.close(); + } + return items; + } + + public final void saveItems(@NonNull List items) throws IOException { + // Convert items to JSONObjects and store in a JSONArray + JSONArray array = new JSONArray(); + try { + for (T item : items) { + array.put(item.toJsonObject()); + } + } catch (JSONException e) { + throw new RuntimeException(e); + } + + OutputStreamWriter writer = null; + try { + // Create a character stream from the byte stream + writer = new OutputStreamWriter(mContext.openFileOutput(mFilename, Context.MODE_PRIVATE)); + // Write JSONArray to file + writer.write(array.toString()); + } finally { + if (writer != null) { + writer.close(); // also calls close on the byte stream + } + } + } +} diff --git a/app/src/main/java/com/philliphsu/clock2/model/JsonSerializable.java b/app/src/main/java/com/philliphsu/clock2/model/JsonSerializable.java new file mode 100644 index 0000000..a24e89c --- /dev/null +++ b/app/src/main/java/com/philliphsu/clock2/model/JsonSerializable.java @@ -0,0 +1,16 @@ +package com.philliphsu.clock2.model; + +import android.support.annotation.NonNull; + +import org.json.JSONException; +import org.json.JSONObject; + +/** + * Created by Phillip Hsu on 5/31/2016. + */ +public interface JsonSerializable { + String KEY_ID = "id"; + + @NonNull JSONObject toJsonObject() throws JSONException; + long id(); +} diff --git a/app/src/main/java/com/philliphsu/clock2/model/Repository.java b/app/src/main/java/com/philliphsu/clock2/model/Repository.java new file mode 100644 index 0000000..a5bbda0 --- /dev/null +++ b/app/src/main/java/com/philliphsu/clock2/model/Repository.java @@ -0,0 +1,19 @@ +package com.philliphsu.clock2.model; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import java.util.List; + +/** + * Created by Phillip Hsu on 5/31/2016. + */ +public interface Repository { + @NonNull List getItems(); + @Nullable T getItem(long id); + void addItem(@NonNull T item); + void deleteItem(@NonNull T item); + void updateItem(@NonNull T item1, @NonNull T item2); + boolean saveItems(); + void clear(); +}