Java tutorial
/** * RxDroid - A Medication Reminder * Copyright (C) 2011-2014 Joseph Lehner <joseph.c.lehner@gmail.com> * * * RxDroid is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. Additional terms apply (see LICENSE). * * RxDroid is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with RxDroid. If not, see <http://www.gnu.org/licenses/>. * * */ package at.jclehner.rxdroid.ui; import java.util.ArrayList; import java.util.Calendar; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.GregorianCalendar; import java.util.List; import android.content.Intent; import android.os.Bundle; import android.support.v4.view.MenuItemCompat; import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.BaseExpandableListAdapter; import android.widget.ExpandableListView; import android.widget.ImageView; import android.widget.TextView; import at.jclehner.rxdroid.BuildConfig; import at.jclehner.rxdroid.DoseView; import at.jclehner.rxdroid.DrugListActivity2; import at.jclehner.rxdroid.Fraction; import at.jclehner.rxdroid.R; import at.jclehner.rxdroid.Settings; import at.jclehner.rxdroid.Settings.DoseTimeInfo; import at.jclehner.rxdroid.Settings.Keys; import at.jclehner.rxdroid.Theme; import at.jclehner.rxdroid.db.DoseEvent; import at.jclehner.rxdroid.db.Drug; import at.jclehner.rxdroid.db.Entries; import at.jclehner.rxdroid.db.Patient; import at.jclehner.rxdroid.db.Schedule; import at.jclehner.rxdroid.ui.ExpandableListFragment.OnGroupCollapseExpandListener; import at.jclehner.rxdroid.util.Constants; import at.jclehner.rxdroid.util.DateTime; import at.jclehner.rxdroid.util.Timer; import at.jclehner.rxdroid.util.Util; public class DoseLogFragment extends ExpandableListFragment implements OnGroupCollapseExpandListener { private static final String TAG = DoseLogFragment.class.getSimpleName(); private static final boolean LOGV = BuildConfig.DEBUG; private static final int[] DOSE_NAME_IDS = { R.string._title_morning_dose, R.string._title_noon_dose, R.string._title_evening_dose, R.string._title_night_dose }; private static final int MENU_COLLAPSE_EXPAND = 0; public static final int SHOW_MISSED = 1; public static final int SHOW_TAKEN = 1 << 1; public static final int SHOW_SKIPPED = 1 << 2; private List<List<EventInfo>> mGroupedEvents = Collections.emptyList(); private Date mToday; private int mCollapsedCount = 0; public static DoseLogFragment newInstance(Drug drug, int flags) { DoseLogFragment f = new DoseLogFragment(); f.setHasOptionsMenu(true); f.setArguments(Util.createBundle("drug_id", drug.getId(), "flags", flags)); return f; } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mToday = DateTime.today(); setOnGroupCollapseExpandListener(this); setEmptyViewText(R.string._msg_no_history_data); updateListView(getArguments().getInt("flags")); } @Override public boolean onChildClick(ExpandableListView parent, View v, int groupPosition, int childPosition, long id) { Log.d(TAG, "onChildClick: " + groupPosition + ":" + childPosition); final Date date = getGroupDate(groupPosition); if (date == null) return false; final Intent intent = new Intent(getActivity(), DrugListActivity2.class); intent.putExtra(DrugListActivity2.EXTRA_DATE, date); intent.setFlags(Intent.FLAG_ACTIVITY_NO_HISTORY); startActivity(intent); return true; } @Override public void collapseAll() { super.collapseAll(); mCollapsedCount = mGroupedEvents.size(); } @Override public void expandAll(boolean animate) { super.expandAll(animate); mCollapsedCount = 0; } @Override public void onGroupCollapse(int groupPosition) { if (!isAllCollapsed()) { if (++mCollapsedCount == mGroupedEvents.size()) getActivity().supportInvalidateOptionsMenu(); } } @Override public void onGroupExpand(int groupPosition) { if (mCollapsedCount != 0) { if (--mCollapsedCount == 0) getActivity().supportInvalidateOptionsMenu(); } } @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { final int titleResId = isAllCollapsed() ? R.string._title_expand : R.string._title_collapse; MenuItem item = menu.add(0, MENU_COLLAPSE_EXPAND, 0, titleResId) .setIcon(isAllCollapsed() ? R.drawable.ic_action_expand_white : R.drawable.ic_action_collapse_white) .setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { @Override public boolean onMenuItemClick(MenuItem item) { final boolean isAllCollapsed = isAllCollapsed(); if (isAllCollapsed) expandAll(true); else collapseAll(); Settings.putBoolean(Keys.LOG_IS_ALL_COLLAPSED, !isAllCollapsed); //invalidateOptionsMenu(); return true; } }); item.setVisible(!getListAdapter().isEmpty()); MenuItemCompat.setShowAsAction(item, MenuItemCompat.SHOW_AS_ACTION_IF_ROOM); super.onCreateOptionsMenu(menu, inflater); } private boolean isAllCollapsed() { return mCollapsedCount == getListAdapter().getGroupCount(); } private void updateListView(int flags) { getArguments().putInt("flags", flags); gatherEventInfos(flags); setListAdapter(new Adapter()); if (!Settings.getBoolean(Keys.LOG_IS_ALL_COLLAPSED, true)) expandAll(false); else mCollapsedCount = getListAdapter().getGroupCount(); } private Drug getDrug() { return Drug.get(getArguments().getInt("drug_id")); } private void gatherEventInfos(int flags) { final Timer t; if (LOGV) t = new Timer(); else t = null; final Drug drug = getDrug(); final List<EventInfo> infos = new ArrayList<EventInfo>(); final List<DoseEvent> events = Entries.findDoseEvents(drug, null, null); for (DoseEvent event : events) { boolean isSkipped = event.getDose().isZero(); if ((isSkipped && (flags & SHOW_SKIPPED) == 0) || (!isSkipped && (flags & SHOW_TAKEN) == 0)) { continue; } infos.add(EventInfo.newTakenOrIgnoredEvent(event)); } Date date = Settings.getDate(Keys.OLDEST_POSSIBLE_DOSE_EVENT_TIME); if (date != null && !events.isEmpty()) { Collections.sort(infos, EventInfoByDateComparator.INSTANCE); date = DateTime.max(date, Settings.getOldestPossibleHistoryDate(mToday)); date = DateTime.min(date, events.get(events.size() - 1).getDate()); } else if (date == null) date = Settings.getOldestPossibleHistoryDate(mToday); final Date lastScheduleUpdateDate = drug.getLastScheduleUpdateDate(); if (date == null) { if ((date = lastScheduleUpdateDate) == null) { Log.w(TAG, "gatherEventInfos(" + flags + "): no date to begin; giving up"); return; } } // final Date lastDosesClearedDate = drug.getLastDosesClearedDate(); // if(lastDosesClearedDate != null) // { // while(!date.after(lastDosesClearedDate)) // date = DateTime.add(date, Calendar.DAY_OF_MONTH, 1); // } final DoseTimeInfo dtInfo = Settings.getDoseTimeInfo(); if ((flags & SHOW_MISSED) != 0) { while (!date.after(mToday)) { if (lastScheduleUpdateDate == null || !date.before(lastScheduleUpdateDate)) { if (drug.hasDoseOnDate(date)) { for (int doseTime : Constants.DOSE_TIMES) { if (date.equals(mToday) && doseTime == dtInfo.activeOrNextDoseTime()) break; Fraction dose = drug.getDose(doseTime, date); if (!dose.isZero() && !containsDoseEvent(events, date, doseTime)) { //Log.d(TAG, "Creating missed event: date=" + date + ", doseTime=" + doseTime); infos.add(EventInfo.newMissedEvent(date, doseTime, dose)); } } } } date = DateTime.add(date, Calendar.DAY_OF_MONTH, 1); } } Collections.sort(infos, EventInfoByDateComparator.INSTANCE); mGroupedEvents = new ArrayList<List<EventInfo>>(); for (int i = 0; i < infos.size();) { date = infos.get(i).date; List<EventInfo> group = new ArrayList<EventInfo>(); while (i < infos.size() && date.equals(infos.get(i).date)) { //Log.d(TAG, " CHILD: " + events.get(i)); group.add(infos.get(i)); ++i; } if (!group.isEmpty()) { Collections.sort(group, EventInfoByDoseTimeComparator.INSTANCE); mGroupedEvents.add(group); } // if i == infos.size() this is the last entry, in which case it's not // neccessary to confuse the user with the lastScheduleUpdateDate message // (it only matters if taken doses exist before this date) if (date.equals(lastScheduleUpdateDate) && i != infos.size()) { group = new ArrayList<EventInfo>(); group.add(EventInfo.newScheduleUpdatedEvent(date)); mGroupedEvents.add(group); } } if (LOGV) Log.d(TAG, "gatherEvents: " + t); } private Date getGroupDate(int groupPosition) { if (!mGroupedEvents.get(groupPosition).isEmpty()) return mGroupedEvents.get(groupPosition).get(0).date; return null; } private static boolean containsDoseEvent(List<DoseEvent> doseEvents, Date date, int doseTime) { for (DoseEvent doseEvent : doseEvents) { if (doseEvent.getDate().equals(date) && doseEvent.getDoseTime() == doseTime) return true; } return false; } private class Adapter extends BaseExpandableListAdapter { private Timer mChildTimer = new Timer(); LayoutInflater mInflater; public Adapter() { mInflater = LayoutInflater.from(getActivity()); } @Override public int getGroupCount() { return mGroupedEvents.size(); } @Override public int getChildrenCount(int groupPosition) { return mGroupedEvents.get(groupPosition).size() + 1; } @Override public Object getGroup(int groupPosition) { return mGroupedEvents.get(groupPosition); } @Override public Object getChild(int groupPosition, int childPosition) { return mGroupedEvents.get(groupPosition).get(childPosition); } @Override public View getGroupView(int groupPosition, boolean isExpanded, View view, ViewGroup parent) { final GroupViewHolder holder; if (view == null) { view = mInflater.inflate(R.layout.log_group, null); holder = new GroupViewHolder(); holder.status = (ImageView) view.findViewById(R.id.img_status); holder.date = (TextView) view.findViewById(R.id.text_date); view.setTag(holder); } else holder = (GroupViewHolder) view.getTag(); final List<EventInfo> events = mGroupedEvents.get(groupPosition); if (events.isEmpty()) { Log.w(TAG, "Group " + groupPosition + " is empty..."); holder.status.setImageResource(0); holder.date.setText("?"); return view; } final Date date = events.get(0).date; holder.date.setText(DateTime.toNativeDate(date)); int statusResId = Theme.getResourceAttribute(R.attr.doseStatusTaken); for (EventInfo info : events) { if (info.status == EventInfo.STAT_MISSED) { statusResId = Theme.getResourceAttribute(R.attr.doseStatusMissed); break; } else if (info.status == EventInfo.STAT_SKIPPED) statusResId = Theme.getResourceAttribute(R.attr.doseStatusIgnored); else if (info.status == EventInfo.STAT_SCHEDULE_UPDATED) statusResId = Theme.getResourceAttribute(R.attr.iconWarning); } holder.status.setImageResource(statusResId); return view; } @Override public View getChildView(final int groupPosition, int childPosition, boolean isLastChild, View view, ViewGroup parent) { if (childPosition == 0) mChildTimer.restart(); final ChildViewHolder holder; if (view == null) { view = mInflater.inflate(R.layout.log_child, null); holder = new ChildViewHolder(); holder.dose = (DoseView) view.findViewById(R.id.dose_dose); holder.time = (TextView) view.findViewById(R.id.text_time); holder.text = (TextView) view.findViewById(R.id.text_info); holder.gotoDate = (ImageView) view.findViewById(R.id.img_goto_date); view.setTag(holder); } else holder = (ChildViewHolder) view.getTag(); if (!isLastChild) { holder.dose.setVisibility(View.VISIBLE); holder.time.setVisibility(View.VISIBLE); holder.gotoDate.setVisibility(View.INVISIBLE); //holder.text.setGravity(Gravity.CENTER_VERTICAL); final EventInfo info = mGroupedEvents.get(groupPosition).get(childPosition); if (info.status != EventInfo.STAT_SCHEDULE_UPDATED) { } final int timeColorAttr; final int textResId; final DoseEvent doseEvent = info.intake; if (doseEvent != null) { //holder.dose.setDose(doseEvent.getDose()); if (info.status == EventInfo.STAT_TAKEN) { textResId = R.string._title_taken; timeColorAttr = R.attr.colorStatusTaken; } else { textResId = R.string._title_skipped; timeColorAttr = R.attr.colorStatusSkipped; } } else { //holder.dose.setDoseFromDrugAndDate(info.date, null); //holder.dose.setVisibility(View.GONE); textResId = R.string._title_missed; timeColorAttr = R.attr.colorStatusMissed; } if (info.doseTime != Schedule.TIME_INVALID) { holder.time.setText(info.getTimeString()); holder.dose.setDose(info.dose); final StringBuilder sb = new StringBuilder(); sb.append(getString(textResId) + ": "); sb.append(getString(DOSE_NAME_IDS[info.doseTime])); holder.text.setText(sb.toString()); final int color = Theme.getColorAttribute(timeColorAttr); holder.time.setText(info.getTimeString()); holder.time.setTextColor(color); } else { holder.gotoDate.setVisibility(View.GONE); holder.dose.setVisibility(View.GONE); holder.time.setVisibility(View.GONE); holder.text.setText(R.string._msg_schedule_changed); } } else { holder.dose.setVisibility(View.INVISIBLE); holder.time.setVisibility(View.INVISIBLE); holder.gotoDate.setVisibility(View.VISIBLE); //holder.text.setGravity(Gravity.CENTER); /*final OnClickListener clickListener = new OnClickListener() { @Override public void onClick(View v) { final Intent intent = new Intent(getActivity(), DrugListActivity.class); intent.putExtra(DrugListActivity.EXTRA_DATE, getGroupDate(groupPosition)); intent.setFlags(Intent.FLAG_ACTIVITY_NO_HISTORY); startActivity(intent); } }; holder.gotoDate.setOnClickListener(clickListener); holder.text.setOnClickListener(clickListener);*/ holder.text.setText(R.string._msg_tap_to_view_date); } if (isLastChild) Log.i(TAG, (childPosition + 1) + " child views created in " + mChildTimer); return view; } @Override public boolean isChildSelectable(int groupPosition, int childPosition) { return true; } @Override public long getChildId(int groupPosition, int childPosition) { // TODO Auto-generated method stub return childPosition; } @Override public long getGroupId(int groupPosition) { // TODO Auto-generated method stub return groupPosition; } @Override public boolean hasStableIds() { // TODO Auto-generated method stub return false; } } } class GroupViewHolder { ImageView status; TextView date; } class ChildViewHolder { DoseView dose; TextView time; TextView text; ImageView gotoDate; } enum EventInfoByDateComparator implements Comparator<EventInfo> { INSTANCE; @Override public int compare(EventInfo lhs, EventInfo rhs) { return signum(rhs.getSortingTime() - lhs.getSortingTime()); } private static int signum(long l) { if (l == 0) return 0; return l < 0 ? -1 : 1; } } enum EventInfoByDoseTimeComparator implements Comparator<EventInfo> { INSTANCE; @Override public int compare(EventInfo lhs, EventInfo rhs) { return lhs.doseTime - rhs.doseTime; } } class EventInfo { static final int STAT_TAKEN = 0; static final int STAT_MISSED = 1; static final int STAT_SKIPPED = 2; static final int STAT_SCHEDULE_UPDATED = 3; Date timestamp; final Date date; final int doseTime; final Fraction dose; final int status; final DoseEvent intake; static EventInfo newTakenOrIgnoredEvent(DoseEvent intake) { return new EventInfo(intake); } static EventInfo newMissedEvent(Date date, int doseTime, Fraction dose) { return new EventInfo(date, doseTime, dose); } static EventInfo newScheduleUpdatedEvent(Date date) { return new EventInfo(date); } @Override public String toString() { String statusStr; switch (status) { case STAT_TAKEN: statusStr = "taken"; break; case STAT_MISSED: statusStr = "missed"; break; case STAT_SKIPPED: statusStr = "skipped"; break; case STAT_SCHEDULE_UPDATED: return "EventInfo{ " + DateTime.toDateString(date) + ", schedule updated }"; default: statusStr = "???"; } return "EventInfo { " + timestamp + ", " + DateTime.toDateString(date) + ", " + Util.getDoseTimeName(doseTime) + ", " + statusStr + " }"; } @Override public boolean equals(Object o) { if (o == null || !(o instanceof EventInfo)) return false; return getSortingTime() == ((EventInfo) o).getSortingTime(); } @Override public int hashCode() { return (int) (getSortingTime() & 0x00000000ffffffffL); } long getSortingTime() { return date.getTime(); } String getTimeString() { if (timestamp == null) { final long offset = Settings.getTrueDoseTimeEndOffset(doseTime); timestamp = DateTime.add(date, Calendar.MILLISECOND, (int) offset); } final Date dateOfTimestamp = DateTime.getMidnightDate(timestamp); final StringBuilder sb = new StringBuilder(DateTime.toNativeTime(timestamp, false)); long diffDays = DateTime.diffDays(date, dateOfTimestamp); if (diffDays != 0) { sb.append(' '); if (diffDays > 0) sb.append('+'); sb.append(diffDays); } return sb.toString(); } private EventInfo(DoseEvent intake) { timestamp = new Date(intake.getTimestamp().getTime()); date = intake.getDate(); doseTime = intake.getDoseTime(); Fraction dose = intake.getDose(); if (dose.isZero()) { this.dose = intake.getDrug().getDose(doseTime, date); this.status = STAT_SKIPPED; } else { this.dose = dose; this.status = STAT_TAKEN; } this.intake = intake; } private EventInfo(Date date, int doseTime, Fraction dose) { this.date = date; this.doseTime = doseTime; this.dose = dose; status = STAT_MISSED; intake = null; } private EventInfo(Date date) { this.date = date; status = STAT_SCHEDULE_UPDATED; doseTime = Schedule.TIME_INVALID; dose = null; intake = null; } }