Display dialog if unsaved settings would be lost

When navigating away from a preferences screen and unsaved settings would be
lost, display a confirmation dialog. The user can either accept or cancel the
action. If canceling, the user is returned to the settings screen they were
currently on. Otherwise, they are taken to a new fragment (the exact
destination depends upon whether the user navigated "back" or selected another
header)

There is one additional change that needs to be made. In the case of navigating
to another header, we are notified _after_ the new header is selected. In this
scenario, the action is not cancelable and the user will lose any changes. We
must display an appropriate message when this happens. [note: this is the same
behaviour as when the user selects a breadcrumb]

bug 3327737

Change-Id: I4bd3b393a6323f3e63510e3ed08e4e1e745b04c4
This commit is contained in:
Todd Kennedy 2011-01-13 14:41:26 -08:00
parent fd1b9def4c
commit c890a4e4a2
5 changed files with 147 additions and 25 deletions

View File

@ -31,6 +31,9 @@ import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import java.net.URI;
import java.net.URISyntaxException;
/**
* Common base class for server settings fragments, so they can be more easily manipulated by
* AccountSettingsXL. Provides the following common functionality:
@ -42,18 +45,22 @@ import android.widget.Button;
public abstract class AccountServerBaseFragment extends Fragment
implements AccountCheckSettingsFragment.Callbacks, OnClickListener {
public static Bundle sSetupModeArgs = null;
protected static URI sDefaultUri;
private final static String BUNDLE_KEY_SETTINGS = "AccountServerBaseFragment.settings";
protected Context mContext;
protected Callback mCallback = EmptyCallback.INSTANCE;
protected boolean mSettingsMode;
// The URI that represents this account's currently saved settings
protected URI mLoadedUri;
// This is null in the setup wizard screens, and non-null in AccountSettings mode
public Button mProceedButton;
// This is used to debounce multiple clicks on the proceed button (which does async work)
public boolean mProceedButtonPressed;
public static Bundle sSetupModeArgs = null;
/**
* Callback interface that owning activities must provide
*/
@ -99,6 +106,16 @@ public abstract class AccountServerBaseFragment extends Fragment
return sSetupModeArgs;
}
public AccountServerBaseFragment() {
if (sDefaultUri == null) {
try {
sDefaultUri = new URI("");
} catch (URISyntaxException ignore) {
// ignore; will never happen
}
}
}
/**
* At onCreate time, read the fragment arguments
*/
@ -272,6 +289,21 @@ public abstract class AccountServerBaseFragment extends Fragment
throw new IllegalStateException();
}
/**
* Returns whether or not any settings have changed.
*/
public boolean haveSettingsChanged() {
URI newUri = null;
try {
newUri = getUri();
} catch (URISyntaxException ignore) {
// ignore
}
return (mLoadedUri == null) || !mLoadedUri.equals(newUri);
}
/**
* Save settings after "OK" result from checker. Concrete classes must implement.
* This is called from a worker thread and is allowed to perform DB operations.
@ -288,4 +320,7 @@ public abstract class AccountServerBaseFragment extends Fragment
* Respond to a click of the "Next" button. Concrete classes must implement.
*/
public abstract void onNext();
protected abstract URI getUri() throws URISyntaxException;
}

View File

@ -66,7 +66,6 @@ import java.util.List;
* dealing with accounts being added/deleted and triggering the header reload.
*/
public class AccountSettingsXL extends PreferenceActivity {
// Intent extras for our internal activity launch
/* package */ static final String EXTRA_ACCOUNT_ID = "AccountSettingsXL.account_id";
private static final String EXTRA_ENABLE_DEBUG = "AccountSettingsXL.enable_debug";
@ -243,8 +242,8 @@ public class AccountSettingsXL extends PreferenceActivity {
}
/**
* TODO: Any time we exit via this pathway, and we are showing a server settings fragment,
* we should put up the exit-save-changes dialog. This will work for the following cases:
* Any time we exit via this pathway, and we are showing a server settings fragment,
* we put up the exit-save-changes dialog. This will work for the following cases:
* Cancel button
* Back button
* Up arrow in application icon
@ -254,6 +253,15 @@ public class AccountSettingsXL extends PreferenceActivity {
*/
@Override
public void onBackPressed() {
if (mCurrentFragment instanceof AccountServerBaseFragment) {
boolean changed = ((AccountServerBaseFragment)mCurrentFragment).haveSettingsChanged();
if (changed) {
UnsavedChangesDialogFragment dialogFragment =
UnsavedChangesDialogFragment.newInstanceForBack();
dialogFragment.show(getFragmentManager(), UnsavedChangesDialogFragment.TAG);
return; // Prevent "back" from being handled
}
}
super.onBackPressed();
}
@ -444,12 +452,13 @@ public class AccountSettingsXL extends PreferenceActivity {
public void onHeaderClick(Header header, int position) {
// special case when exiting the server settings fragments
if (mCurrentFragment instanceof AccountServerBaseFragment) {
if (position != mCurrentHeaderPosition) {
boolean changed = ((AccountServerBaseFragment)mCurrentFragment).haveSettingsChanged();
if (changed) {
UnsavedChangesDialogFragment dialogFragment =
UnsavedChangesDialogFragment.newInstance(position);
UnsavedChangesDialogFragment.newInstanceForHeader(position);
dialogFragment.show(getFragmentManager(), UnsavedChangesDialogFragment.TAG);
return;
}
return;
}
// Secret keys: Click 10x to enable debug settings
@ -472,10 +481,23 @@ public class AccountSettingsXL extends PreferenceActivity {
* in {@link #onHeaderClick(Header, int)}. Called after we interrupted a header switch
* with a dialog, and the user OK'd it.
*/
private void forceSwitchHeader(int newPosition) {
mCurrentHeaderPosition = newPosition;
Header header = mGeneratedHeaders.get(newPosition);
switchToHeader(header.fragment, header.fragmentArguments);
private void forceSwitchHeader(int position) {
mCurrentHeaderPosition = position;
// Clear the current fragment; we're navigating away
mCurrentFragment = null;
// Ensure the UI visually shows the correct header selected
setSelection(position);
Header header = mGeneratedHeaders.get(position);
switchToHeader(header);
}
/**
* Forcefully go backward in the stack. This may potentially discard unsaved settings.
*/
private void forceBack() {
// Clear the current fragment; we're navigating away
mCurrentFragment = null;
onBackPressed();
}
/**
@ -539,6 +561,8 @@ public class AccountSettingsXL extends PreferenceActivity {
*/
public void onCheckSettingsComplete(int result, int setupMode) {
if (result == AccountCheckSettingsFragment.CHECK_SETTINGS_OK) {
// Settings checked & saved; clear current fragment
mCurrentFragment = null;
onBackPressed();
}
}
@ -653,27 +677,47 @@ public class AccountSettingsXL extends PreferenceActivity {
/**
* Dialog fragment to show "exit with unsaved changes?" dialog
*/
public static class UnsavedChangesDialogFragment extends DialogFragment {
/* package */ static class UnsavedChangesDialogFragment extends DialogFragment {
private final static String TAG = "UnsavedChangesDialogFragment";
// Argument bundle keys
private final static String BUNDLE_KEY_NEW_HEADER = "UnsavedChangesDialogFragment.Header";
private final static String BUNDLE_KEY_HEADER = "UnsavedChangesDialogFragment.Header";
private final static String BUNDLE_KEY_BACK = "UnsavedChangesDialogFragment.Back";
/**
* Create the dialog with parameters
* Creates a save changes dialog when the user selects a new header
* @param position The new header index to make active if the user accepts the dialog. This
* must be a valid header index although there is no error checking.
*/
public static UnsavedChangesDialogFragment newInstance(int newPosition) {
public static UnsavedChangesDialogFragment newInstanceForHeader(int position) {
UnsavedChangesDialogFragment f = new UnsavedChangesDialogFragment();
Bundle b = new Bundle();
b.putInt(BUNDLE_KEY_NEW_HEADER, newPosition);
b.putInt(BUNDLE_KEY_HEADER, position);
f.setArguments(b);
return f;
}
/**
* Creates a save changes dialog when the user navigates "back".
* {@link AccountSettingsXL#onBackPressed()} defines in which case this may be triggered.
*/
public static UnsavedChangesDialogFragment newInstanceForBack() {
UnsavedChangesDialogFragment f = new UnsavedChangesDialogFragment();
Bundle b = new Bundle();
b.putBoolean(BUNDLE_KEY_BACK, true);
f.setArguments(b);
return f;
}
// Force usage of newInstance()
private UnsavedChangesDialogFragment() {
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
final AccountSettingsXL activity = (AccountSettingsXL) getActivity();
final int newPosition = getArguments().getInt(BUNDLE_KEY_NEW_HEADER);
final int position = getArguments().getInt(BUNDLE_KEY_HEADER);
final boolean isBack = getArguments().getBoolean(BUNDLE_KEY_BACK);
return new AlertDialog.Builder(activity)
.setIcon(android.R.drawable.ic_dialog_alert)
@ -683,13 +727,16 @@ public class AccountSettingsXL extends PreferenceActivity {
R.string.okay_action,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
activity.forceSwitchHeader(newPosition);
if (isBack) {
activity.forceBack();
} else {
activity.forceSwitchHeader(position);
}
dismiss();
}
})
.setNegativeButton(
activity.getString(R.string.cancel_action),
null)
activity.getString(R.string.cancel_action), null)
.create();
}
}

View File

@ -262,6 +262,12 @@ public class AccountSetupExchangeFragment extends AccountServerBaseFragment
mTrustCertificatesView.setChecked(trustCertificates);
showTrustCertificates(ssl);
try {
mLoadedUri = getUri();
} catch (URISyntaxException ignore) {
// ignore; should not happen
}
mLoaded = true;
return validateFields();
}
@ -347,7 +353,8 @@ public class AccountSetupExchangeFragment extends AccountServerBaseFragment
* a problem with the user input.
* @return a URI built from the account setup fields
*/
/* package */ URI getUri() throws URISyntaxException {
@Override
protected URI getUri() throws URISyntaxException {
boolean sslRequired = mSslSecurityView.isChecked();
boolean trustCertificates = mTrustCertificatesView.isChecked();
String scheme = (sslRequired)

View File

@ -78,6 +78,8 @@ public class AccountSetupIncomingFragment extends AccountServerBaseFragment {
private Spinner mDeletePolicyView;
private View mImapPathPrefixSectionView;
private EditText mImapPathPrefixView;
// Delete policy as loaded from the device
private int mLoadedDeletePolicy;
// Support for lifecycle
private boolean mStarted;
@ -333,7 +335,8 @@ public class AccountSetupIncomingFragment extends AccountServerBaseFragment {
}
if (uri.getScheme().startsWith("pop3")) {
SpinnerOption.setSpinnerOptionValue(mDeletePolicyView, account.getDeletePolicy());
mLoadedDeletePolicy = account.getDeletePolicy();
SpinnerOption.setSpinnerOptionValue(mDeletePolicyView, mLoadedDeletePolicy);
} else if (uri.getScheme().startsWith("imap")) {
if (uri.getPath() != null && uri.getPath().length() > 0) {
mImapPathPrefixView.setText(uri.getPath().substring(1));
@ -363,6 +366,13 @@ public class AccountSetupIncomingFragment extends AccountServerBaseFragment {
*/
throw new Error(use);
}
try {
mLoadedUri = getUri();
} catch (URISyntaxException ignore) {
// ignore; should not happen
}
mLoaded = true;
validateFields();
}
@ -440,7 +450,8 @@ public class AccountSetupIncomingFragment extends AccountServerBaseFragment {
* a problem with the user input.
* @return a URI built from the account setup fields
*/
/* package */ URI getUri() throws URISyntaxException {
@Override
protected URI getUri() throws URISyntaxException {
int securityType = (Integer)((SpinnerOption)mSecurityTypeView.getSelectedItem()).value;
String path = null;
if (mAccountSchemes[securityType].startsWith("imap")) {
@ -485,4 +496,18 @@ public class AccountSetupIncomingFragment extends AccountServerBaseFragment {
throw new Error(use);
}
}
@Override
public boolean haveSettingsChanged() {
boolean deletePolicyChanged = false;
// Only verify the delete policy if the control is visible (i.e. is a pop3 account)
if (mDeletePolicyView.getVisibility() == View.VISIBLE) {
int newDeletePolicy =
(Integer)((SpinnerOption)mDeletePolicyView.getSelectedItem()).value;
deletePolicyChanged = mLoadedDeletePolicy != newDeletePolicy;
}
return deletePolicyChanged || super.haveSettingsChanged();
}
}

View File

@ -300,6 +300,13 @@ public class AccountSetupOutgoingFragment extends AccountServerBaseFragment
*/
throw new Error(use);
}
try {
mLoadedUri = getUri();
} catch (URISyntaxException ignore) {
// ignore; should not happen
}
mLoaded = true;
validateFields();
}
@ -368,7 +375,8 @@ public class AccountSetupOutgoingFragment extends AccountServerBaseFragment
* a problem with the user input.
* @return a URI built from the account setup fields
*/
/* package */ URI getUri() throws URISyntaxException {
@Override
protected URI getUri() throws URISyntaxException {
int securityType = (Integer)((SpinnerOption)mSecurityTypeView.getSelectedItem()).value;
String userInfo = null;
if (mRequireLoginView.isChecked()) {