From 3732246bb792fe8b336f090441cdd2e47c62dd31 Mon Sep 17 00:00:00 2001 From: Phillip Hsu Date: Wed, 17 Aug 2016 01:08:37 -0700 Subject: [PATCH] Depcrecated EditAlarmActivity, moved some code to AlarmFragment and ExpandedAlarmViewHolder --- .../com/philliphsu/clock2/MainActivity.java | 46 ++++++ .../clock2/alarms/AlarmsFragment.java | 155 +++++++++++++----- .../alarms/ExpandedAlarmViewHolder.java | 19 ++- .../editalarm/BaseTimePickerDialog.java | 2 +- .../clock2/editalarm/EditAlarmActivity.java | 3 +- .../main/res/layout/item_expanded_alarm.xml | 26 +-- 6 files changed, 188 insertions(+), 63 deletions(-) diff --git a/app/src/main/java/com/philliphsu/clock2/MainActivity.java b/app/src/main/java/com/philliphsu/clock2/MainActivity.java index e01b631..f07cad0 100644 --- a/app/src/main/java/com/philliphsu/clock2/MainActivity.java +++ b/app/src/main/java/com/philliphsu/clock2/MainActivity.java @@ -218,6 +218,52 @@ public class MainActivity extends BaseActivity { mAddItemDrawable = ContextCompat.getDrawable(this, R.drawable.ic_add_24dp); } + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + // If we get here, either this Activity OR one of its hosted Fragments + // started a requested Activity for a result. The latter case may seem + // strange; the Fragment is the one starting the requested Activity, so why + // does the result end up in its host Activity? Shouldn't it end up in + // Fragment#onActivityResult()? Actually, the Fragment's host Activity gets the + // first shot at handling the result, before delegating it to the Fragment + // in Fragment#onActivityResult(). + // + // There are subtle points to keep in mind when it is actually the Fragment + // that should handle the result, NOT this Activity. You MUST start + // the requested Activity with Fragment#startActivityForResult(), NOT + // Activity#startActivityForResult(). The former calls + // FragmentActivity#startActivityFromFragment() to implement its behavior. + // Among other things (not relevant to the discussion), + // FragmentActivity#startActivityFromFragment() sets internal bit flags + // that are necessary to achieve the described behavior (that this Activity + // should delegate the result to the Fragment). Finally, you MUST call + // through to the super implementation of Activity#onActivityResult(), + // i.e. FragmentActivity#onActivityResult(). It is this method where + // the aforementioned internal bit flags will be read to determine + // which of this Activity's hosted Fragments started the requested + // Activity. + // + // If you are not careful with these points and instead mistakenly call + // Activity#startActivityForResult(), THEN YOU WILL ONLY BE ABLE TO + // HANDLE THE REQUEST HERE; the super implementation of onActivityResult() + // will not delegate the result to the Fragment, because the requisite + // internal bit flags are not set with Activity#startActivityForResult(). + // + // Further reading: + // http://stackoverflow.com/q/6147884/5055032 + // http://stackoverflow.com/a/24303360/5055032 + super.onActivityResult(requestCode, resultCode, data); + // This is a hacky workaround when you absolutely must have a Fragment + // handle the result, even when it was not the one to start the requested + // Activity. For example, the ExpandedAlarmViewHolder can start the ringtone + // picker dialog (which is an Activity) for a result; ExpandedAlarmViewHolder + // has no reference to the AlarmsFragment, but it does have a reference to a + // Context (which we can cast to Activity). Thus, ExpandedAlarmViewHolder + // uses Activity#startActivityForResult(). + mSectionsPagerAdapter.getFragment(mViewPager.getCurrentItem()) + .onActivityResult(requestCode, resultCode, data); + } + @Override protected int layoutResId() { return R.layout.activity_main; diff --git a/app/src/main/java/com/philliphsu/clock2/alarms/AlarmsFragment.java b/app/src/main/java/com/philliphsu/clock2/alarms/AlarmsFragment.java index 4f2c60f..0ee5366 100644 --- a/app/src/main/java/com/philliphsu/clock2/alarms/AlarmsFragment.java +++ b/app/src/main/java/com/philliphsu/clock2/alarms/AlarmsFragment.java @@ -2,18 +2,25 @@ package com.philliphsu.clock2.alarms; import android.app.Activity; import android.content.Intent; +import android.media.RingtoneManager; +import android.net.Uri; import android.os.Bundle; import android.os.Handler; +import android.preference.PreferenceManager; import android.support.annotation.Nullable; import android.support.v4.content.Loader; +import android.text.format.DateFormat; import android.util.Log; import android.view.View; +import android.view.ViewGroup; import com.philliphsu.clock2.Alarm; import com.philliphsu.clock2.AsyncAlarmsTableUpdateHandler; import com.philliphsu.clock2.R; import com.philliphsu.clock2.RecyclerViewFragment; -import com.philliphsu.clock2.editalarm.EditAlarmActivity; +import com.philliphsu.clock2.editalarm.BaseTimePickerDialog; +import com.philliphsu.clock2.editalarm.NumberGridTimePickerDialog; +import com.philliphsu.clock2.editalarm.NumpadTimePickerDialog; import com.philliphsu.clock2.model.AlarmCursor; import com.philliphsu.clock2.model.AlarmsListCursorLoader; import com.philliphsu.clock2.util.AlarmController; @@ -23,15 +30,24 @@ public class AlarmsFragment extends RecyclerViewFragment< Alarm, BaseAlarmViewHolder, AlarmCursor, - AlarmsCursorAdapter> implements ScrollHandler { // TODO: Move interface to base class + AlarmsCursorAdapter> + implements ScrollHandler, // TODO: Move interface to base class + BaseTimePickerDialog.OnTimeSetListener { private static final String TAG = "AlarmsFragment"; - private static final int REQUEST_EDIT_ALARM = 0; - // Public because MainActivity needs to use it. - // TODO: private because we handle fab clicks in the fragment now - public static final int REQUEST_CREATE_ALARM = 1; + private static final String TAG_TIME_PICKER = "time_picker"; + + // TODO: Delete these constants. We no longer use EditAlarmActivity. +// @Deprecated +// private static final int REQUEST_EDIT_ALARM = 0; +// // Public because MainActivity needs to use it. +// // TODO: private because we handle fab clicks in the fragment now +// @Deprecated +// public static final int REQUEST_CREATE_ALARM = 1; + + public static final int REQUEST_PICK_RINGTONE = 1; // private AlarmsCursorAdapter mAdapter; - private AsyncAlarmsTableUpdateHandler mAsyncAlarmsTableUpdateHandler; + private AsyncAlarmsTableUpdateHandler mAsyncUpdateHandler; private AlarmController mAlarmController; private Handler mHandler = new Handler(); private View mSnackbarAnchor; @@ -64,7 +80,7 @@ public class AlarmsFragment extends RecyclerViewFragment< // See the Fragment lifecycle. mSnackbarAnchor = getActivity().findViewById(R.id.main_content); mAlarmController = new AlarmController(getActivity(), mSnackbarAnchor); - mAsyncAlarmsTableUpdateHandler = new AsyncAlarmsTableUpdateHandler(getActivity(), + mAsyncUpdateHandler = new AsyncAlarmsTableUpdateHandler(getActivity(), mSnackbarAnchor, this, mAlarmController); } @@ -90,8 +106,37 @@ public class AlarmsFragment extends RecyclerViewFragment< @Override public void onFabClick() { - Intent intent = new Intent(getActivity(), EditAlarmActivity.class); - startActivityForResult(intent, REQUEST_CREATE_ALARM); +// Intent intent = new Intent(getActivity(), EditAlarmActivity.class); +// startActivityForResult(intent, REQUEST_CREATE_ALARM); + + // Close the keyboard first, or else our dialog will be screwed up. + // If not open, this does nothing. + // TODO: I don't think the keyboard can possibly be open in this Fragment? +// hideKeyboard(this); // This is only important for BottomSheetDialogs! + + // Create a new instance each time we want to show the dialog. + // If we keep a reference to the dialog, we keep its previous state as well. + // So the next time we call show() on it, the input field will show the + // last inputted time. + BaseTimePickerDialog dialog = null; + String numpadStyle = getString(R.string.number_pad); + String gridStyle = getString(R.string.grid_selector); + String prefTimePickerStyle = PreferenceManager.getDefaultSharedPreferences(getActivity()).getString( + // key for the preference value to retrieve + getString(R.string.key_time_picker_style), + // default value + numpadStyle); + if (prefTimePickerStyle.equals(numpadStyle)) { + dialog = NumpadTimePickerDialog.newInstance(this); + } else if (prefTimePickerStyle.equals(gridStyle)) { + dialog = NumberGridTimePickerDialog.newInstance( + this, // OnTimeSetListener + 0, // Initial hour of day + 0, // Initial minute + DateFormat.is24HourFormat(getActivity())); + } + // DISREGARD THE LINT WARNING ABOUT DIALOG BEING NULL. + dialog.show(getFragmentManager(), TAG_TIME_PICKER); } @Nullable @@ -113,39 +158,47 @@ public class AlarmsFragment extends RecyclerViewFragment< Log.d(TAG, "onActivityResult()"); if (resultCode != Activity.RESULT_OK || data == null) return; - final Alarm alarm = data.getParcelableExtra(EditAlarmActivity.EXTRA_MODIFIED_ALARM); - if (alarm == null) - return; - - // http://stackoverflow.com/a/27055512/5055032 - // "RecyclerView does not run animations in the first layout - // pass after being attached." A workaround is to postpone - // the CRUD operation to the next frame. A delay of 300ms is - // short enough to not be noticeable, and long enough to - // give us the animation *most of the time*. - switch (requestCode) { - case REQUEST_CREATE_ALARM: - mHandler.postDelayed( - new AsyncAddItemRunnable(mAsyncAlarmsTableUpdateHandler, alarm), - 300); - break; - case REQUEST_EDIT_ALARM: - if (data.getBooleanExtra(EditAlarmActivity.EXTRA_IS_DELETING, false)) { - // TODO: Should we delay this too? It seems animations run - // some of the time. - mAsyncAlarmsTableUpdateHandler.asyncDelete(alarm); - } else { - // TODO: Increase the delay, because update animation is - // more elusive than insert. - mHandler.postDelayed( - new AsyncUpdateItemRunnable(mAsyncAlarmsTableUpdateHandler, alarm), - 300); - } - break; - default: - Log.i(TAG, "Could not handle request code " + requestCode); - break; + if (requestCode == REQUEST_PICK_RINGTONE) { + Uri uri = data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI); + Log.d(TAG, "Retrieved ringtone URI: " + uri); + // TODO: We'll have to create a new Alarm instance with this ringtone value + // because we don't have a setter method. Alternatively, write an independent + // SQL update statement updating COLUMN_RINGTONE. } + +// final Alarm alarm = data.getParcelableExtra(EditAlarmActivity.EXTRA_MODIFIED_ALARM); +// if (alarm == null) +// return; +// +// // http://stackoverflow.com/a/27055512/5055032 +// // "RecyclerView does not run animations in the first layout +// // pass after being attached." A workaround is to postpone +// // the CRUD operation to the next frame. A delay of 300ms is +// // short enough to not be noticeable, and long enough to +// // give us the animation *most of the time*. +// switch (requestCode) { +// case REQUEST_CREATE_ALARM: +// mHandler.postDelayed( +// new AsyncAddItemRunnable(mAsyncUpdateHandler, alarm), +// 300); +// break; +// case REQUEST_EDIT_ALARM: +// if (data.getBooleanExtra(EditAlarmActivity.EXTRA_IS_DELETING, false)) { +// // TODO: Should we delay this too? It seems animations run +// // some of the time. +// mAsyncUpdateHandler.asyncDelete(alarm); +// } else { +// // TODO: Increase the delay, because update animation is +// // more elusive than insert. +// mHandler.postDelayed( +// new AsyncUpdateItemRunnable(mAsyncUpdateHandler, alarm), +// 300); +// } +// break; +// default: +// Log.i(TAG, "Could not handle request code " + requestCode); +// break; +// } } @Override @@ -160,7 +213,7 @@ public class AlarmsFragment extends RecyclerViewFragment< } ///////////////////////////////////////////////////////////////////////////////////////////////// - // TODO: Just like with TimersCursorAdapter, we could pass in the mAsyncAlarmsTableUpdateHandler + // TODO: Just like with TimersCursorAdapter, we could pass in the mAsyncUpdateHandler // to the AlarmsCursorAdapter and call these on the save and delete button click bindings. @Override @@ -168,7 +221,7 @@ public class AlarmsFragment extends RecyclerViewFragment< public void onListItemDeleted(final Alarm item) { // The corresponding VH will be automatically removed from view following // the requery, so we don't have to do anything to it. - mAsyncAlarmsTableUpdateHandler.asyncDelete(item); + mAsyncUpdateHandler.asyncDelete(item); } @Override @@ -180,7 +233,7 @@ public class AlarmsFragment extends RecyclerViewFragment< // TODO: Implement editing in the expanded VH. Then verify that changes // while in that VH are saved and updated after the requery. // getAdapter().collapse(position); - mAsyncAlarmsTableUpdateHandler.asyncUpdate(item.getId(), item); + mAsyncUpdateHandler.asyncUpdate(item.getId(), item); } ///////////////////////////////////////////////////////////////////////////////////////////////// @@ -197,6 +250,18 @@ public class AlarmsFragment extends RecyclerViewFragment< } } + @Override + public void onTimeSet(ViewGroup viewGroup, int hourOfDay, int minute) { + // When we request the Builder, default values are provided for us, + // which is why we don't have to set the ringtone, label, etc. + Alarm alarm = Alarm.builder() + .hour(hourOfDay) + .minutes(minute) + .build(); + alarm.setEnabled(true); + mAsyncUpdateHandler.asyncInsert(alarm); + } + ///////////////////////////////////////////////////////////////////////////////////// // TODO: We won't need these anymore, since we won't handle the db // update in onActivityResult() anymore. diff --git a/app/src/main/java/com/philliphsu/clock2/alarms/ExpandedAlarmViewHolder.java b/app/src/main/java/com/philliphsu/clock2/alarms/ExpandedAlarmViewHolder.java index f8303d2..027330e 100644 --- a/app/src/main/java/com/philliphsu/clock2/alarms/ExpandedAlarmViewHolder.java +++ b/app/src/main/java/com/philliphsu/clock2/alarms/ExpandedAlarmViewHolder.java @@ -1,5 +1,7 @@ package com.philliphsu.clock2.alarms; +import android.app.Activity; +import android.content.Intent; import android.media.RingtoneManager; import android.net.Uri; import android.view.View; @@ -23,7 +25,7 @@ import butterknife.OnClick; */ public class ExpandedAlarmViewHolder extends BaseAlarmViewHolder { - @Bind(R.id.save) Button mSave; + @Bind(R.id.ok) Button mOk; @Bind(R.id.delete) Button mDelete; @Bind(R.id.ringtone) Button mRingtone; @Bind(R.id.vibrate) CheckBox mVibrate; @@ -41,7 +43,7 @@ public class ExpandedAlarmViewHolder extends BaseAlarmViewHolder { listener.onListItemDeleted(getAlarm()); } }); - mSave.setOnClickListener(new View.OnClickListener() { + mOk.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { listener.onListItemUpdate(getAlarm(), getAdapterPosition()); @@ -74,7 +76,18 @@ public class ExpandedAlarmViewHolder extends BaseAlarmViewHolder { @OnClick(R.id.ringtone) void showRingtonePickerDialog() { - // TODO + Intent intent = new Intent(RingtoneManager.ACTION_RINGTONE_PICKER); + intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_ALARM) + .putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, false) + // The ringtone to show as selected when the dialog is opened + .putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, Uri.parse(getAlarm().ringtone())) + // Whether to show "Default" item in the list + .putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, false); + // The ringtone that plays when default option is selected + //.putExtra(RingtoneManager.EXTRA_RINGTONE_DEFAULT_URI, DEFAULT_TONE); + // TODO: This is VERY BAD. Use a Controller/Presenter instead. + // The result will be delivered to MainActivity, and then delegated to AlarmsFragment. + ((Activity) getContext()).startActivityForResult(intent, AlarmsFragment.REQUEST_PICK_RINGTONE); } /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/app/src/main/java/com/philliphsu/clock2/editalarm/BaseTimePickerDialog.java b/app/src/main/java/com/philliphsu/clock2/editalarm/BaseTimePickerDialog.java index a89b200..0425216 100644 --- a/app/src/main/java/com/philliphsu/clock2/editalarm/BaseTimePickerDialog.java +++ b/app/src/main/java/com/philliphsu/clock2/editalarm/BaseTimePickerDialog.java @@ -23,7 +23,7 @@ public abstract class BaseTimePickerDialog extends BottomSheetDialogFragment { * The callback interface used to indicate the user is done filling in * the time (they clicked on the 'Set' button). */ - interface OnTimeSetListener { + public interface OnTimeSetListener { /** * @param viewGroup The view associated with this listener. * @param hourOfDay The hour that was set. diff --git a/app/src/main/java/com/philliphsu/clock2/editalarm/EditAlarmActivity.java b/app/src/main/java/com/philliphsu/clock2/editalarm/EditAlarmActivity.java index 53b80b3..bca5efa 100644 --- a/app/src/main/java/com/philliphsu/clock2/editalarm/EditAlarmActivity.java +++ b/app/src/main/java/com/philliphsu/clock2/editalarm/EditAlarmActivity.java @@ -52,6 +52,7 @@ import static com.philliphsu.clock2.util.Preconditions.checkNotNull; * The class would have the API for editing the alarm, so move all * the relevant helper methods from here to there. */ +@Deprecated public class EditAlarmActivity extends BaseActivity implements EditAlarmContract.View, // TODO: Remove @Override from the methods AlarmUtilsHelper, @@ -517,7 +518,7 @@ public class EditAlarmActivity extends BaseActivity implements // The ringtone to show as selected when the dialog is opened .putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, mSelectedRingtoneUri) // Whether to show "Default" item in the list - .putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, false); // TODO: false? + .putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, false); // The ringtone that plays when default option is selected //.putExtra(RingtoneManager.EXTRA_RINGTONE_DEFAULT_URI, DEFAULT_TONE); startActivityForResult(intent, REQUEST_PICK_RINGTONE); diff --git a/app/src/main/res/layout/item_expanded_alarm.xml b/app/src/main/res/layout/item_expanded_alarm.xml index 72ddf34..8f9e993 100644 --- a/app/src/main/res/layout/item_expanded_alarm.xml +++ b/app/src/main/res/layout/item_expanded_alarm.xml @@ -12,14 +12,15 @@ - Alternatively, just keep the CardView because that takes care of the non-transparent - background issue for free. --> - @@ -128,6 +129,7 @@ android:layout_height="48dp" android:hint="Add label" android:layout_marginBottom="8dp" + android:background="?android:attr/selectableItemBackground" android:gravity="center_vertical"/> - +