Depcrecated EditAlarmActivity, moved some code to AlarmFragment and ExpandedAlarmViewHolder

This commit is contained in:
Phillip Hsu 2016-08-17 01:08:37 -07:00
parent e408a7467a
commit 3732246bb7
6 changed files with 188 additions and 63 deletions

View File

@ -218,6 +218,52 @@ public class MainActivity extends BaseActivity {
mAddItemDrawable = ContextCompat.getDrawable(this, R.drawable.ic_add_24dp); 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 @Override
protected int layoutResId() { protected int layoutResId() {
return R.layout.activity_main; return R.layout.activity_main;

View File

@ -2,18 +2,25 @@ package com.philliphsu.clock2.alarms;
import android.app.Activity; import android.app.Activity;
import android.content.Intent; import android.content.Intent;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.preference.PreferenceManager;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.v4.content.Loader; import android.support.v4.content.Loader;
import android.text.format.DateFormat;
import android.util.Log; import android.util.Log;
import android.view.View; import android.view.View;
import android.view.ViewGroup;
import com.philliphsu.clock2.Alarm; import com.philliphsu.clock2.Alarm;
import com.philliphsu.clock2.AsyncAlarmsTableUpdateHandler; import com.philliphsu.clock2.AsyncAlarmsTableUpdateHandler;
import com.philliphsu.clock2.R; import com.philliphsu.clock2.R;
import com.philliphsu.clock2.RecyclerViewFragment; 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.AlarmCursor;
import com.philliphsu.clock2.model.AlarmsListCursorLoader; import com.philliphsu.clock2.model.AlarmsListCursorLoader;
import com.philliphsu.clock2.util.AlarmController; import com.philliphsu.clock2.util.AlarmController;
@ -23,15 +30,24 @@ public class AlarmsFragment extends RecyclerViewFragment<
Alarm, Alarm,
BaseAlarmViewHolder, BaseAlarmViewHolder,
AlarmCursor, 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 String TAG = "AlarmsFragment";
private static final int REQUEST_EDIT_ALARM = 0; private static final String TAG_TIME_PICKER = "time_picker";
// Public because MainActivity needs to use it.
// TODO: private because we handle fab clicks in the fragment now // TODO: Delete these constants. We no longer use EditAlarmActivity.
public static final int REQUEST_CREATE_ALARM = 1; // @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 AlarmsCursorAdapter mAdapter;
private AsyncAlarmsTableUpdateHandler mAsyncAlarmsTableUpdateHandler; private AsyncAlarmsTableUpdateHandler mAsyncUpdateHandler;
private AlarmController mAlarmController; private AlarmController mAlarmController;
private Handler mHandler = new Handler(); private Handler mHandler = new Handler();
private View mSnackbarAnchor; private View mSnackbarAnchor;
@ -64,7 +80,7 @@ public class AlarmsFragment extends RecyclerViewFragment<
// See the Fragment lifecycle. // See the Fragment lifecycle.
mSnackbarAnchor = getActivity().findViewById(R.id.main_content); mSnackbarAnchor = getActivity().findViewById(R.id.main_content);
mAlarmController = new AlarmController(getActivity(), mSnackbarAnchor); mAlarmController = new AlarmController(getActivity(), mSnackbarAnchor);
mAsyncAlarmsTableUpdateHandler = new AsyncAlarmsTableUpdateHandler(getActivity(), mAsyncUpdateHandler = new AsyncAlarmsTableUpdateHandler(getActivity(),
mSnackbarAnchor, this, mAlarmController); mSnackbarAnchor, this, mAlarmController);
} }
@ -90,8 +106,37 @@ public class AlarmsFragment extends RecyclerViewFragment<
@Override @Override
public void onFabClick() { public void onFabClick() {
Intent intent = new Intent(getActivity(), EditAlarmActivity.class); // Intent intent = new Intent(getActivity(), EditAlarmActivity.class);
startActivityForResult(intent, REQUEST_CREATE_ALARM); // 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 @Nullable
@ -113,39 +158,47 @@ public class AlarmsFragment extends RecyclerViewFragment<
Log.d(TAG, "onActivityResult()"); Log.d(TAG, "onActivityResult()");
if (resultCode != Activity.RESULT_OK || data == null) if (resultCode != Activity.RESULT_OK || data == null)
return; return;
final Alarm alarm = data.getParcelableExtra(EditAlarmActivity.EXTRA_MODIFIED_ALARM); if (requestCode == REQUEST_PICK_RINGTONE) {
if (alarm == null) Uri uri = data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI);
return; 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.
}
// http://stackoverflow.com/a/27055512/5055032 // final Alarm alarm = data.getParcelableExtra(EditAlarmActivity.EXTRA_MODIFIED_ALARM);
// "RecyclerView does not run animations in the first layout // if (alarm == null)
// pass after being attached." A workaround is to postpone // return;
// the CRUD operation to the next frame. A delay of 300ms is //
// short enough to not be noticeable, and long enough to // // http://stackoverflow.com/a/27055512/5055032
// give us the animation *most of the time*. // // "RecyclerView does not run animations in the first layout
switch (requestCode) { // // pass after being attached." A workaround is to postpone
case REQUEST_CREATE_ALARM: // // the CRUD operation to the next frame. A delay of 300ms is
mHandler.postDelayed( // // short enough to not be noticeable, and long enough to
new AsyncAddItemRunnable(mAsyncAlarmsTableUpdateHandler, alarm), // // give us the animation *most of the time*.
300); // switch (requestCode) {
break; // case REQUEST_CREATE_ALARM:
case REQUEST_EDIT_ALARM: // mHandler.postDelayed(
if (data.getBooleanExtra(EditAlarmActivity.EXTRA_IS_DELETING, false)) { // new AsyncAddItemRunnable(mAsyncUpdateHandler, alarm),
// TODO: Should we delay this too? It seems animations run // 300);
// some of the time. // break;
mAsyncAlarmsTableUpdateHandler.asyncDelete(alarm); // case REQUEST_EDIT_ALARM:
} else { // if (data.getBooleanExtra(EditAlarmActivity.EXTRA_IS_DELETING, false)) {
// TODO: Increase the delay, because update animation is // // TODO: Should we delay this too? It seems animations run
// more elusive than insert. // // some of the time.
mHandler.postDelayed( // mAsyncUpdateHandler.asyncDelete(alarm);
new AsyncUpdateItemRunnable(mAsyncAlarmsTableUpdateHandler, alarm), // } else {
300); // // TODO: Increase the delay, because update animation is
} // // more elusive than insert.
break; // mHandler.postDelayed(
default: // new AsyncUpdateItemRunnable(mAsyncUpdateHandler, alarm),
Log.i(TAG, "Could not handle request code " + requestCode); // 300);
break; // }
} // break;
// default:
// Log.i(TAG, "Could not handle request code " + requestCode);
// break;
// }
} }
@Override @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. // to the AlarmsCursorAdapter and call these on the save and delete button click bindings.
@Override @Override
@ -168,7 +221,7 @@ public class AlarmsFragment extends RecyclerViewFragment<
public void onListItemDeleted(final Alarm item) { public void onListItemDeleted(final Alarm item) {
// The corresponding VH will be automatically removed from view following // The corresponding VH will be automatically removed from view following
// the requery, so we don't have to do anything to it. // the requery, so we don't have to do anything to it.
mAsyncAlarmsTableUpdateHandler.asyncDelete(item); mAsyncUpdateHandler.asyncDelete(item);
} }
@Override @Override
@ -180,7 +233,7 @@ public class AlarmsFragment extends RecyclerViewFragment<
// TODO: Implement editing in the expanded VH. Then verify that changes // TODO: Implement editing in the expanded VH. Then verify that changes
// while in that VH are saved and updated after the requery. // while in that VH are saved and updated after the requery.
// getAdapter().collapse(position); // 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 // TODO: We won't need these anymore, since we won't handle the db
// update in onActivityResult() anymore. // update in onActivityResult() anymore.

View File

@ -1,5 +1,7 @@
package com.philliphsu.clock2.alarms; package com.philliphsu.clock2.alarms;
import android.app.Activity;
import android.content.Intent;
import android.media.RingtoneManager; import android.media.RingtoneManager;
import android.net.Uri; import android.net.Uri;
import android.view.View; import android.view.View;
@ -23,7 +25,7 @@ import butterknife.OnClick;
*/ */
public class ExpandedAlarmViewHolder extends BaseAlarmViewHolder { 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.delete) Button mDelete;
@Bind(R.id.ringtone) Button mRingtone; @Bind(R.id.ringtone) Button mRingtone;
@Bind(R.id.vibrate) CheckBox mVibrate; @Bind(R.id.vibrate) CheckBox mVibrate;
@ -41,7 +43,7 @@ public class ExpandedAlarmViewHolder extends BaseAlarmViewHolder {
listener.onListItemDeleted(getAlarm()); listener.onListItemDeleted(getAlarm());
} }
}); });
mSave.setOnClickListener(new View.OnClickListener() { mOk.setOnClickListener(new View.OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
listener.onListItemUpdate(getAlarm(), getAdapterPosition()); listener.onListItemUpdate(getAlarm(), getAdapterPosition());
@ -74,7 +76,18 @@ public class ExpandedAlarmViewHolder extends BaseAlarmViewHolder {
@OnClick(R.id.ringtone) @OnClick(R.id.ringtone)
void showRingtonePickerDialog() { 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);
} }
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////

View File

@ -23,7 +23,7 @@ public abstract class BaseTimePickerDialog extends BottomSheetDialogFragment {
* The callback interface used to indicate the user is done filling in * The callback interface used to indicate the user is done filling in
* the time (they clicked on the 'Set' button). * the time (they clicked on the 'Set' button).
*/ */
interface OnTimeSetListener { public interface OnTimeSetListener {
/** /**
* @param viewGroup The view associated with this listener. * @param viewGroup The view associated with this listener.
* @param hourOfDay The hour that was set. * @param hourOfDay The hour that was set.

View File

@ -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 class would have the API for editing the alarm, so move all
* the relevant helper methods from here to there. * the relevant helper methods from here to there.
*/ */
@Deprecated
public class EditAlarmActivity extends BaseActivity implements public class EditAlarmActivity extends BaseActivity implements
EditAlarmContract.View, // TODO: Remove @Override from the methods EditAlarmContract.View, // TODO: Remove @Override from the methods
AlarmUtilsHelper, AlarmUtilsHelper,
@ -517,7 +518,7 @@ public class EditAlarmActivity extends BaseActivity implements
// The ringtone to show as selected when the dialog is opened // The ringtone to show as selected when the dialog is opened
.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, mSelectedRingtoneUri) .putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, mSelectedRingtoneUri)
// Whether to show "Default" item in the list // 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 // The ringtone that plays when default option is selected
//.putExtra(RingtoneManager.EXTRA_RINGTONE_DEFAULT_URI, DEFAULT_TONE); //.putExtra(RingtoneManager.EXTRA_RINGTONE_DEFAULT_URI, DEFAULT_TONE);
startActivityForResult(intent, REQUEST_PICK_RINGTONE); startActivityForResult(intent, REQUEST_PICK_RINGTONE);

View File

@ -12,7 +12,8 @@
- Alternatively, just keep the CardView because that takes care of the non-transparent - Alternatively, just keep the CardView because that takes care of the non-transparent
- background issue for free. - background issue for free.
--> -->
<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android" <android.support.v7.widget.CardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@ -128,6 +129,7 @@
android:layout_height="48dp" android:layout_height="48dp"
android:hint="Add label" android:hint="Add label"
android:layout_marginBottom="8dp" android:layout_marginBottom="8dp"
android:background="?android:attr/selectableItemBackground"
android:gravity="center_vertical"/> android:gravity="center_vertical"/>
<LinearLayout <LinearLayout
@ -169,9 +171,7 @@
</LinearLayout> </LinearLayout>
<View <View style="@style/Divider.Horizontal"/>
android:id="@+id/divider"
style="@style/Divider.Horizontal"/>
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
@ -187,11 +187,11 @@
android:text="@string/delete"/> android:text="@string/delete"/>
<Button <Button
android:id="@+id/save" android:id="@+id/ok"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
style="@style/Widget.AppCompat.Button.Borderless.Colored" style="@style/Widget.AppCompat.Button.Borderless.Colored"
android:text="@string/save"/> android:text="@android:string/ok"/>
</LinearLayout> </LinearLayout>