Repository created

This commit is contained in:
Phillip Hsu 2016-05-31 19:39:00 -07:00
parent 15b850d9bd
commit e80596060b
9 changed files with 372 additions and 14 deletions

View File

@ -1,6 +1,9 @@
package com.philliphsu.clock2; package com.philliphsu.clock2;
import android.support.annotation.NonNull;
import com.google.auto.value.AutoValue; import com.google.auto.value.AutoValue;
import com.philliphsu.clock2.model.JsonSerializable;
import org.json.JSONArray; import org.json.JSONArray;
import org.json.JSONException; import org.json.JSONException;
@ -16,12 +19,12 @@ import static com.philliphsu.clock2.DaysOfWeek.SUNDAY;
* Created by Phillip Hsu on 5/26/2016. * Created by Phillip Hsu on 5/26/2016.
*/ */
@AutoValue @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. private static final int MAX_MINUTES_CAN_SNOOZE = 30; // TODO: Delete this along with all snooze stuff.
// JSON property names // JSON property names
private static final String KEY_ENABLED = "enabled"; 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_HOUR = "hour";
private static final String KEY_MINUTES = "minutes"; private static final String KEY_MINUTES = "minutes";
private static final String KEY_RECURRING_DAYS = "recurring_days"; private static final String KEY_RECURRING_DAYS = "recurring_days";
@ -34,7 +37,7 @@ public abstract class Alarm {
private boolean enabled; 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 hour();
public abstract int minutes(); public abstract int minutes();
@SuppressWarnings("mutable") @SuppressWarnings("mutable")
@ -48,11 +51,16 @@ public abstract class Alarm {
public static Alarm create(JSONObject jsonObject) { public static Alarm create(JSONObject jsonObject) {
try { 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() Alarm alarm = new AutoValue_Alarm.Builder()
.id(jsonObject.getLong(KEY_ID)) .id(jsonObject.getLong(KEY_ID))
.hour(jsonObject.getInt(KEY_HOUR)) .hour(jsonObject.getInt(KEY_HOUR))
.minutes(jsonObject.getInt(KEY_MINUTES)) .minutes(jsonObject.getInt(KEY_MINUTES))
.recurringDays((boolean[]) jsonObject.get(KEY_RECURRING_DAYS)) .recurringDays(recurringDays)
.label(jsonObject.getString(KEY_LABEL)) .label(jsonObject.getString(KEY_LABEL))
.ringtone(jsonObject.getString(KEY_RINGTONE)) .ringtone(jsonObject.getString(KEY_RINGTONE))
.vibrates(jsonObject.getBoolean(KEY_VIBRATES)) .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. // Fields that were not set when build() is called will throw an exception.
// TODO: How can QualityMatters get away with not setting defaults????? // TODO: How can QualityMatters get away with not setting defaults?????
return new AutoValue_Alarm.Builder() return new AutoValue_Alarm.Builder()
.id(-1) //.id(-1) // Set when build() is called
.hour(0) .hour(0)
.minutes(0) .minutes(0)
.recurringDays(new boolean[DaysOfWeek.NUM_DAYS]) .recurringDays(new boolean[DaysOfWeek.NUM_DAYS])
@ -78,9 +86,6 @@ public abstract class Alarm {
.vibrates(false); .vibrates(false);
} }
// --------------------------------------------------------------
// TODO: Snoozing functionality not necessary. Delete methods.
public void snooze(int minutes) { public void snooze(int minutes) {
if (minutes <= 0 || minutes > MAX_MINUTES_CAN_SNOOZE) if (minutes <= 0 || minutes > MAX_MINUTES_CAN_SNOOZE)
throw new IllegalArgumentException("Cannot snooze for "+minutes+" minutes"); throw new IllegalArgumentException("Cannot snooze for "+minutes+" minutes");
@ -99,8 +104,6 @@ public abstract class Alarm {
return true; return true;
} }
// --------------------------------------------------------------
public void setEnabled(boolean enabled) { public void setEnabled(boolean enabled) {
this.enabled = enabled; this.enabled = enabled;
} }
@ -169,6 +172,8 @@ public abstract class Alarm {
return ringsIn() <= hours * 3600000; return ringsIn() <= hours * 3600000;
} }
@Override
@NonNull
public JSONObject toJsonObject() { public JSONObject toJsonObject() {
try { try {
return new JSONObject() return new JSONObject()
@ -187,6 +192,7 @@ public abstract class Alarm {
@AutoValue.Builder @AutoValue.Builder
public abstract static class Builder { public abstract static class Builder {
private static long idCount = 0;
// Builder is mutable, so these are inherently setter methods. // 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 // 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. // class after copying and pasting the accessor fields here.
@ -212,6 +218,7 @@ public abstract class Alarm {
/*not public*/abstract Alarm autoBuild(); /*not public*/abstract Alarm autoBuild();
public Alarm build() { public Alarm build() {
this.id(idCount++);
Alarm alarm = autoBuild(); Alarm alarm = autoBuild();
checkTime(alarm.hour(), alarm.minutes()); checkTime(alarm.hour(), alarm.minutes());
return alarm; return alarm;

View File

@ -13,7 +13,8 @@ import android.view.ViewGroup;
import com.philliphsu.clock2.Alarm; import com.philliphsu.clock2.Alarm;
import com.philliphsu.clock2.OnListItemInteractionListener; import com.philliphsu.clock2.OnListItemInteractionListener;
import com.philliphsu.clock2.R; 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. * 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} * Activities containing this fragment MUST implement the {@link OnAlarmInteractionListener}
* interface. * interface.
*/ */
public class AlarmsFragment extends Fragment { public class AlarmsFragment extends Fragment implements BaseRepository.DataObserver<Alarm> {
// TODO: Customize parameter argument names // TODO: Customize parameter argument names
private static final String ARG_COLUMN_COUNT = "column-count"; private static final String ARG_COLUMN_COUNT = "column-count";
@ -29,6 +30,9 @@ public class AlarmsFragment extends Fragment {
private int mColumnCount = 1; private int mColumnCount = 1;
private OnAlarmInteractionListener mListener; private OnAlarmInteractionListener mListener;
private AlarmsAdapter mAdapter;
private AlarmsRepository mRepo;
/** /**
* Mandatory empty constructor for the fragment manager to instantiate the * Mandatory empty constructor for the fragment manager to instantiate the
* fragment (e.g. upon screen orientation changes). * fragment (e.g. upon screen orientation changes).
@ -68,17 +72,19 @@ public class AlarmsFragment extends Fragment {
} else { } else {
recyclerView.setLayoutManager(new GridLayoutManager(context, mColumnCount)); recyclerView.setLayoutManager(new GridLayoutManager(context, mColumnCount));
} }
recyclerView.setAdapter(new AlarmsAdapter(DummyContent.ITEMS, mListener)); mAdapter = new AlarmsAdapter(mRepo.getItems(), mListener);
recyclerView.setAdapter(mAdapter);
} }
return view; return view;
} }
@Override @Override
public void onAttach(Context context) { public void onAttach(Context context) {
super.onAttach(context); super.onAttach(context);
if (context instanceof OnAlarmInteractionListener) { if (context instanceof OnAlarmInteractionListener) {
mListener = (OnAlarmInteractionListener) context; mListener = (OnAlarmInteractionListener) context;
mRepo = AlarmsRepository.getInstance(context);
mRepo.registerDataObserver(this);
} else { } else {
throw new RuntimeException(context.toString() throw new RuntimeException(context.toString()
+ " must implement OnAlarmInteractionListener"); + " must implement OnAlarmInteractionListener");
@ -91,6 +97,21 @@ public class AlarmsFragment extends Fragment {
mListener = null; 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 * This interface must be implemented by activities that contain this
* fragment to allow an interaction in this fragment to be communicated * fragment to allow an interaction in this fragment to be communicated

View File

@ -7,11 +7,14 @@ import android.widget.CheckBox;
import android.widget.EditText; import android.widget.EditText;
import android.widget.ToggleButton; import android.widget.ToggleButton;
import com.philliphsu.clock2.Alarm;
import com.philliphsu.clock2.BaseActivity; import com.philliphsu.clock2.BaseActivity;
import com.philliphsu.clock2.DaysOfWeek; import com.philliphsu.clock2.DaysOfWeek;
import com.philliphsu.clock2.R; import com.philliphsu.clock2.R;
import com.philliphsu.clock2.model.AlarmsRepository;
import butterknife.Bind; import butterknife.Bind;
import butterknife.OnClick;
public class EditAlarmActivity extends BaseActivity { public class EditAlarmActivity extends BaseActivity {
@ -47,6 +50,11 @@ public class EditAlarmActivity extends BaseActivity {
return true; return true;
} }
@OnClick(R.id.save)
void save() {
AlarmsRepository.getInstance(this).addItem(Alarm.builder().build());
}
private void setWeekDaysText() { private void setWeekDaysText() {
for (int i = 0; i < mDays.length; i++) { for (int i = 0; i < mDays.length; i++) {
int weekDay = DaysOfWeek.getInstance(this).weekDay(i); int weekDay = DaysOfWeek.getInstance(this).weekDay(i);

View File

@ -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<Alarm> {
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);
}
}

View File

@ -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<Alarm> {
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;
}
}

View File

@ -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<T extends JsonSerializable> implements Repository<T> {
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<T> 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<T> mItems;
@NonNull private final JsonIoHelper<T> mIoHelper;
// TODO: Test that the callbacks work.
private DataObserver<T> 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<T2> {
void onItemAdded(T2 item);
void onItemDeleted(T2 item);
void onItemUpdated(T2 oldItem, T2 newItem);
}
/*package-private*/ BaseRepository(@NonNull Context context,
@NonNull JsonIoHelper<T> 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<T> 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<T> observer) {
mDataObserver = observer;
}
// TODO: Do we need to call this?
public final void unregisterDataObserver() {
mDataObserver = null;
}
@NonNull
private List<T> loadItems() {
try {
return mIoHelper.loadItems();
} catch (IOException e) {
Log.e(TAG, "Error loading items from file: " + e);
return new ArrayList<>();
}
}
}

View File

@ -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<T extends JsonSerializable> {
@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<T> loadItems() throws IOException {
ArrayList<T> 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<T> 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
}
}
}
}

View File

@ -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();
}

View File

@ -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<T> {
@NonNull List<T> 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();
}