package com.ianhanniballake.contractiontimer.ui;
import android.content.AsyncQueryHandler;
import android.content.ContentUris;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.provider.BaseColumns;
import android.support.v4.app.Fragment;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.support.v4.widget.CursorAdapter;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.view.ActionMode;
import android.support.v7.widget.ActionMenuView;
import android.text.TextUtils;
import android.text.format.DateFormat;
import android.text.format.DateUtils;
import android.util.Log;
import android.util.SparseBooleanArray;
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.AbsListView;
import android.widget.AdapterView;
import android.widget.FrameLayout;
import android.widget.ListAdapter;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.ViewAnimator;
import com.google.firebase.analytics.FirebaseAnalytics;
import com.ianhanniballake.contractiontimer.BuildConfig;
import com.ianhanniballake.contractiontimer.R;
import com.ianhanniballake.contractiontimer.appwidget.AppWidgetUpdateHandler;
import com.ianhanniballake.contractiontimer.notification.NotificationUpdateService;
import com.ianhanniballake.contractiontimer.provider.ContractionContract;
import java.lang.ref.WeakReference;
import java.util.Calendar;
/**
* Fragment to list contractions entered by the user
*/
public class ContractionListFragment extends Fragment implements LoaderManager.LoaderCallbacks<Cursor> {
private final static String TAG = ContractionListFragment.class.getSimpleName();
/**
* Key used to store the selected item note in the bundle
*/
private final static String SELECTED_ITEM_NOTE_KEY = "com.ianhanniballake.contractiontimer.SELECTED_ITEM_NOTE_KEY";
/**
* Handler of live duration updates
*/
final Handler liveDurationHandler = new Handler();
/**
* Handler of time since last contraction updates
*/
final Handler timeSinceLastHandler = new Handler();
/**
* Note associated with the currently selected item
*/
String selectedItemNote = null;
/**
* Current ActionMode, if any
*/
ActionMode mActionMode;
/**
* Start time of the current contraction
*/
long currentContractionStartTime = 0;
/**
* Reference to the Runnable live duration updater
*/
final Runnable liveDurationUpdate = new Runnable() {
/**
* Updates the appropriate duration view to the current elapsed time and schedules this to rerun in 1 second
*/
@Override
public void run() {
final View rootView = getView();
if (rootView != null) {
final TextView currentContractionDurationView = (TextView) rootView.findViewWithTag("durationView");
if (currentContractionDurationView != null) {
final long durationInSeconds = (System.currentTimeMillis() - currentContractionStartTime) / 1000;
currentContractionDurationView.setText(DateUtils.formatElapsedTime(durationInSeconds));
}
}
liveDurationHandler.postDelayed(this, 1000);
}
};
ListView mListView;
ViewAnimator mEmptyView;
/**
* View for the header row
*/
View headerView = null;
/**
* Reference to the Runnable time since last contraction updater
*/
private final Runnable timeSinceLastUpdate = new Runnable() {
/**
* Updates the time since last contraction and schedules this to rerun in 1 second
*/
@Override
public void run() {
if (headerView != null) {
final TextView timeSinceLastView = (TextView) headerView.findViewById(R.id.list_header_time_since_last);
if (timeSinceLastView != null && currentContractionStartTime != 0) {
final long timeSinceLastInSeconds = (System.currentTimeMillis() - currentContractionStartTime) / 1000;
timeSinceLastView.setText(DateUtils.formatElapsedTime(timeSinceLastInSeconds));
}
}
timeSinceLastHandler.postDelayed(this, 1000);
}
};
/**
* Column headers view
*/
private ViewGroup mColumnHeaders;
/**
* Adapter to display the list's data
*/
private CursorAdapter adapter;
/**
* Handler for asynchronous deletes of contractions
*/
private AsyncQueryHandler contractionQueryHandler = null;
/**
* Deletes a given contraction
*
* @param id contraction id to delete
*/
protected void deleteContraction(final long id) {
// Ensure we don't attempt to delete contractions with invalid ids
if (id < 0)
return;
final Uri deleteUri = ContentUris.withAppendedId(ContractionContract.Contractions.CONTENT_ID_URI_BASE, id);
if (contractionQueryHandler == null) {
contractionQueryHandler = new DeleteContractionQueryHandler(getActivity());
}
contractionQueryHandler.startDelete(0, 0, deleteUri, null, null);
}
private void itemSelected(ListView listView, int position) {
final long[] checkedItems = listView.getCheckedItemIds();
if (BuildConfig.DEBUG)
Log.d(TAG, "Item clicked: " + checkedItems.length);
if (checkedItems.length == 0) {
mActionMode.finish();
return;
} else if (checkedItems.length == 1) {
SparseBooleanArray checked = listView.getCheckedItemPositions();
int selectedPosition = checked.keyAt(0);
// The checked item positions sometime contain both the old and new items. We need to make sure we
// pick the remaining selected item, rather than the recently de-selected item.
if (selectedPosition == position && listView.isItemChecked(position)) {
selectedPosition = checked.keyAt(1);
}
final ListAdapter adapter = listView.getAdapter();
if (adapter.isEmpty()) // onLoaderReset swapped in a null cursor
return;
final Cursor cursor = (Cursor) adapter.getItem(selectedPosition);
if (cursor == null) // Ensure a valid cursor
return;
final int noteColumnIndex = cursor
.getColumnIndex(ContractionContract.Contractions.COLUMN_NAME_NOTE);
selectedItemNote = cursor.getString(noteColumnIndex);
}
mActionMode.invalidate();
}
@Override
public void onSaveInstanceState(final Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString(ContractionListFragment.SELECTED_ITEM_NOTE_KEY, selectedItemNote);
}
@Override
public void onActivityCreated(final Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (savedInstanceState != null)
selectedItemNote = savedInstanceState.getString(ContractionListFragment.SELECTED_ITEM_NOTE_KEY);
headerView = getLayoutInflater(savedInstanceState).inflate(R.layout.list_header, mListView, false);
final FrameLayout headerFrame = new FrameLayout(getActivity());
headerFrame.addView(headerView);
mListView.addHeaderView(headerFrame, null, false);
adapter = new ContractionListCursorAdapter(getActivity());
mListView.setAdapter(adapter);
mListView.setDrawSelectorOnTop(true);
mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(final AdapterView<?> parent, final View view, final int position, final long id) {
if (mActionMode == null) {
viewContraction(id);
} else {
itemSelected(mListView, position);
}
}
});
mListView.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {
@Override
public boolean onItemLongClick(final AdapterView<?> parent, final View view,
final int position, final long id) {
if (mActionMode != null) {
mListView.setItemChecked(position, !mListView.isItemChecked(position));
itemSelected(mListView, position);
return true;
}
mListView.setChoiceMode(AbsListView.CHOICE_MODE_MULTIPLE);
mListView.setItemChecked(position, true);
AppCompatActivity appCompatActivity = (AppCompatActivity) getActivity();
appCompatActivity.startSupportActionMode(new ActionMode.Callback() {
@Override
public boolean onActionItemClicked(final ActionMode actionMode, final MenuItem menuItem) {
FirebaseAnalytics analytics = FirebaseAnalytics.getInstance(getContext());
final long contractionId = mListView.getCheckedItemIds()[0];
switch (menuItem.getItemId()) {
case R.id.menu_context_view:
if (BuildConfig.DEBUG)
Log.d(TAG, "Context Action Mode selected view");
analytics.logEvent("view_cab", null);
viewContraction(contractionId);
return true;
case R.id.menu_context_note:
final int position = mListView.getCheckedItemPositions().keyAt(0);
final Cursor cursor = (Cursor) mListView.getAdapter().getItem(position);
final int noteColumnIndex = cursor
.getColumnIndex(ContractionContract.Contractions.COLUMN_NAME_NOTE);
final String existingNote = cursor.getString(noteColumnIndex);
if (BuildConfig.DEBUG)
Log.d(TAG, "Context Action Mode selected " + (TextUtils.isEmpty(existingNote) ?
"Add Note" : "Edit Note"));
String noteEvent = TextUtils.isEmpty(existingNote) ? "note_add_cab" : "note_edit_cab";
analytics.logEvent(noteEvent, null);
showNoteDialog(contractionId, existingNote);
actionMode.finish();
return true;
case R.id.menu_context_delete:
final long[] selectedIds = ContractionListFragment.this.mListView.getCheckedItemIds();
if (BuildConfig.DEBUG)
Log.d(TAG, "Context Action Mode selected delete");
Bundle bundle = new Bundle();
bundle.putString(FirebaseAnalytics.Param.VALUE, Integer.toString(selectedIds.length));
analytics.logEvent("delete_cab", bundle);
for (final long id : selectedIds)
deleteContraction(id);
actionMode.finish();
return true;
default:
return false;
}
}
@Override
public boolean onCreateActionMode(final ActionMode actionMode, final Menu menu) {
mActionMode = actionMode;
final MenuInflater inflater = actionMode.getMenuInflater();
inflater.inflate(R.menu.list_context, menu);
return true;
}
@Override
public void onDestroyActionMode(final ActionMode actionMode) {
SparseBooleanArray selectedItems = mListView.getCheckedItemPositions();
if (selectedItems != null) {
for (int i = 0; i < selectedItems.size(); i++) {
mListView.setItemChecked(selectedItems.keyAt(i), false);
}
}
mListView.post(new Runnable() {
@Override
public void run() {
mListView.setChoiceMode(ListView.CHOICE_MODE_NONE);
}
});
mActionMode = null;
}
@Override
public boolean onPrepareActionMode(final ActionMode actionMode, final Menu menu) {
final int selectedItemsSize = mListView.getCheckedItemIds().length;
// Show or hide the view menu item
final MenuItem viewItem = menu.findItem(R.id.menu_context_view);
final boolean showViewItem = selectedItemsSize == 1;
viewItem.setVisible(showViewItem);
// Set whether to display the note menu item
final MenuItem noteItem = menu.findItem(R.id.menu_context_note);
final boolean showNoteItem = selectedItemsSize == 1;
// Set the title of the note menu item
if (showNoteItem)
if (TextUtils.isEmpty(selectedItemNote))
noteItem.setTitle(R.string.note_dialog_title_add);
else
noteItem.setTitle(R.string.note_dialog_title_edit);
noteItem.setVisible(showNoteItem);
// Set the title of the delete menu item
final MenuItem deleteItem = menu.findItem(R.id.menu_context_delete);
final CharSequence currentTitle = deleteItem.getTitle();
final CharSequence newTitle = getResources().getQuantityText(R.plurals.menu_context_delete,
selectedItemsSize);
deleteItem.setTitle(newTitle);
// Set the Contextual Action Bar title with the new item size
final CharSequence modeTitle = actionMode.getTitle();
final CharSequence newModeTitle = String.format(getString(R.string.menu_context_action_mode_title),
selectedItemsSize);
actionMode.setTitle(newModeTitle);
return !newModeTitle.equals(modeTitle) || !newTitle.equals(currentTitle);
}
});
return true;
}
});
getLoaderManager().initLoader(0, null, this);
}
@Override
public Loader<Cursor> onCreateLoader(final int id, final Bundle args) {
return new CursorLoader(getActivity(), getActivity().getIntent().getData(), null, null, null, null);
}
@Override
public View onCreateView(final LayoutInflater inflater, final ViewGroup container, final Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_contraction_list, container, false);
mListView = (ListView) view.findViewById(android.R.id.list);
mEmptyView = (ViewAnimator) view.findViewById(android.R.id.empty);
mListView.setEmptyView(mEmptyView);
mColumnHeaders = (ViewGroup) view.findViewById(R.id.list_column_headers);
return view;
}
@Override
public void onLoaderReset(final Loader<Cursor> loader) {
liveDurationHandler.removeCallbacks(liveDurationUpdate);
mColumnHeaders.setVisibility(View.GONE);
adapter.swapCursor(null);
}
@Override
public void onLoadFinished(final Loader<Cursor> loader, final Cursor data) {
liveDurationHandler.removeCallbacks(liveDurationUpdate);
timeSinceLastHandler.removeCallbacks(timeSinceLastUpdate);
adapter.swapCursor(data);
if (data == null || data.getCount() == 0) {
mColumnHeaders.setVisibility(View.GONE);
mEmptyView.setDisplayedChild(1);
} else {
mColumnHeaders.setVisibility(View.VISIBLE);
mListView.setSelection(0);
data.moveToFirst();
final int endTimeColumnIndex = data.getColumnIndex(ContractionContract.Contractions.COLUMN_NAME_END_TIME);
final boolean isContractionOngoing = data.isNull(endTimeColumnIndex);
if (isContractionOngoing)
headerView.setVisibility(View.GONE);
else {
final int startTimeColumnIndex = data
.getColumnIndex(ContractionContract.Contractions.COLUMN_NAME_START_TIME);
currentContractionStartTime = data.getLong(startTimeColumnIndex);
timeSinceLastHandler.post(timeSinceLastUpdate);
headerView.setVisibility(View.VISIBLE);
}
}
}
@Override
public void onPause() {
super.onPause();
liveDurationHandler.removeCallbacks(liveDurationUpdate);
timeSinceLastHandler.removeCallbacks(timeSinceLastUpdate);
}
@Override
public void onResume() {
super.onResume();
final View rootView = getView();
if (rootView != null) {
final TextView currentContractionDurationView = (TextView) rootView.findViewWithTag("durationView");
if (currentContractionDurationView != null) {
// Ensures the live duration update is running
liveDurationHandler.removeCallbacks(liveDurationUpdate);
liveDurationHandler.post(liveDurationUpdate);
}
}
if (headerView != null) {
// Ensures the live time since last update is running
timeSinceLastHandler.removeCallbacks(timeSinceLastUpdate);
timeSinceLastHandler.post(timeSinceLastUpdate);
}
}
/**
* Shows the 'note' dialog
*
* @param id contraction id
* @param existingNote existing note attached to this contraction if it exists
*/
protected void showNoteDialog(final long id, final String existingNote) {
// Ensure we don't attempt to change the note on contractions with
// invalid ids
if (id < 0)
return;
final NoteDialogFragment noteDialogFragment = new NoteDialogFragment();
final Bundle args = new Bundle();
args.putLong(NoteDialogFragment.CONTRACTION_ID_ARGUMENT, id);
args.putString(NoteDialogFragment.EXISTING_NOTE_ARGUMENT, existingNote);
noteDialogFragment.setArguments(args);
if (BuildConfig.DEBUG)
Log.d(TAG, "Showing Dialog");
noteDialogFragment.show(getFragmentManager(), "note");
}
/**
* View the details of the given contraction
*
* @param id contraction id
*/
protected void viewContraction(final long id) {
// Ensure we don't attempt to view contractions with invalid ids
if (id < 0)
return;
if (isDetached()) // Can't startActivity if we are detached
return;
final Uri contractionUri = ContentUris.withAppendedId(ContractionContract.Contractions.CONTENT_ID_URI_BASE, id);
final Intent intent = new Intent(Intent.ACTION_VIEW, contractionUri).setPackage(getActivity().getPackageName());
startActivity(intent);
}
private static class DeleteContractionQueryHandler extends AsyncQueryHandler {
private WeakReference<Context> mContext;
public DeleteContractionQueryHandler(final Context context) {
super(context.getContentResolver());
mContext = new WeakReference<>(context.getApplicationContext());
}
@Override
protected void onDeleteComplete(final int token, final Object cookie, final int result) {
Context context = mContext.get();
if (context != null) {
AppWidgetUpdateHandler.createInstance().updateAllWidgets(context);
NotificationUpdateService.updateNotification(context);
}
}
}
/**
* Cursor Adapter for creating and binding contraction list view items
*/
private class ContractionListCursorAdapter extends CursorAdapter {
/**
* Local reference to the layout inflater service
*/
private final LayoutInflater inflater;
/**
* @param context The context where the ListView associated with this Adapter
*/
public ContractionListCursorAdapter(final Context context) {
super(context, null, 0);
inflater = LayoutInflater.from(context);
}
@Override
public void bindView(final View view, final Context context, final Cursor cursor) {
String timeFormat = "hh:mm:ssa";
if (DateFormat.is24HourFormat(context))
timeFormat = "kk:mm:ss";
String dateFormat;
try {
final char[] dateFormatOrder = DateFormat.getDateFormatOrder(mContext);
final char[] dateFormatArray = {dateFormatOrder[0], dateFormatOrder[0], '/', dateFormatOrder[1],
dateFormatOrder[1]};
dateFormat = new String(dateFormatArray);
} catch (IllegalArgumentException e) {
dateFormat = "MM/dd";
}
final int startTimeColumnIndex = cursor
.getColumnIndex(ContractionContract.Contractions.COLUMN_NAME_START_TIME);
final long startTime = cursor.getLong(startTimeColumnIndex);
final Calendar startCal = Calendar.getInstance();
startCal.setTimeInMillis(startTime);
boolean showDateOnStartTime = false;
final TextView startTimeView = (TextView) view.findViewById(R.id.start_time);
final int endTimeColumnIndex = cursor.getColumnIndex(ContractionContract.Contractions.COLUMN_NAME_END_TIME);
final boolean isContractionOngoing = cursor.isNull(endTimeColumnIndex);
final TextView endTimeView = (TextView) view.findViewById(R.id.end_time);
final TextView durationView = (TextView) view.findViewById(R.id.duration);
final Calendar endCal = Calendar.getInstance();
boolean showDateOnEndTime = false;
if (isContractionOngoing) {
durationView.setText("");
currentContractionStartTime = startTime;
durationView.setTag("durationView");
liveDurationHandler.removeCallbacks(liveDurationUpdate);
liveDurationHandler.post(liveDurationUpdate);
} else {
final long endTime = cursor.getLong(endTimeColumnIndex);
endCal.setTimeInMillis(endTime);
durationView.setTag("");
final long durationInSeconds = (endTime - startTime) / 1000;
durationView.setText(DateUtils.formatElapsedTime(durationInSeconds));
}
if (startCal.get(Calendar.YEAR) != endCal.get(Calendar.YEAR)
|| startCal.get(Calendar.DAY_OF_YEAR) != endCal.get(Calendar.DAY_OF_YEAR))
showDateOnEndTime = true;
final TextView frequencyView = (TextView) view.findViewById(R.id.frequency);
// If we aren't the last entry, move to the next (previous in time)
// contraction to get its start time to compute the frequency
if (!cursor.isLast() && cursor.moveToNext()) {
final int prevContractionStartTimeColumnIndex = cursor
.getColumnIndex(ContractionContract.Contractions.COLUMN_NAME_START_TIME);
final long prevContractionStartTime = cursor.getLong(prevContractionStartTimeColumnIndex);
final long frequencyInSeconds = (startTime - prevContractionStartTime) / 1000;
frequencyView.setText(DateUtils.formatElapsedTime(frequencyInSeconds));
// Check to see if the date changed between Contractions
final int prevContractionEndTimeColumnIndex = cursor
.getColumnIndex(ContractionContract.Contractions.COLUMN_NAME_END_TIME);
final long prevContractionEndTime = cursor.getLong(prevContractionEndTimeColumnIndex);
final Calendar prevEndCal = Calendar.getInstance();
prevEndCal.setTimeInMillis(prevContractionEndTime);
if (startCal.get(Calendar.YEAR) != prevEndCal.get(Calendar.YEAR)
|| startCal.get(Calendar.DAY_OF_YEAR) != prevEndCal.get(Calendar.DAY_OF_YEAR))
showDateOnStartTime = true;
// Go back to the previous spot
cursor.moveToPrevious();
} else {
frequencyView.setText("");
// Always show the date on the very first start time
showDateOnStartTime = true;
}
startTimeView.setText(DateFormat.format(timeFormat, startCal)
+ (showDateOnStartTime ? " " + DateFormat.format(dateFormat, startCal) : ""));
if (isContractionOngoing)
endTimeView.setText(" ");
else
endTimeView.setText(DateFormat.format(timeFormat, endCal)
+ (showDateOnEndTime ? " " + DateFormat.format(dateFormat, endCal) : ""));
final int noteColumnIndex = cursor.getColumnIndex(ContractionContract.Contractions.COLUMN_NAME_NOTE);
final String note = cursor.getString(noteColumnIndex);
final TextView noteView = (TextView) view.findViewById(R.id.note);
noteView.setText(note);
if (TextUtils.isEmpty(note))
noteView.setVisibility(View.GONE);
else
noteView.setVisibility(View.VISIBLE);
final int idColumnIndex = cursor.getColumnIndex(BaseColumns._ID);
final long id = cursor.getLong(idColumnIndex);
final ActionMenuView showPopupView = (ActionMenuView) view.findViewById(R.id.show_popup);
final MenuItem noteItem = showPopupView.getMenu().findItem(R.id.menu_context_note);
if (TextUtils.isEmpty(note))
noteItem.setTitle(R.string.note_dialog_title_add);
else
noteItem.setTitle(R.string.note_dialog_title_edit);
final MenuItem deleteItem = showPopupView.getMenu().findItem(R.id.menu_context_delete);
deleteItem.setTitle(getResources().getQuantityText(R.plurals.menu_context_delete, 1));
showPopupView.setOnMenuItemClickListener(new ActionMenuView.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(final MenuItem item) {
FirebaseAnalytics analytics = FirebaseAnalytics.getInstance(getContext());
switch (item.getItemId()) {
case R.id.menu_context_view:
if (BuildConfig.DEBUG)
Log.d(TAG, "Popup Menu selected view");
analytics.logEvent("view_popup", null);
viewContraction(id);
return true;
case R.id.menu_context_note:
String type = TextUtils.isEmpty(note) ? "Add Note" : "Edit Note";
if (BuildConfig.DEBUG)
Log.d(TAG, "Popup Menu selected " + type);
String noteEvent = TextUtils.isEmpty(note) ? "note_add_popup" : "note_edit_popup";
analytics.logEvent(noteEvent, null);
showNoteDialog(id, note);
return true;
case R.id.menu_context_delete:
if (BuildConfig.DEBUG)
Log.d(TAG, "Popup Menu selected delete");
Bundle bundle = new Bundle();
bundle.putString(FirebaseAnalytics.Param.VALUE, Integer.toString(1));
analytics.logEvent("delete_popup", bundle);
deleteContraction(id);
return true;
default:
return false;
}
}
});
// Don't allow popup menu while the Contextual Action Bar is present
showPopupView.setEnabled(mActionMode == null);
}
@Override
public View newView(final Context context, final Cursor cursor, final ViewGroup parent) {
final View view = inflater.inflate(R.layout.list_item_contraction, parent, false);
final ActionMenuView showPopup = (ActionMenuView) view.findViewById(R.id.show_popup);
MenuInflater menuInflater = getActivity().getMenuInflater();
menuInflater.inflate(R.menu.list_popup, showPopup.getMenu());
return view;
}
}
}