package com.ianhanniballake.contractiontimer.ui; import android.app.Activity; import android.content.AsyncQueryHandler; import android.content.BroadcastReceiver; import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.database.Cursor; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; import android.provider.BaseColumns; import android.support.annotation.NonNull; import android.support.annotation.Nullable; 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.content.LocalBroadcastManager; import android.support.v4.widget.CursorAdapter; import android.text.Editable; import android.text.TextWatcher; import android.text.format.DateFormat; import android.text.format.DateUtils; 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.View.OnClickListener; import android.view.ViewGroup; import android.widget.EditText; import android.widget.TextView; 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.util.Calendar; /** * Fragment showing the details of an individual contraction */ public class EditFragment extends Fragment implements LoaderManager.LoaderCallbacks<Cursor> { /** * Action associated with the end time's date being changed */ public final static String END_DATE_ACTION = "com.ianhanniballake.contractiontimer.END_DATE"; /** * Action associated with the end time's time being changed */ public final static String END_TIME_ACTION = "com.ianhanniballake.contractiontimer.END_TIME"; /** * Action associated with the start time's date being changed */ public final static String START_DATE_ACTION = "com.ianhanniballake.contractiontimer.START_DATE"; /** * Action associated with the start time's time being changed */ public final static String START_TIME_ACTION = "com.ianhanniballake.contractiontimer.START_TIME"; private final static String TAG = EditFragment.class.getSimpleName(); /** * Current end time of the contraction */ Calendar endTime = null; /** * Current note of the contraction */ String note = ""; /** * Whether the End Time Order check passed */ boolean passedEndTimeOrderCheck = false; /** * Whether the End Time Overlap check passed */ boolean passedEndTimeOverlapCheck = false; /** * Whether the Start Time Overlap check passed */ boolean passedStartTimeOverlapCheck = false; /** * Whether the Time Overlap check passed */ boolean passedTimeOverlapCheck = false; /** * Current start time of the contraction */ Calendar startTime = null; /** * BroadcastReceiver listening for START_DATE_ACTION and END_DATE_ACTION actions */ private final BroadcastReceiver dateSetBroadcastReceiver = new BroadcastReceiver() { @Override public void onReceive(final Context context, final Intent intent) { final String action = intent.getAction(); final int year = intent.getIntExtra(DatePickerDialogFragment.YEAR_EXTRA, startTime.get(Calendar.YEAR)); final int monthOfYear = intent.getIntExtra(DatePickerDialogFragment.MONTH_OF_YEAR_EXTRA, startTime.get(Calendar.MONTH)); final int dayOfMonth = intent.getIntExtra(DatePickerDialogFragment.DAY_OF_MONTH_EXTRA, startTime.get(Calendar.DAY_OF_MONTH)); if (BuildConfig.DEBUG) Log.d(TAG, "Date Receive: " + action + "; " + year + "-" + monthOfYear + "-" + dayOfMonth); if (EditFragment.START_DATE_ACTION.equals(action)) { final long oldStartTime = startTime.getTimeInMillis(); startTime.set(Calendar.YEAR, year); startTime.set(Calendar.MONTH, monthOfYear); startTime.set(Calendar.DAY_OF_MONTH, dayOfMonth); final long timeOffset = startTime.getTimeInMillis() - oldStartTime; endTime.setTimeInMillis(endTime.getTimeInMillis() + timeOffset); } else if (EditFragment.END_DATE_ACTION.equals(action)) { endTime.set(Calendar.YEAR, year); endTime.set(Calendar.MONTH, monthOfYear); endTime.set(Calendar.DAY_OF_MONTH, dayOfMonth); } updateViews(); } }; /** * BroadcastReceiver listening for START_TIME_ACTION and END_TIME_ACTION actions */ private final BroadcastReceiver timeSetBroadcastReceiver = new BroadcastReceiver() { @Override public void onReceive(final Context context, final Intent intent) { final String action = intent.getAction(); final int hourOfDay = intent.getIntExtra(TimePickerDialogFragment.HOUR_OF_DAY_EXTRA, startTime.get(Calendar.HOUR_OF_DAY)); final int minute = intent .getIntExtra(TimePickerDialogFragment.MINUTE_EXTRA, startTime.get(Calendar.MINUTE)); final int second = intent .getIntExtra(TimePickerDialogFragment.SECOND_EXTRA, startTime.get(Calendar.SECOND)); if (BuildConfig.DEBUG) Log.d(TAG, "Time Receive: " + action + "; " + hourOfDay + ", " + minute); if (EditFragment.START_TIME_ACTION.equals(action)) { final long oldStartTime = startTime.getTimeInMillis(); startTime.set(Calendar.HOUR_OF_DAY, hourOfDay); startTime.set(Calendar.MINUTE, minute); startTime.set(Calendar.SECOND, second); startTime.set(Calendar.MILLISECOND, 0); final long timeOffset = startTime.getTimeInMillis() - oldStartTime; endTime.setTimeInMillis(endTime.getTimeInMillis() + timeOffset); } else if (EditFragment.END_TIME_ACTION.equals(action)) { endTime.set(Calendar.HOUR_OF_DAY, hourOfDay); endTime.set(Calendar.MINUTE, minute); endTime.set(Calendar.SECOND, second); endTime.set(Calendar.MILLISECOND, 0); } updateViews(); } }; /** * Adapter to display the detailed data */ private CursorAdapter adapter; /** * Handler for asynchronous updates of contractions */ private AsyncQueryHandler contractionQueryHandler; /** * Reset the current data to empty/current dates */ private void clear() { startTime = Calendar.getInstance(); endTime = Calendar.getInstance(); endTime.setTimeInMillis(startTime.getTimeInMillis()); startTime.add(Calendar.MINUTE, -1); note = ""; } /** * Gets the current values from the edit fields for updating the contraction * * @return ContentValues associated with the current (possibly edited) data */ @NonNull private ContentValues getContentValues() { final ContentValues values = new ContentValues(); values.put(ContractionContract.Contractions.COLUMN_NAME_START_TIME, startTime.getTimeInMillis()); values.put(ContractionContract.Contractions.COLUMN_NAME_END_TIME, endTime.getTimeInMillis()); values.put(ContractionContract.Contractions.COLUMN_NAME_NOTE, note); return values; } /** * We need to find the exact edit_fragment view as there is a NoSaveStateFrameLayout view inserted in between the * parent and the view we created in onCreateView * * @return View created in onCreateView */ @Nullable View getFragmentView() { final View rootView = getView(); return rootView == null ? null : rootView.findViewById(R.id.edit_fragment); } @Override public void onActivityCreated(final Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); setHasOptionsMenu(true); final Context applicationContext = getActivity().getApplicationContext(); contractionQueryHandler = new AsyncQueryHandler(getActivity().getContentResolver()) { @Override protected void onInsertComplete(final int token, final Object cookie, final Uri uri) { AppWidgetUpdateHandler.createInstance().updateAllWidgets(applicationContext); NotificationUpdateService.updateNotification(applicationContext); final Activity activity = getActivity(); if (activity != null) activity.finish(); } @Override protected void onUpdateComplete(final int token, final Object cookie, final int result) { AppWidgetUpdateHandler.createInstance().updateAllWidgets(applicationContext); NotificationUpdateService.updateNotification(applicationContext); final Activity activity = getActivity(); if (activity != null) activity.finish(); } }; adapter = new CursorAdapter(getActivity(), null, 0) { @Override public void bindView(final View view, final Context context, final Cursor cursor) { final int startTimeColumnIndex = cursor .getColumnIndex(ContractionContract.Contractions.COLUMN_NAME_START_TIME); startTime = Calendar.getInstance(); startTime.setTimeInMillis(cursor.getLong(startTimeColumnIndex)); final int endTimeColumnIndex = cursor .getColumnIndex(ContractionContract.Contractions.COLUMN_NAME_END_TIME); if (cursor.isNull(endTimeColumnIndex)) endTime = null; else { endTime = Calendar.getInstance(); endTime.setTimeInMillis(cursor.getLong(endTimeColumnIndex)); } final int noteColumnIndex = cursor.getColumnIndex(ContractionContract.Contractions.COLUMN_NAME_NOTE); note = cursor.getString(noteColumnIndex); } @Override public View newView(final Context context, final Cursor cursor, final ViewGroup parent) { // View is already inflated in onCreateView return null; } }; if (savedInstanceState != null) { startTime = (Calendar) savedInstanceState .getSerializable(ContractionContract.Contractions.COLUMN_NAME_START_TIME); endTime = (Calendar) savedInstanceState .getSerializable(ContractionContract.Contractions.COLUMN_NAME_END_TIME); note = savedInstanceState.getString(ContractionContract.Contractions.COLUMN_NAME_NOTE); if (startTime == null) // The user may have paused before the loader completed so re-query getLoaderManager().initLoader(0, null, this); else { // No longer need the loader as we have valid local copies (which may have changes) from now on getLoaderManager().destroyLoader(0); updateViews(); } } else if (Intent.ACTION_EDIT.equals(getActivity().getIntent().getAction())) getLoaderManager().initLoader(0, null, this); else { clear(); updateViews(); } } @Override public Loader<Cursor> onCreateLoader(final int id, final Bundle args) { return new CursorLoader(getActivity(), getActivity().getIntent().getData(), null, null, null, null); } @Override public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) { super.onCreateOptionsMenu(menu, inflater); final boolean allErrorCheckPassed = passedEndTimeOverlapCheck && passedStartTimeOverlapCheck && passedTimeOverlapCheck && passedEndTimeOrderCheck; if (BuildConfig.DEBUG) Log.d(TAG, "EndTimeOverlap Pass: " + passedEndTimeOverlapCheck + ", StartTimeOverlap Pass: " + passedStartTimeOverlapCheck + ", TimeOverlap Pass: " + passedStartTimeOverlapCheck + ", EndTimeOrder Pass: " + passedEndTimeOrderCheck + ". Allow save: " + allErrorCheckPassed); menu.findItem(R.id.menu_save).setEnabled(allErrorCheckPassed); } @Override public View onCreateView(final LayoutInflater inflater, final ViewGroup container, final Bundle savedInstanceState) { final View view = inflater.inflate(R.layout.fragment_edit, container, false); final TextView startTimeView = (TextView) view.findViewById(R.id.start_time); startTimeView.setOnClickListener(new OnClickListener() { @Override public void onClick(final View v) { final TimePickerDialogFragment timePicker = new TimePickerDialogFragment(); final Bundle args = new Bundle(); args.putString(TimePickerDialogFragment.CALLBACK_ACTION, EditFragment.START_TIME_ACTION); args.putSerializable(TimePickerDialogFragment.TIME_ARGUMENT, startTime); timePicker.setArguments(args); if (BuildConfig.DEBUG) Log.d(TAG, "Showing Start Time Dialog"); timePicker.show(getFragmentManager(), "startTime"); } }); final TextView startDateView = (TextView) view.findViewById(R.id.start_date); startDateView.setOnClickListener(new OnClickListener() { @Override public void onClick(final View v) { final DatePickerDialogFragment datePicker = new DatePickerDialogFragment(); final Bundle args = new Bundle(); args.putString(DatePickerDialogFragment.CALLBACK_ACTION, EditFragment.START_DATE_ACTION); args.putSerializable(DatePickerDialogFragment.DATE_ARGUMENT, startTime); datePicker.setArguments(args); if (BuildConfig.DEBUG) Log.d(TAG, "Showing Start Date Dialog"); datePicker.show(getFragmentManager(), "startDate"); } }); final TextView endTimeView = (TextView) view.findViewById(R.id.end_time); endTimeView.setOnClickListener(new OnClickListener() { @Override public void onClick(final View v) { final TimePickerDialogFragment timePicker = new TimePickerDialogFragment(); final Bundle args = new Bundle(); args.putString(TimePickerDialogFragment.CALLBACK_ACTION, EditFragment.END_TIME_ACTION); args.putSerializable(TimePickerDialogFragment.TIME_ARGUMENT, endTime); timePicker.setArguments(args); if (BuildConfig.DEBUG) Log.d(TAG, "Showing End Time Dialog"); timePicker.show(getFragmentManager(), "endTime"); } }); final TextView endDateView = (TextView) view.findViewById(R.id.end_date); endDateView.setOnClickListener(new OnClickListener() { @Override public void onClick(final View v) { final DatePickerDialogFragment datePicker = new DatePickerDialogFragment(); final Bundle args = new Bundle(); args.putString(DatePickerDialogFragment.CALLBACK_ACTION, EditFragment.END_DATE_ACTION); args.putSerializable(DatePickerDialogFragment.DATE_ARGUMENT, endTime); datePicker.setArguments(args); if (BuildConfig.DEBUG) Log.d(TAG, "Showing End Date Dialog"); datePicker.show(getFragmentManager(), "endDate"); } }); final EditText noteView = (EditText) view.findViewById(R.id.note); noteView.addTextChangedListener(new TextWatcher() { @Override public void afterTextChanged(final Editable s) { note = s.toString(); } @Override public void beforeTextChanged(final CharSequence s, final int start, final int count, final int after) { // Nothing to do } @Override public void onTextChanged(final CharSequence s, final int start, final int before, final int count) { // Nothing to do } }); return view; } @Override public void onLoaderReset(final Loader<Cursor> data) { adapter.swapCursor(null); } @Override public void onLoadFinished(final Loader<Cursor> loader, final Cursor data) { adapter.swapCursor(data); if (data.moveToFirst()) adapter.bindView(getFragmentView(), getActivity(), data); else clear(); updateViews(); } @Override public boolean onOptionsItemSelected(final MenuItem item) { switch (item.getItemId()) { case R.id.menu_save: final ContentValues values = getContentValues(); if (Intent.ACTION_INSERT.equals(getActivity().getIntent().getAction())) { if (BuildConfig.DEBUG) Log.d(TAG, "Add selected save"); FirebaseAnalytics.getInstance(getContext()).logEvent("add_save", null); contractionQueryHandler.startInsert(0, null, getActivity().getIntent().getData(), values); } else { if (BuildConfig.DEBUG) Log.d(TAG, "Edit selected save"); FirebaseAnalytics.getInstance(getContext()).logEvent("edit_save", null); contractionQueryHandler.startUpdate(0, null, getActivity().getIntent().getData(), values, null, null); } return true; case R.id.menu_cancel: if (BuildConfig.DEBUG) Log.d(TAG, "Edit selected cancel"); getActivity().finish(); return true; default: return super.onOptionsItemSelected(item); } } @Override public void onSaveInstanceState(final Bundle outState) { super.onSaveInstanceState(outState); outState.putSerializable(ContractionContract.Contractions.COLUMN_NAME_START_TIME, startTime); outState.putSerializable(ContractionContract.Contractions.COLUMN_NAME_END_TIME, endTime); outState.putString(ContractionContract.Contractions.COLUMN_NAME_NOTE, note); } @Override public void onStart() { super.onStart(); final LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(getActivity()); final IntentFilter timeFilter = new IntentFilter(); timeFilter.addAction(EditFragment.START_TIME_ACTION); timeFilter.addAction(EditFragment.END_TIME_ACTION); localBroadcastManager.registerReceiver(timeSetBroadcastReceiver, timeFilter); final IntentFilter dateFilter = new IntentFilter(); dateFilter.addAction(EditFragment.START_DATE_ACTION); dateFilter.addAction(EditFragment.END_DATE_ACTION); localBroadcastManager.registerReceiver(dateSetBroadcastReceiver, dateFilter); } @Override public void onStop() { super.onStop(); final LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(getActivity()); localBroadcastManager.unregisterReceiver(timeSetBroadcastReceiver); localBroadcastManager.unregisterReceiver(dateSetBroadcastReceiver); } /** * Updates the edit views based on the current internal data */ void updateViews() { passedEndTimeOverlapCheck = false; passedStartTimeOverlapCheck = false; passedTimeOverlapCheck = false; passedEndTimeOrderCheck = false; new StartTimeOverlapCheck().execute(); new EndTimeOverlapCheck().execute(); new TimeOverlapCheck().execute(); final View view = getFragmentView(); if (view == null) return; final TextView startTimeView = (TextView) view.findViewById(R.id.start_time); final TextView startDateView = (TextView) view.findViewById(R.id.start_date); String timeFormat = "hh:mm:ssa"; if (DateFormat.is24HourFormat(getActivity())) timeFormat = "kk:mm:ss"; startTimeView.setText(DateFormat.format(timeFormat, startTime)); startDateView.setText(DateFormat.getDateFormat(getActivity()).format(startTime.getTime())); final TextView endTimeView = (TextView) view.findViewById(R.id.end_time); final TextView endDateView = (TextView) view.findViewById(R.id.end_date); final TextView durationView = (TextView) view.findViewById(R.id.duration); final boolean isContractionOngoing = endTime == null; if (isContractionOngoing) { endTimeView.setText(""); endDateView.setText(""); durationView.setText(getString(R.string.duration_ongoing)); } else { endTimeView.setText(DateFormat.format(timeFormat, endTime)); endDateView.setText(DateFormat.getDateFormat(getActivity()).format(endTime.getTime())); final TextView endTimeErrorOrderView = (TextView) view.findViewById(R.id.end_time_error_order); passedEndTimeOrderCheck = startTime.before(endTime); if (passedEndTimeOrderCheck) { endTimeErrorOrderView.setVisibility(View.GONE); final long durationInSeconds = (endTime.getTimeInMillis() - startTime.getTimeInMillis()) / 1000; durationView.setText(DateUtils.formatElapsedTime(durationInSeconds)); } else { endTimeErrorOrderView.setVisibility(View.VISIBLE); durationView.setText(""); } getActivity().supportInvalidateOptionsMenu(); } final EditText noteView = (EditText) view.findViewById(R.id.note); noteView.setText(note); } /** * AsyncTask used to check to see if the current end time overlaps any other existing contraction, displaying an * error message if it does */ private class EndTimeOverlapCheck extends AsyncTask<Void, Void, Boolean> { public EndTimeOverlapCheck() { } @Override protected Boolean doInBackground(final Void... params) { final Activity activity = getActivity(); if (activity == null || endTime == null) return false; final String[] projection = {BaseColumns._ID}; final String selection = BaseColumns._ID + "<>? AND " + ContractionContract.Contractions.COLUMN_NAME_START_TIME + "<=? AND " + ContractionContract.Contractions.COLUMN_NAME_END_TIME + ">=?"; final String currentEndTimeMillis = Long.toString(endTime.getTimeInMillis()); final long contractionId = Intent.ACTION_INSERT.equals(activity.getIntent().getAction()) ? 0 : ContentUris .parseId(activity.getIntent().getData()); final String[] selectionArgs = {Long.toString(contractionId), currentEndTimeMillis, currentEndTimeMillis}; final Cursor data = activity.getContentResolver().query(ContractionContract.Contractions.CONTENT_URI, projection, selection, selectionArgs, null); final boolean overlapExists = data != null && data.moveToFirst(); if (data != null) data.close(); return overlapExists; } @Override protected void onPostExecute(final Boolean overlapExists) { if (BuildConfig.DEBUG) Log.d(TAG, "End time overlap: " + overlapExists); final View view = getFragmentView(); if (view == null) return; final TextView endTimeErrorOverlapView = (TextView) view.findViewById(R.id.end_time_error_overlap); if (endTimeErrorOverlapView == null) return; if (overlapExists) endTimeErrorOverlapView.setVisibility(View.VISIBLE); else endTimeErrorOverlapView.setVisibility(View.GONE); passedEndTimeOverlapCheck = !overlapExists; getActivity().supportInvalidateOptionsMenu(); } } /** * AsyncTask used to check to see if the current start time overlaps any other existing contraction, displaying an * error message if it does */ private class StartTimeOverlapCheck extends AsyncTask<Void, Void, Boolean> { public StartTimeOverlapCheck() { } @Override protected Boolean doInBackground(final Void... params) { final Activity activity = getActivity(); if (activity == null || startTime == null) return false; final String[] projection = {BaseColumns._ID}; final String selection = BaseColumns._ID + "<>? AND " + ContractionContract.Contractions.COLUMN_NAME_START_TIME + "<=? AND " + ContractionContract.Contractions.COLUMN_NAME_END_TIME + ">=?"; final String currentStartTimeMillis = Long.toString(startTime.getTimeInMillis()); final long contractionId = Intent.ACTION_INSERT.equals(activity.getIntent().getAction()) ? 0 : ContentUris .parseId(activity.getIntent().getData()); final String[] selectionArgs = {Long.toString(contractionId), currentStartTimeMillis, currentStartTimeMillis}; final Cursor data = activity.getContentResolver().query(ContractionContract.Contractions.CONTENT_URI, projection, selection, selectionArgs, null); final boolean overlapExists = data != null && data.moveToFirst(); if (data != null) data.close(); return overlapExists; } @Override protected void onPostExecute(final Boolean overlapExists) { if (BuildConfig.DEBUG) Log.d(TAG, "Start time overlap: " + overlapExists); final View view = getFragmentView(); if (view == null) return; final TextView startTimeErrorOverlapView = (TextView) view.findViewById(R.id.start_time_error_overlap); if (startTimeErrorOverlapView == null) return; if (overlapExists) startTimeErrorOverlapView.setVisibility(View.VISIBLE); else startTimeErrorOverlapView.setVisibility(View.GONE); passedStartTimeOverlapCheck = !overlapExists; getActivity().supportInvalidateOptionsMenu(); } } /** * AsyncTask used to check to see if the current start/end time overlaps any other existing contraction, displaying * an error message if it does */ private class TimeOverlapCheck extends AsyncTask<Void, Void, Boolean> { public TimeOverlapCheck() { } @Override protected Boolean doInBackground(final Void... params) { final Activity activity = getActivity(); if (activity == null || startTime == null || endTime == null) return false; final String[] projection = {BaseColumns._ID}; final String selection = BaseColumns._ID + "<>? AND " + ContractionContract.Contractions.COLUMN_NAME_START_TIME + ">=? AND " + ContractionContract.Contractions.COLUMN_NAME_END_TIME + "<=?"; final String currentStartTimeMillis = Long.toString(startTime.getTimeInMillis()); final String currentEndTimeMillis = Long.toString(endTime.getTimeInMillis()); final long contractionId = Intent.ACTION_INSERT.equals(activity.getIntent().getAction()) ? 0 : ContentUris .parseId(activity.getIntent().getData()); final String[] selectionArgs = {Long.toString(contractionId), currentStartTimeMillis, currentEndTimeMillis}; final Cursor data = activity.getContentResolver().query(ContractionContract.Contractions.CONTENT_URI, projection, selection, selectionArgs, null); final boolean overlapExists = data != null && data.moveToFirst(); if (data != null) data.close(); return overlapExists; } @Override protected void onPostExecute(final Boolean overlapExists) { if (BuildConfig.DEBUG) Log.d(TAG, "Time overlap: " + overlapExists); final View view = getFragmentView(); if (view == null) return; final TextView timeErrorOverlapView = (TextView) view.findViewById(R.id.time_error_overlap); if (timeErrorOverlapView == null) return; if (overlapExists) timeErrorOverlapView.setVisibility(View.VISIBLE); else timeErrorOverlapView.setVisibility(View.GONE); passedTimeOverlapCheck = !overlapExists; getActivity().supportInvalidateOptionsMenu(); } } }