Create DialogFragmentController to manage showing and restoring dialogs

This commit is contained in:
Phillip Hsu 2016-09-07 02:20:11 -07:00
parent e0ddd0b702
commit 17ad81d55e
9 changed files with 219 additions and 131 deletions

View File

@ -6,26 +6,24 @@ import android.util.Log;
/**
* Created by Phillip Hsu on 9/6/2016.
*/
public final class AddLabelDialogController {
private static final String TAG = "add_label_dialog";
public final class AddLabelDialogController extends DialogFragmentController<AddLabelDialog> {
private static final String TAG = "AddLabelController";
private final FragmentManager mFragmentManager;
private final AddLabelDialog.OnLabelSetListener mListener;
public AddLabelDialogController(FragmentManager fragmentManager, AddLabelDialog.OnLabelSetListener listener) {
mFragmentManager = fragmentManager;
super(fragmentManager);
mListener = listener;
}
public void show(CharSequence initialText) {
public void show(CharSequence initialText, String tag) {
AddLabelDialog dialog = AddLabelDialog.newInstance(mListener, initialText);
dialog.show(mFragmentManager, TAG);
// show(dialog, TAG);
show(dialog, tag);
}
// TODO: Rename to onConfigurationChange()?
public void tryRestoreCallback() {
AddLabelDialog labelDialog = (AddLabelDialog) mFragmentManager.findFragmentByTag(TAG);
@Override
public void tryRestoreCallback(String tag) {
AddLabelDialog labelDialog = findDialog(tag);
if (labelDialog != null) {
Log.i(TAG, "Restoring add label callback");
labelDialog.setOnLabelSetListener(mListener);

View File

@ -0,0 +1,44 @@
package com.philliphsu.clock2;
import android.support.annotation.Nullable;
import android.support.v4.app.DialogFragment;
import android.support.v4.app.FragmentManager;
import android.util.Log;
/**
* Created by Phillip Hsu on 9/6/2016.
*/
public abstract class DialogFragmentController<T extends DialogFragment> {
private static final String TAG = "DialogController";
private final FragmentManager mFragmentManager;
// TODO: Rename to onConfigurationChange()?
public abstract void tryRestoreCallback(String tag);
public DialogFragmentController(FragmentManager fragmentManager) {
mFragmentManager = fragmentManager;
}
/**
* Shows the dialog with the given tag.
*/
protected final void show(T dialog, String tag) {
Log.d(TAG, "Showing dialog " + dialog + "with tag " + tag);
dialog.show(mFragmentManager, tag);
}
/**
* Tries to find the dialog in our {@code FragmentManager} with the provided tag.
*/
@Nullable
@SuppressWarnings("unchecked")
protected final T findDialog(String tag) {
// https://docs.oracle.com/javase/tutorial/java/generics/restrictions.html#cannotCast
// Typically, we can't cast to a generic type. However, I've written non-generic code that
// blindly casts the result to an arbitrary type that I expect is correct, so this is
// pretty much the same thing.
Log.d(TAG, "Finding dialog with tag " + tag);
return (T) mFragmentManager.findFragmentByTag(tag);
}
}

View File

@ -0,0 +1,64 @@
package com.philliphsu.clock2;
import android.content.Context;
import android.preference.PreferenceManager;
import android.support.v4.app.FragmentManager;
import android.text.format.DateFormat;
import android.util.Log;
import com.philliphsu.clock2.editalarm.BaseTimePickerDialog;
import com.philliphsu.clock2.editalarm.NumberGridTimePickerDialog;
import com.philliphsu.clock2.editalarm.NumpadTimePickerDialog;
/**
* Created by Phillip Hsu on 9/6/2016.
*/
public final class TimePickerDialogController extends DialogFragmentController<BaseTimePickerDialog> {
private static final String TAG = "TimePickerController";
private final BaseTimePickerDialog.OnTimeSetListener mListener;
private final Context mContext;
/**
* @param context Used to read the user's preference for the style of the time picker dialog to show.
*/
public TimePickerDialogController(FragmentManager fragmentManager, Context context,
BaseTimePickerDialog.OnTimeSetListener listener) {
super(fragmentManager);
mContext = context;
mListener = listener;
}
public void show(int initialHourOfDay, int initialMinute, String tag) {
BaseTimePickerDialog dialog = null;
String numpadStyle = mContext.getString(R.string.number_pad);
String gridStyle = mContext.getString(R.string.grid_selector);
String prefTimePickerStyle = PreferenceManager.getDefaultSharedPreferences(mContext).getString(
// key for the preference value to retrieve
mContext.getString(R.string.key_time_picker_style),
// default value
numpadStyle);
if (prefTimePickerStyle.equals(numpadStyle)) {
dialog = NumpadTimePickerDialog.newInstance(mListener);
} else if (prefTimePickerStyle.equals(gridStyle)) {
dialog = NumberGridTimePickerDialog.newInstance(
mListener,
initialHourOfDay,
initialMinute,
DateFormat.is24HourFormat(mContext));
}
// We don't have a default case, because we don't need one; prefTimePickerStyle
// will ALWAYS match one of numpadStyle or gridStyle. As such, the dialog
// will NEVER be null.
show(dialog, tag);
}
@Override
public void tryRestoreCallback(String tag) {
BaseTimePickerDialog picker = findDialog(tag);
if (picker != null) {
Log.i(TAG, "Restoring time picker callback: " + mListener);
picker.setOnTimeSetListener(mListener);
}
}
}

View File

@ -16,13 +16,15 @@ import com.philliphsu.clock2.Alarm;
import com.philliphsu.clock2.AsyncAlarmsTableUpdateHandler;
import com.philliphsu.clock2.R;
import com.philliphsu.clock2.RecyclerViewFragment;
import com.philliphsu.clock2.TimePickerDialogController;
import com.philliphsu.clock2.editalarm.BaseTimePickerDialog;
import com.philliphsu.clock2.editalarm.TimePickerHelper;
import com.philliphsu.clock2.model.AlarmCursor;
import com.philliphsu.clock2.model.AlarmsListCursorLoader;
import com.philliphsu.clock2.util.AlarmController;
import com.philliphsu.clock2.util.DelayedSnackbarHandler;
import static com.philliphsu.clock2.util.FragmentTagUtils.makeTag;
public class AlarmsFragment extends RecyclerViewFragment<
Alarm,
BaseAlarmViewHolder,
@ -32,8 +34,6 @@ public class AlarmsFragment extends RecyclerViewFragment<
BaseTimePickerDialog.OnTimeSetListener {
private static final String TAG = "AlarmsFragment";
static final String TAG_TIME_PICKER = "time_picker";
private static final String KEY_EXPANDED_POSITION = "expanded_position";
// TODO: Delete these constants. We no longer use EditAlarmActivity.
@ -44,13 +44,15 @@ public class AlarmsFragment extends RecyclerViewFragment<
// @Deprecated
// public static final int REQUEST_CREATE_ALARM = 1;
// TODO: Delete this. We no longer use the system's ringtone picker.
public static final int REQUEST_PICK_RINGTONE = 1;
// private AlarmsCursorAdapter mAdapter;
private AsyncAlarmsTableUpdateHandler mAsyncUpdateHandler;
private AlarmController mAlarmController;
// TODO: Delete this. If I recall correctly, this was just used for delaying item animations.
private Handler mHandler = new Handler();
private View mSnackbarAnchor;
private TimePickerDialogController mTimePickerDialogController;
private int mExpandedPosition = RecyclerView.NO_POSITION;
@ -91,6 +93,9 @@ public class AlarmsFragment extends RecyclerViewFragment<
mAlarmController = new AlarmController(getActivity(), mSnackbarAnchor);
mAsyncUpdateHandler = new AsyncAlarmsTableUpdateHandler(getActivity(),
mSnackbarAnchor, this, mAlarmController);
mTimePickerDialogController = new TimePickerDialogController(
getFragmentManager(), getActivity(), this);
mTimePickerDialogController.tryRestoreCallback(makeTimePickerDialogTag());
}
@Override
@ -120,21 +125,7 @@ public class AlarmsFragment extends RecyclerViewFragment<
@Override
public void onFabClick() {
// 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 = TimePickerHelper.newDialog(getActivity(), this, 0, 0);
// DISREGARD THE LINT WARNING ABOUT DIALOG BEING NULL.
dialog.show(getFragmentManager(), TAG_TIME_PICKER);
mTimePickerDialogController.show(0, 0, makeTimePickerDialogTag());
}
@Override
@ -304,6 +295,10 @@ public class AlarmsFragment extends RecyclerViewFragment<
}
}
private static String makeTimePickerDialogTag() {
return makeTag(AlarmsFragment.class, R.id.fab);
}
/////////////////////////////////////////////////////////////////////////////////////
// TODO: We won't need these anymore, since we won't handle the db
// update in onActivityResult() anymore.

View File

@ -2,6 +2,7 @@ package com.philliphsu.clock2.alarms;
import android.content.res.ColorStateList;
import android.graphics.drawable.Drawable;
import android.support.annotation.IdRes;
import android.support.annotation.LayoutRes;
import android.support.annotation.NonNull;
import android.support.v4.app.FragmentManager;
@ -9,7 +10,6 @@ import android.support.v4.content.ContextCompat;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.SwitchCompat;
import android.text.format.DateFormat;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
@ -22,13 +22,13 @@ import com.philliphsu.clock2.Alarm;
import com.philliphsu.clock2.BaseViewHolder;
import com.philliphsu.clock2.OnListItemInteractionListener;
import com.philliphsu.clock2.R;
import com.philliphsu.clock2.TimePickerDialogController;
import com.philliphsu.clock2.aospdatetimepicker.Utils;
import com.philliphsu.clock2.editalarm.BaseTimePickerDialog;
import com.philliphsu.clock2.editalarm.BaseTimePickerDialog.OnTimeSetListener;
import com.philliphsu.clock2.editalarm.TimePickerHelper;
import com.philliphsu.clock2.editalarm.TimeTextUtils;
import com.philliphsu.clock2.util.AlarmController;
import com.philliphsu.clock2.util.AlarmUtils;
import com.philliphsu.clock2.util.FragmentTagUtils;
import java.util.Date;
@ -49,12 +49,14 @@ public abstract class BaseAlarmViewHolder extends BaseViewHolder<Alarm> {
private final AlarmController mAlarmController;
private final AddLabelDialogController mAddLabelDialogController;
private final TimePickerDialogController mTimePickerDialogController;
// TODO: Should we use VectorDrawable type?
private final Drawable mDismissNowDrawable;
private final Drawable mCancelSnoozeDrawable;
// Exposed for use by subclasses (obviously in this package.
// TODO: THis is still here for ExpandedVH's RingtonePickerDialog. If we ever write a
// Controller for it, finally delete this.
final FragmentManager mFragmentManager;
@Bind(R.id.time) TextView mTime;
@ -76,30 +78,48 @@ public abstract class BaseAlarmViewHolder extends BaseViewHolder<Alarm> {
// or simply pass in an instance of FragmentManager to the ctor.
AppCompatActivity act = (AppCompatActivity) getContext();
mFragmentManager = act.getSupportFragmentManager();
mAddLabelDialogController = new AddLabelDialogController(
mFragmentManager,
new AddLabelDialog.OnLabelSetListener() {
@Override
public void onLabelSet(String label) {
final Alarm oldAlarm = getAlarm();
Alarm newAlarm = oldAlarm.toBuilder()
.label(label)
.build();
oldAlarm.copyMutableFieldsTo(newAlarm);
persistUpdatedAlarm(newAlarm, false);
}
});
mAddLabelDialogController = new AddLabelDialogController(mFragmentManager,
// TODO: Why can't we implement the interface and pass `this` instead?
new AddLabelDialog.OnLabelSetListener() {
@Override
public void onLabelSet(String label) {
final Alarm oldAlarm = getAlarm();
Alarm newAlarm = oldAlarm.toBuilder()
.label(label)
.build();
oldAlarm.copyMutableFieldsTo(newAlarm);
persistUpdatedAlarm(newAlarm, false);
}
}
);
mTimePickerDialogController = new TimePickerDialogController(mFragmentManager, getContext(),
// TODO: Why can't we implement the interface and pass `this` instead?
new OnTimeSetListener() {
@Override
public void onTimeSet(ViewGroup viewGroup, int hourOfDay, int minute) {
final Alarm oldAlarm = getAlarm();
// I don't think we need this; scheduling a new alarm that is considered
// equal to a previous alarm will overwrite the previous alarm.
// mAlarmController.cancelAlarm(oldAlarm, false);
Alarm newAlarm = oldAlarm.toBuilder()
.hour(hourOfDay)
.minutes(minute)
.build();
oldAlarm.copyMutableFieldsTo(newAlarm);
// -------------------------------------------
// TOneverDO: precede copyMutableFieldsTo()
newAlarm.setEnabled(true); // Always enabled, esp. if oldAlarm is not enabled
// ----------------------------------------------
persistUpdatedAlarm(newAlarm, true);
}
}
);
// Are we recreating this because of a rotation?
// If so, try finding any dialog that was last shown in our backstack,
// and restore the callback.
BaseTimePickerDialog picker = (BaseTimePickerDialog)
mFragmentManager.findFragmentByTag(AlarmsFragment.TAG_TIME_PICKER);
if (picker != null) {
Log.i(TAG, "Restoring time picker callback");
picker.setOnTimeSetListener(newOnTimeSetListener());
}
mAddLabelDialogController.tryRestoreCallback();
mAddLabelDialogController.tryRestoreCallback(makeTag(R.id.label));
mTimePickerDialogController.tryRestoreCallback(makeTag(R.id.time));
}
@Override
@ -204,14 +224,12 @@ public abstract class BaseAlarmViewHolder extends BaseViewHolder<Alarm> {
@OnClick(R.id.time)
void openTimePicker() {
Alarm alarm = getAlarm();
BaseTimePickerDialog dialog = TimePickerHelper.newDialog(getContext(),
newOnTimeSetListener(), alarm.hour(), alarm.minutes());
dialog.show(mFragmentManager, AlarmsFragment.TAG_TIME_PICKER);
mTimePickerDialogController.show(alarm.hour(), alarm.minutes(), makeTag(R.id.time));
}
@OnClick(R.id.label)
void openLabelEditor() {
mAddLabelDialogController.show(mLabel.getText());
mAddLabelDialogController.show(mLabel.getText(), makeTag(R.id.label));
}
/**
@ -282,32 +300,7 @@ public abstract class BaseAlarmViewHolder extends BaseViewHolder<Alarm> {
bindLabel(visible, label);
}
private OnTimeSetListener newOnTimeSetListener() {
// Create a new listener per request. This is primarily used for
// setting the dialog callback again after a rotation.
//
// If we saved a reference to a listener, it would be tied to
// its ViewHolder instance. ViewHolders are reused, so we
// could accidentally leak this reference to other Alarm items
// in the list.
return new OnTimeSetListener() {
@Override
public void onTimeSet(ViewGroup viewGroup, int hourOfDay, int minute) {
final Alarm oldAlarm = getAlarm();
// I don't think we need this; scheduling a new alarm that is considered
// equal to a previous alarm will overwrite the previous alarm.
// mAlarmController.cancelAlarm(oldAlarm, false);
Alarm newAlarm = oldAlarm.toBuilder()
.hour(hourOfDay)
.minutes(minute)
.build();
oldAlarm.copyMutableFieldsTo(newAlarm);
// -------------------------------------------
// TOneverDO: precede copyMutableFieldsTo()
newAlarm.setEnabled(true); // Always enabled, esp. if oldAlarm is not enabled
// ----------------------------------------------
persistUpdatedAlarm(newAlarm, true);
}
};
private String makeTag(@IdRes int viewId) {
return FragmentTagUtils.makeTag(BaseAlarmViewHolder.class, viewId, getItemId());
}
}

View File

@ -1,42 +0,0 @@
package com.philliphsu.clock2.editalarm;
import android.content.Context;
import android.preference.PreferenceManager;
import android.text.format.DateFormat;
import com.philliphsu.clock2.R;
/**
* Created by Phillip Hsu on 9/2/2016.
*
* Helper for creating a time picker dialog.
*/
public final class TimePickerHelper {
public static BaseTimePickerDialog newDialog(Context context, BaseTimePickerDialog.OnTimeSetListener l,
int initialHourOfDay, int initialMinute) {
BaseTimePickerDialog dialog = null;
String numpadStyle = context.getString(R.string.number_pad);
String gridStyle = context.getString(R.string.grid_selector);
String prefTimePickerStyle = PreferenceManager.getDefaultSharedPreferences(context).getString(
// key for the preference value to retrieve
context.getString(R.string.key_time_picker_style),
// default value
numpadStyle);
if (prefTimePickerStyle.equals(numpadStyle)) {
dialog = NumpadTimePickerDialog.newInstance(l);
} else if (prefTimePickerStyle.equals(gridStyle)) {
dialog = NumberGridTimePickerDialog.newInstance(
l,
initialHourOfDay,
initialMinute,
DateFormat.is24HourFormat(context));
}
// We don't have a default case, because we don't need one; prefTimePickerStyle
// will ALWAYS match one of numpadStyle or gridStyle. As such, the dialog returned
// from here will NEVER be null.
return dialog;
}
private TimePickerHelper() {}
}

View File

@ -2,6 +2,7 @@ package com.philliphsu.clock2.edittimer;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.IdRes;
import android.support.design.widget.FloatingActionButton;
import android.support.v7.widget.GridLayout;
import android.text.InputType;
@ -15,6 +16,7 @@ import com.philliphsu.clock2.AddLabelDialog;
import com.philliphsu.clock2.AddLabelDialogController;
import com.philliphsu.clock2.BaseActivity;
import com.philliphsu.clock2.R;
import com.philliphsu.clock2.util.FragmentTagUtils;
import butterknife.Bind;
import butterknife.OnClick;
@ -50,7 +52,7 @@ public class EditTimerActivity extends BaseActivity implements AddLabelDialog.On
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mAddLabelDialogController = new AddLabelDialogController(getSupportFragmentManager(), this);
mAddLabelDialogController.tryRestoreCallback();
mAddLabelDialogController.tryRestoreCallback(makeTag(R.id.label));
}
@Override
@ -154,7 +156,7 @@ public class EditTimerActivity extends BaseActivity implements AddLabelDialog.On
@OnClick(R.id.label)
void openEditLabelDialog() {
mAddLabelDialogController.show(mLabel.getText());
mAddLabelDialogController.show(mLabel.getText(), makeTag(R.id.label));
}
@OnClick(R.id.fab)
@ -179,4 +181,8 @@ public class EditTimerActivity extends BaseActivity implements AddLabelDialog.On
private EditText getFocusedField() {
return (EditText) mEditFieldsLayout.findFocus();
}
private static String makeTag(@IdRes int viewId) {
return FragmentTagUtils.makeTag(EditTimerActivity.class, viewId);
}
}

View File

@ -2,6 +2,7 @@ package com.philliphsu.clock2.timers;
import android.animation.ObjectAnimator;
import android.graphics.drawable.Drawable;
import android.support.annotation.IdRes;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.PopupMenu;
@ -20,6 +21,7 @@ import com.philliphsu.clock2.BaseViewHolder;
import com.philliphsu.clock2.OnListItemInteractionListener;
import com.philliphsu.clock2.R;
import com.philliphsu.clock2.Timer;
import com.philliphsu.clock2.util.FragmentTagUtils;
import com.philliphsu.clock2.util.ProgressBarUtils;
import butterknife.Bind;
@ -30,7 +32,6 @@ import butterknife.OnClick;
*/
public class TimerViewHolder extends BaseViewHolder<Timer> {
private static final String TAG = "TimerViewHolder";
private static final String TAG_ADD_LABEL_DIALOG = "add_label_dialog";
private final AsyncTimersTableUpdateHandler mAsyncTimersTableUpdateHandler;
private TimerController mController;
@ -67,7 +68,7 @@ public class TimerViewHolder extends BaseViewHolder<Timer> {
mController.updateLabel(label);
}
});
mAddLabelDialogController.tryRestoreCallback();
mAddLabelDialogController.tryRestoreCallback(makeTag(R.id.label));
// The item layout is inflated in the super ctor, so we can safely reference our views.
mPopupMenu = new PopupMenu(getContext(), mMenuButton);
@ -115,7 +116,7 @@ public class TimerViewHolder extends BaseViewHolder<Timer> {
@OnClick(R.id.label)
void openLabelEditor() {
mAddLabelDialogController.show(mLabel.getText());
mAddLabelDialogController.show(mLabel.getText(), makeTag(R.id.label));
}
@OnClick(R.id.menu)
@ -195,4 +196,8 @@ public class TimerViewHolder extends BaseViewHolder<Timer> {
}
mSeekBar.getThumb().mutate().setAlpha(timeRemaining <= 0 ? 0 : 255);
}
private String makeTag(@IdRes int viewId) {
return FragmentTagUtils.makeTag(TimerViewHolder.class, viewId, getItemId());
}
}

View File

@ -0,0 +1,25 @@
package com.philliphsu.clock2.util;
import android.support.annotation.IdRes;
/**
* Created by Phillip Hsu on 9/7/2016.
*/
public final class FragmentTagUtils {
/**
* For general use.
*/
public static String makeTag(Class<?> cls, @IdRes int viewId) {
return cls.getName() + ":" + viewId;
}
/**
* A version suitable for our ViewHolders.
*/
public static String makeTag(Class<?> cls, @IdRes int viewId, long itemId) {
return makeTag(cls, viewId) + ":" + itemId;
}
private FragmentTagUtils() {}
}