Created DaysOfWeek class

This commit is contained in:
Phillip Hsu
2016-05-31 00:29:15 -07:00
parent a8e2a88140
commit ba41c1311e
17 changed files with 521 additions and 154 deletions
@@ -2,30 +2,38 @@ package com.philliphsu.clock2;
import com.google.auto.value.AutoValue;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.Calendar;
import java.util.GregorianCalendar;
import static com.philliphsu.clock2.DaysOfWeek.SATURDAY;
import static com.philliphsu.clock2.DaysOfWeek.SUNDAY;
/**
* Created by Phillip Hsu on 5/26/2016.
*/
@AutoValue
public abstract class Alarm {
private static final int MAX_MINUTES_CAN_SNOOZE = 30;
// Define our own day constants because those in the
// Calendar class are not zero-based.
public static final int SUNDAY = 0;
public static final int MONDAY = 1;
public static final int TUESDAY = 2;
public static final int WEDNESDAY = 3;
public static final int THURSDAY = 4;
public static final int FRIDAY = 5;
public static final int SATURDAY = 6;
public static final int NUM_DAYS = 7;
private static final int MAX_MINUTES_CAN_SNOOZE = 30; // TODO: Delete this along with all snooze stuff.
// =========== MUTABLE ===========
private long snoozingUntilMillis; // TODO: Not necessary? Can just schedule another alarm w/ AlarmManager.
// JSON property names
private static final String KEY_ENABLED = "enabled";
private static final String KEY_ID = "id";
private static final String KEY_HOUR = "hour";
private static final String KEY_MINUTES = "minutes";
private static final String KEY_RECURRING_DAYS = "recurring_days";
private static final String KEY_LABEL = "label";
private static final String KEY_RINGTONE = "ringtone";
private static final String KEY_VIBRATES = "vibrates";
// ========= MUTABLE ==============
private long snoozingUntilMillis;
private boolean enabled;
// ===============================
// ================================
public abstract long id(); // TODO: Counter in the repository. Set this field as the repo creates instances.
public abstract int hour();
public abstract int minutes();
@@ -38,6 +46,24 @@ public abstract class Alarm {
/** Initializes a Builder to the same property values as this instance */
public abstract Builder toBuilder();
public static Alarm create(JSONObject jsonObject) {
try {
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))
.label(jsonObject.getString(KEY_LABEL))
.ringtone(jsonObject.getString(KEY_RINGTONE))
.vibrates(jsonObject.getBoolean(KEY_VIBRATES))
.build();
alarm.setEnabled(jsonObject.getBoolean(KEY_ENABLED));
return alarm;
} catch (JSONException e) {
throw new RuntimeException(e);
}
}
public static Builder builder() {
// Unfortunately, default values must be provided for generated Builders.
// Fields that were not set when build() is called will throw an exception.
@@ -46,12 +72,15 @@ public abstract class Alarm {
.id(-1)
.hour(0)
.minutes(0)
.recurringDays(new boolean[NUM_DAYS])
.recurringDays(new boolean[DaysOfWeek.NUM_DAYS])
.label("")
.ringtone("")
.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");
@@ -59,7 +88,7 @@ public abstract class Alarm {
}
public long snoozingUntil() {
return snoozingUntilMillis;
return isSnoozed() ? snoozingUntilMillis : 0;
}
public boolean isSnoozed() {
@@ -70,6 +99,8 @@ public abstract class Alarm {
return true;
}
// --------------------------------------------------------------
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
@@ -89,9 +120,14 @@ public abstract class Alarm {
}
public boolean hasRecurrence() {
return numRecurringDays() > 0;
}
public int numRecurringDays() {
int count = 0;
for (boolean b : recurringDays())
if (b) return true;
return false;
if (b) count++;
return count;
}
public long ringsAt() {
@@ -133,6 +169,22 @@ public abstract class Alarm {
return ringsIn() <= hours * 3600000;
}
public JSONObject toJsonObject() {
try {
return new JSONObject()
.put(KEY_ENABLED, enabled)
.put(KEY_ID, id())
.put(KEY_HOUR, hour())
.put(KEY_MINUTES, minutes())
.put(KEY_RECURRING_DAYS, new JSONArray(recurringDays()))
.put(KEY_LABEL, label())
.put(KEY_RINGTONE, ringtone())
.put(KEY_VIBRATES, vibrates());
} catch (JSONException e) {
throw new RuntimeException(e);
}
}
@AutoValue.Builder
public abstract static class Builder {
// Builder is mutable, so these are inherently setter methods.
@@ -166,14 +218,15 @@ public abstract class Alarm {
}
}
private static void checkDay(int day) {
if (day < SUNDAY || day > SATURDAY) {
throw new IllegalArgumentException("Invalid day of week: " + day);
}
}
private static void checkTime(int hour, int minutes) {
if (hour < 0 || hour > 23 || minutes < 0 || minutes > 59) {
throw new IllegalStateException("Hour and minutes invalid");
}
}
private static void checkDay(int day) {
if (day < SUNDAY || day > SATURDAY)
throw new IllegalArgumentException("Invalid day " + day);
}
}
@@ -0,0 +1,93 @@
package com.philliphsu.clock2;
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.util.Log;
import java.util.Arrays;
/**
* Created by Phillip Hsu on 5/30/2016.
*/
public class DaysOfWeek {
private static final String TAG = "DaysOfWeek";
// DAY_OF_WEEK constants in Calendar class not zero-based
public static final int SUNDAY = 0;
public static final int MONDAY = 1;
public static final int TUESDAY = 2;
public static final int WEDNESDAY = 3;
public static final int THURSDAY = 4;
public static final int FRIDAY = 5;
public static final int SATURDAY = 6;
public static final int NUM_DAYS = 7;
private static final int[] DAYS = new int[NUM_DAYS];
private static final int[] LABELS_RES = new int[NUM_DAYS];
static {
LABELS_RES[SUNDAY] = R.string.sun;
LABELS_RES[MONDAY] = R.string.mon;
LABELS_RES[TUESDAY] = R.string.tue;
LABELS_RES[WEDNESDAY] = R.string.wed;
LABELS_RES[THURSDAY] = R.string.thu;
LABELS_RES[FRIDAY] = R.string.fri;
LABELS_RES[SATURDAY] = R.string.sat;
}
private static Context sAppContext;
private static DaysOfWeek sInstance;
private static String sLastPreferredFirstDay;
public static DaysOfWeek getInstance(Context context) {
sAppContext = context.getApplicationContext();
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
// TODO First day of week preference. Entries are the full days' names and values are their respective integers.
String preferredFirstDay = prefs.getString("", "1");
if (sInstance == null || !preferredFirstDay.equals(sLastPreferredFirstDay)) {
sLastPreferredFirstDay = preferredFirstDay;
sInstance = new DaysOfWeek(Integer.parseInt(preferredFirstDay));
}
Log.d(TAG, sInstance.toString());
return sInstance;
}
/** @param weekDay the day constant as defined in this class */
public static String getLabel(int weekDay) {
return sAppContext.getString(LABELS_RES[weekDay]);
}
/** @return the week day whose order is specified by {@code ordinalDay} */
// Compiler complains if annotated with @Day
public int weekDay(int ordinalDay) {
if (ordinalDay < 0 || ordinalDay > 6)
throw new ArrayIndexOutOfBoundsException("Ordinal day out of range");
return DAYS[ordinalDay];
}
@Override
public String toString() {
return "DaysOfWeek{"
+ "DAYS=" + Arrays.toString(DAYS)
+ "}";
}
private DaysOfWeek(int firstDayOfWeek /*Compiler complains if annotated with @Day*/) {
if (firstDayOfWeek != SATURDAY && firstDayOfWeek != SUNDAY && firstDayOfWeek != MONDAY)
throw new IllegalArgumentException("Invalid first day of week: " + firstDayOfWeek);
DAYS[0] = firstDayOfWeek;
for (int i = 1; i < 7; i++) {
if (firstDayOfWeek == SATURDAY) {
DAYS[i] = i - 1;
} else if (firstDayOfWeek == MONDAY) {
if (i == 6) {
DAYS[i] = SUNDAY;
} else {
DAYS[i] = i + 1;
}
} else {
DAYS[i] = i;
}
}
}
}
@@ -21,9 +21,10 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import com.philliphsu.clock2.alarms.AlarmsFragment;
import com.philliphsu.clock2.ringtone.RingtoneActivity;
public class MainActivity extends AppCompatActivity {
public class MainActivity extends AppCompatActivity implements AlarmsFragment.OnListFragmentInteractionListener {
/**
* The {@link android.support.v4.view.PagerAdapter} that will provide
@@ -153,7 +154,8 @@ public class MainActivity extends AppCompatActivity {
public Fragment getItem(int position) {
// getItem is called to instantiate the fragment for the given page.
// Return a PlaceholderFragment (defined as a static inner class below).
return PlaceholderFragment.newInstance(position + 1);
//return PlaceholderFragment.newInstance(position + 1);
return AlarmsFragment.newInstance(1);
}
@Override
@@ -176,6 +178,11 @@ public class MainActivity extends AppCompatActivity {
}
}
@Override
public void onListFragmentInteraction(Alarm item) {
// TODO react to click
}
private void scheduleAlarm() {
AlarmManager am = (AlarmManager) getSystemService(ALARM_SERVICE);
// If there is already an alarm for this Intent scheduled (with the equality of two
@@ -1,77 +1,186 @@
package com.philliphsu.clock2.alarms;
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.support.v7.util.SortedList;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.SwitchCompat;
import android.support.v7.widget.util.SortedListAdapterCallback;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.format.DateFormat;
import android.text.style.RelativeSizeSpan;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;
import com.philliphsu.clock2.Alarm;
import com.philliphsu.clock2.DaysOfWeek;
import com.philliphsu.clock2.R;
import com.philliphsu.clock2.alarms.dummy.DummyContent.DummyItem;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import butterknife.Bind;
import butterknife.ButterKnife;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static com.philliphsu.clock2.DaysOfWeek.NUM_DAYS;
/**
* {@link RecyclerView.Adapter} that can display a {@link DummyItem} and makes a call to the
* {@link RecyclerView.Adapter} that can display a {@link Alarm} and makes a call to the
* specified {@link AlarmsFragment.OnListFragmentInteractionListener}.
* TODO: Replace the implementation with code for your data type.
*/
public class AlarmsAdapter extends RecyclerView.Adapter<AlarmsAdapter.ViewHolder> {
private static final RelativeSizeSpan AMPM_SIZE_SPAN = new RelativeSizeSpan(0.5f);
private final List<DummyItem> mValues;
private final SortedList<Alarm> mItems;
private final AlarmsFragment.OnListFragmentInteractionListener mListener;
public AlarmsAdapter(List<DummyItem> items, AlarmsFragment.OnListFragmentInteractionListener listener) {
mValues = items;
public AlarmsAdapter(List<Alarm> alarms, AlarmsFragment.OnListFragmentInteractionListener listener) {
mItems = new SortedList<>(Alarm.class, new SortedListAdapterCallback<Alarm>(this) {
@Override
public int compare(Alarm o1, Alarm o2) {
return Long.compare(o1.ringsAt(), o2.ringsAt());
}
@Override
public boolean areContentsTheSame(Alarm oldItem, Alarm newItem) {
return oldItem.hour() == newItem.hour()
&& oldItem.minutes() == newItem.minutes()
&& oldItem.isEnabled() == newItem.isEnabled()
&& oldItem.label().equals(newItem.label())
&& oldItem.ringsIn() == newItem.ringsIn()
&& Arrays.equals(oldItem.recurringDays(), newItem.recurringDays())
&& oldItem.snoozingUntil() == newItem.snoozingUntil();
}
@Override
public boolean areItemsTheSame(Alarm item1, Alarm item2) {
return item1.id() == item2.id();
}
});
mItems.addAll(alarms);
mListener = listener;
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.fragment_alarms, parent, false);
return new ViewHolder(view);
.inflate(R.layout.item_alarm, parent, false);
return new ViewHolder(view, mListener);
}
@Override
public void onBindViewHolder(final ViewHolder holder, int position) {
holder.mItem = mValues.get(position);
holder.mIdView.setText(mValues.get(position).id);
holder.mContentView.setText(mValues.get(position).content);
holder.mView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (null != mListener) {
// Notify the active callbacks interface (the activity, if the
// fragment is attached to one) that an item has been selected.
mListener.onListFragmentInteraction(holder.mItem);
}
}
});
holder.onBind(mItems.get(position));
}
@Override
public int getItemCount() {
return mValues.size();
return mItems.size();
}
public class ViewHolder extends RecyclerView.ViewHolder {
public final View mView;
public final TextView mIdView;
public final TextView mContentView;
public DummyItem mItem;
static class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
private final Context mContext;
private final AlarmsFragment.OnListFragmentInteractionListener mListener;
private Alarm mItem;
public ViewHolder(View view) {
@Bind(R.id.time) TextView mTime;
@Bind(R.id.on_off_switch) SwitchCompat mSwitch;
@Bind(R.id.label) TextView mLabel;
@Bind(R.id.countdown) TextView mCountdown; // TODO: Change type to NextAlarmText, once you move that class to this project
@Bind(R.id.recurring_days) TextView mDays;
@Bind(R.id.dismiss) Button mDismissButton;
public ViewHolder(View view, AlarmsFragment.OnListFragmentInteractionListener listener) {
super(view);
mView = view;
mIdView = (TextView) view.findViewById(R.id.id);
mContentView = (TextView) view.findViewById(R.id.content);
ButterKnife.bind(this, view);
mContext = view.getContext();
mListener = listener;
view.setOnClickListener(this);
}
public void onBind(Alarm alarm) {
mItem = alarm;
String time = DateFormat.getTimeFormat(mContext).format(new Date(alarm.ringsAt()));
if (DateFormat.is24HourFormat(mContext)) {
mTime.setText(time);
} else {
// No way around having to construct this on binding
SpannableString s = new SpannableString(time);
s.setSpan(AMPM_SIZE_SPAN, time.indexOf(" "), time.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
mTime.setText(s, TextView.BufferType.SPANNABLE);
}
if (alarm.isEnabled()) {
mSwitch.setChecked(true);
//TODO:mCountdown.showAsText(alarm.ringsIn());
mCountdown.setVisibility(VISIBLE);
//todo:mCountdown.getTickHandler().startTicking(true)
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext);
// how many hours before alarm is considered upcoming
// TODO: shared prefs
/*int hoursBeforeUpcoming = Integer.parseInt(prefs.getString(
mContext.getString(-1TODO:R.string.key_notify_me_of_upcoming_alarms),
"2"));*/
if (alarm.ringsWithinHours(2) || alarm.isSnoozed()) {
// TODO: Register dynamic broadcast receiver in this class to listen for
// when this alarm crosses the upcoming threshold, so we can show this button.
mDismissButton.setVisibility(VISIBLE);
} else {
mDismissButton.setVisibility(GONE);
}
} else {
mSwitch.setChecked(false);
mCountdown.setVisibility(GONE);
//TODO:mCountdown.getTickHandler().stopTicking();
mDismissButton.setVisibility(GONE);
}
mLabel.setText(alarm.label());
if (mLabel.length() == 0 && mCountdown.getVisibility() != VISIBLE) {
mLabel.setVisibility(GONE);
} else {
// needed for proper positioning of mCountdown
mLabel.setVisibility(VISIBLE);
}
int numRecurringDays = alarm.numRecurringDays();
if (numRecurringDays > 0) {
String text;
if (numRecurringDays == NUM_DAYS+1) /*TODO: Remove +1*/ {
text = mContext.getString(R.string.every_day);
} else {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < NUM_DAYS; i++) {
// The day at this position in the week
int weekDay = DaysOfWeek.getInstance(mContext).weekDay(i);
if (alarm.isRecurring(weekDay)) {
sb.append(DaysOfWeek.getLabel(weekDay)).append(", ");
}
}
// Cut off the last comma and space
sb.delete(sb.length() - 2, sb.length());
text = sb.toString();
}
mDays.setText(text);
mDays.setVisibility(VISIBLE);
} else {
mDays.setVisibility(GONE);
}
}
@Override
public String toString() {
return super.toString() + " '" + mContentView.getText() + "'";
public void onClick(View v) {
if (mListener != null) {
mListener.onListFragmentInteraction(mItem);
}
}
}
}
@@ -10,9 +10,9 @@ import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.philliphsu.clock2.Alarm;
import com.philliphsu.clock2.R;
import com.philliphsu.clock2.alarms.dummy.DummyContent;
import com.philliphsu.clock2.alarms.dummy.DummyContent.DummyItem;
/**
* A fragment representing a list of Items.
@@ -32,8 +32,7 @@ public class AlarmsFragment extends Fragment {
* Mandatory empty constructor for the fragment manager to instantiate the
* fragment (e.g. upon screen orientation changes).
*/
public AlarmsFragment() {
}
public AlarmsFragment() {}
// TODO: Customize parameter initialization
@SuppressWarnings("unused")
@@ -57,7 +56,7 @@ public class AlarmsFragment extends Fragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_alarms_list, container, false);
View view = inflater.inflate(R.layout.fragment_alarms, container, false);
// Set the adapter
if (view instanceof RecyclerView) {
@@ -102,7 +101,6 @@ public class AlarmsFragment extends Fragment {
* >Communicating with Other Fragments</a> for more information.
*/
public interface OnListFragmentInteractionListener {
// TODO: Update argument type and name
void onListFragmentInteraction(DummyItem item);
void onListFragmentInteraction(Alarm item);
}
}
@@ -1,9 +1,10 @@
package com.philliphsu.clock2.alarms.dummy;
import com.philliphsu.clock2.Alarm;
import com.philliphsu.clock2.DaysOfWeek;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Helper class for providing sample content for user interfaces created by
@@ -16,57 +17,31 @@ public class DummyContent {
/**
* An array of sample (dummy) items.
*/
public static final List<DummyItem> ITEMS = new ArrayList<DummyItem>();
public static final List<Alarm> ITEMS = new ArrayList<>();
/**
* A map of sample (dummy) items, by ID.
*/
public static final Map<String, DummyItem> ITEM_MAP = new HashMap<String, DummyItem>();
private static final int COUNT = 25;
private static final int COUNT = 10;
static {
// Add some sample items.
for (int i = 1; i <= COUNT; i++) {
addItem(createDummyItem(i));
addItem(createAlarm(i));
}
}
private static void addItem(DummyItem item) {
private static void addItem(Alarm item) {
ITEMS.add(item);
ITEM_MAP.put(item.id, item);
}
private static DummyItem createDummyItem(int position) {
return new DummyItem(String.valueOf(position), "Item " + position, makeDetails(position));
}
private static String makeDetails(int position) {
StringBuilder builder = new StringBuilder();
builder.append("Details about Item: ").append(position);
for (int i = 0; i < position; i++) {
builder.append("\nMore details information here.");
}
return builder.toString();
}
/**
* A dummy item representing a piece of content.
*/
public static class DummyItem {
public final String id;
public final String content;
public final String details;
public DummyItem(String id, String content, String details) {
this.id = id;
this.content = content;
this.details = details;
}
@Override
public String toString() {
return content;
private static Alarm createAlarm(int position) {
Alarm.Builder b = Alarm.builder();
if (position % 2 == 0) {
b.hour(21).minutes(0);
}
boolean[] recurrences = new boolean[DaysOfWeek.NUM_DAYS];
recurrences[0] = true;
recurrences[5] = true;
Alarm a = b.id(position).recurringDays(recurrences).build();
a.setEnabled(true);
return a;
}
}