ConfigActivity: Fix fragment state when leaving/entering app

Do this by making the fragment transition functions idempotent.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
This commit is contained in:
Samuel Holland 2017-08-21 21:25:31 -05:00
parent 9026317b0e
commit 90cd59c866
4 changed files with 162 additions and 109 deletions

View File

@ -16,10 +16,6 @@ import com.wireguard.config.Config;
abstract class BaseConfigActivity extends Activity { abstract class BaseConfigActivity extends Activity {
protected static final String KEY_CURRENT_CONFIG = "currentConfig"; protected static final String KEY_CURRENT_CONFIG = "currentConfig";
protected static final String TAG_DETAIL = "detail";
protected static final String TAG_EDIT = "edit";
protected static final String TAG_LIST = "list";
protected static final String TAG_PLACEHOLDER = "placeholder";
private Config currentConfig; private Config currentConfig;
private String initialConfig; private String initialConfig;
@ -60,6 +56,8 @@ abstract class BaseConfigActivity extends Activity {
} }
public void setCurrentConfig(final Config config) { public void setCurrentConfig(final Config config) {
if (currentConfig == config)
return;
currentConfig = config; currentConfig = config;
onCurrentConfigChanged(currentConfig); onCurrentConfigChanged(currentConfig);
} }

View File

@ -41,6 +41,8 @@ abstract class BaseConfigFragment extends Fragment {
} }
public void setCurrentConfig(final Config config) { public void setCurrentConfig(final Config config) {
if (currentConfig == config)
return;
currentConfig = config; currentConfig = config;
onCurrentConfigChanged(currentConfig); onCurrentConfigChanged(currentConfig);
} }

View File

@ -16,38 +16,135 @@ import com.wireguard.config.Config;
*/ */
public class ConfigActivity extends BaseConfigActivity { public class ConfigActivity extends BaseConfigActivity {
private ConfigDetailFragment detailFragment; private static final String TAG_DETAIL = "detail";
private ConfigEditFragment editFragment; private static final String TAG_EDIT = "edit";
private static final String TAG_LIST = "list";
private final FragmentManager fm = getFragmentManager(); private final FragmentManager fm = getFragmentManager();
private final FragmentCache fragments = new FragmentCache(fm);
private boolean isEditing; private boolean isEditing;
private boolean isLayoutFinished;
private boolean isServiceAvailable; private boolean isServiceAvailable;
private boolean isSplitLayout; private boolean isSplitLayout;
private boolean isStateSaved; private boolean isStateSaved;
private ConfigListFragment listFragment;
private int mainContainer; private int mainContainer;
private boolean wasUpdateSkipped; private String visibleFragmentTag;
/**
* Updates the fragment visible in the UI.
* Sets visibleFragmentTag.
*
* @param config The config that should be visible.
* @param tag The tag of the fragment that should be visible.
*/
private void moveToFragment(final Config config, final String tag) {
// Sanity check.
if (tag == null && config != null)
throw new IllegalArgumentException("Cannot set a config on a null fragment");
if ((tag == null && !isSplitLayout) || (TAG_LIST.equals(tag) && isSplitLayout))
throw new IllegalArgumentException("Requested tag " + tag + " does not match layout");
// First tear down fragments as necessary.
if (tag == null || TAG_LIST.equals(tag) || (TAG_DETAIL.equals(tag)
&& TAG_EDIT.equals(visibleFragmentTag))) {
while (visibleFragmentTag != null && !visibleFragmentTag.equals(tag) &&
fm.getBackStackEntryCount() > 0) {
final Fragment removedFragment = fm.findFragmentById(mainContainer);
// The fragment *must* be removed first, or it will stay attached to the layout!
fm.beginTransaction().remove(removedFragment).commit();
fm.popBackStackImmediate();
// Recompute the visible fragment.
if (TAG_EDIT.equals(visibleFragmentTag))
visibleFragmentTag = TAG_DETAIL;
else if (!isSplitLayout && TAG_DETAIL.equals(visibleFragmentTag))
visibleFragmentTag = TAG_LIST;
else
throw new IllegalStateException();
}
}
// Now build up intermediate entries in the back stack as necessary.
if (TAG_EDIT.equals(tag) && !TAG_DETAIL.equals(visibleFragmentTag))
moveToFragment(config, TAG_DETAIL);
// Finally, set the main container's content to the new top-level fragment.
if (tag == null) {
if (visibleFragmentTag != null) {
final BaseConfigFragment fragment = fragments.get(visibleFragmentTag);
fm.beginTransaction().remove(fragment).commit();
fm.executePendingTransactions();
visibleFragmentTag = null;
}
} else if (!TAG_LIST.equals(tag)) {
final BaseConfigFragment fragment = fragments.get(tag);
if (!tag.equals(visibleFragmentTag)) {
final FragmentTransaction transaction = fm.beginTransaction();
if (TAG_EDIT.equals(tag) || (!isSplitLayout && TAG_DETAIL.equals(tag))) {
transaction.addToBackStack(null);
transaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
}
transaction.replace(mainContainer, fragment, tag).commit();
visibleFragmentTag = tag;
}
if (fragment.getCurrentConfig() != config)
fragment.setCurrentConfig(config);
}
}
/**
* Transition the state machine to the desired state, if possible.
* Sets currentConfig and isEditing.
*
* @param config The desired config to show in the UI.
* @param shouldBeEditing Whether or not the config should be in the editing state.
*/
private void moveToState(final Config config, final boolean shouldBeEditing) {
// Update the saved state.
setCurrentConfig(config);
isEditing = shouldBeEditing;
// Avoid performing fragment transactions when the app is not fully initialized.
if (!isLayoutFinished || !isServiceAvailable || isStateSaved)
return;
// Ensure the list is present in the master pane. It will be restored on activity restarts!
final BaseConfigFragment listFragment = fragments.get(TAG_LIST);
if (fm.findFragmentById(R.id.master_fragment) == null) {
fm.beginTransaction().add(R.id.master_fragment, listFragment, TAG_LIST).commit();
fm.executePendingTransactions();
}
// In the single-pane layout, the main container starts holding the list fragment.
if (!isSplitLayout && visibleFragmentTag == null)
visibleFragmentTag = TAG_LIST;
// Forward any config changes to the list (they may have come from the intent or editing).
listFragment.setCurrentConfig(config);
// Ensure the correct main fragment is visible, adjusting the back stack as necessary.
moveToFragment(config, shouldBeEditing ? TAG_EDIT :
(config != null ? TAG_DETAIL : (isSplitLayout ? null : TAG_LIST)));
// Show the current config as the title if the list of configurations is not visible.
setTitle(!isSplitLayout && config != null ? config.getName() : getString(R.string.app_name));
// Show or hide the action bar back button if the back stack is not empty.
if (getActionBar() != null) {
getActionBar().setDisplayHomeAsUpEnabled(config != null &&
(!isSplitLayout || shouldBeEditing));
}
}
@Override @Override
public void onBackPressed() { public void onBackPressed() {
super.onBackPressed(); super.onBackPressed();
if ((isEditing && isSplitLayout) || (!isEditing && !isSplitLayout)) { // The visible fragment is now the one that was on top of the back stack, if there was one.
if (getActionBar() != null)
getActionBar().setDisplayHomeAsUpEnabled(false);
}
// Ensure the current config is cleared when going back to the single-pane-layout list.
if (isEditing) if (isEditing)
isEditing = false; visibleFragmentTag = TAG_DETAIL;
else else if (!isSplitLayout && TAG_DETAIL.equals(visibleFragmentTag))
setCurrentConfig(null); visibleFragmentTag = TAG_LIST;
// If the user went back from the detail screen to the list, clear the current config.
moveToState(isEditing ? getCurrentConfig() : null, false);
} }
@Override @Override
public void onCreate(final Bundle savedInstanceState) { public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.config_activity); setContentView(R.layout.config_activity);
isSplitLayout = findViewById(R.id.detail_fragment) != null; isSplitLayout = findViewById(R.id.detail_fragment) != null;
mainContainer = isSplitLayout ? R.id.detail_fragment : R.id.master_fragment; mainContainer = isSplitLayout ? R.id.detail_fragment : R.id.master_fragment;
Log.d(getClass().getSimpleName(), "onCreate isSplitLayout=" + isSplitLayout); isLayoutFinished = true;
super.onCreate(savedInstanceState); moveToState(getCurrentConfig(), isEditing);
} }
@Override @Override
@ -58,52 +155,28 @@ public class ConfigActivity extends BaseConfigActivity {
@Override @Override
protected void onCurrentConfigChanged(final Config config) { protected void onCurrentConfigChanged(final Config config) {
Log.d(getClass().getSimpleName(), "onCurrentConfigChanged config=" + (config != null ? Log.d(getClass().getSimpleName(), "onCurrentConfigChanged: config=" +
config.getName() : null) + " fragment=" + fm.findFragmentById(mainContainer) + (config != null ? config.getName() : null));
(!isServiceAvailable || isStateSaved ? " SKIPPING" : "")); // Abandon editing a config when the current config changes.
// Avoid performing fragment transactions when it would be illegal or the service is null. moveToState(config, false);
if (!isServiceAvailable || isStateSaved) {
// Signal that updates need to be performed once the activity is resumed.
wasUpdateSkipped = true;
return;
} else {
// Now that an update is being performed, reset the flag.
wasUpdateSkipped = false;
}
// If the config change came from the intent or ConfigEditFragment, forward it to the list.
// listFragment is guaranteed not to be null at this point by onServiceAvailable().
if (listFragment.getCurrentConfig() != config)
listFragment.setCurrentConfig(config);
// Update the activity's title if the list of configurations is not visible.
if (!isSplitLayout)
setTitle(config != null ? config.getName() : getString(R.string.app_name));
// Update the fragment in the main container.
if (isEditing)
onBackPressed();
if (config != null) {
final boolean shouldPush = !isSplitLayout &&
fm.findFragmentById(mainContainer) instanceof ConfigListFragment;
switchToFragment(mainContainer, TAG_DETAIL, shouldPush);
} else if (isSplitLayout) {
switchToFragment(mainContainer, TAG_PLACEHOLDER, false);
}
} }
@Override @Override
public boolean onOptionsItemSelected(final MenuItem item) { public boolean onOptionsItemSelected(final MenuItem item) {
switch (item.getItemId()) { switch (item.getItemId()) {
case android.R.id.home: case android.R.id.home:
// The back arrow in the action bar should act the same as the back button.
onBackPressed(); onBackPressed();
return true; return true;
case R.id.menu_action_add: case R.id.menu_action_add:
startActivity(new Intent(this, ConfigAddActivity.class)); startActivity(new Intent(this, ConfigAddActivity.class));
return true; return true;
case R.id.menu_action_edit: case R.id.menu_action_edit:
isEditing = true; // Try to make the editing fragment visible.
switchToFragment(mainContainer, TAG_EDIT, true); moveToState(getCurrentConfig(), true);
return true; return true;
case R.id.menu_action_save: case R.id.menu_action_save:
// This menu item is handled by the current fragment. // This menu item is handled by the editing fragment.
return false; return false;
case R.id.menu_settings: case R.id.menu_settings:
startActivity(new Intent(this, SettingsActivity.class)); startActivity(new Intent(this, SettingsActivity.class));
@ -116,26 +189,17 @@ public class ConfigActivity extends BaseConfigActivity {
@Override @Override
public void onPostResume() { public void onPostResume() {
super.onPostResume(); super.onPostResume();
final boolean wasStateSaved = isStateSaved; // Allow changes to fragments.
isStateSaved = false; isStateSaved = false;
if (wasStateSaved || wasUpdateSkipped) moveToState(getCurrentConfig(), isEditing);
onCurrentConfigChanged(getCurrentConfig());
} }
@Override @Override
public void onSaveInstanceState(final Bundle outState) { public void onSaveInstanceState(final Bundle outState) {
// We cannot save fragments that might switch between containers if the layout changes. // We cannot save fragments that might switch between containers if the layout changes.
if (fm.getBackStackEntryCount() > 0) { if (isLayoutFinished && isServiceAvailable && !isStateSaved)
final int bottomEntryId = fm.getBackStackEntryAt(0).getId(); moveToFragment(null, isSplitLayout ? null : TAG_LIST);
fm.popBackStackImmediate(bottomEntryId, FragmentManager.POP_BACK_STACK_INCLUSIVE); // Prevent further changes to fragments.
}
if (isSplitLayout) {
final Fragment oldFragment = fm.findFragmentById(mainContainer);
if (oldFragment != null) {
fm.beginTransaction().remove(oldFragment).commit();
fm.executePendingTransactions();
}
}
isStateSaved = true; isStateSaved = true;
super.onSaveInstanceState(outState); super.onSaveInstanceState(outState);
} }
@ -143,55 +207,44 @@ public class ConfigActivity extends BaseConfigActivity {
@Override @Override
protected void onServiceAvailable() { protected void onServiceAvailable() {
super.onServiceAvailable(); super.onServiceAvailable();
// Allow creating fragments.
isServiceAvailable = true; isServiceAvailable = true;
// Create the initial fragment set. moveToState(getCurrentConfig(), isEditing);
final Fragment masterFragment = fm.findFragmentById(R.id.master_fragment);
if (masterFragment instanceof ConfigListFragment)
listFragment = (ConfigListFragment) masterFragment;
else
switchToFragment(R.id.master_fragment, TAG_LIST, false);
// This must run even if no update was skipped, so the restored config gets applied.
onCurrentConfigChanged(getCurrentConfig());
} }
private void switchToFragment(final int container, final String tag, final boolean push) { private static class FragmentCache {
if (tag.equals(TAG_PLACEHOLDER)) { private ConfigDetailFragment detailFragment;
final Fragment oldFragment = fm.findFragmentById(container); private ConfigEditFragment editFragment;
if (oldFragment != null) private final FragmentManager fm;
fm.beginTransaction().remove(oldFragment).commit(); private ConfigListFragment listFragment;
return;
private FragmentCache(final FragmentManager fm) {
this.fm = fm;
} }
final BaseConfigFragment fragment;
switch (tag) { private BaseConfigFragment get(final String tag) {
case TAG_DETAIL: switch (tag) {
if (detailFragment == null) case TAG_DETAIL:
detailFragment = new ConfigDetailFragment(); if (detailFragment == null)
fragment = detailFragment; detailFragment = (ConfigDetailFragment) fm.findFragmentByTag(tag);
break; if (detailFragment == null)
case TAG_EDIT: detailFragment = new ConfigDetailFragment();
if (editFragment == null) return detailFragment;
editFragment = new ConfigEditFragment(); case TAG_EDIT:
fragment = editFragment; if (editFragment == null)
break; editFragment = (ConfigEditFragment) fm.findFragmentByTag(tag);
case TAG_LIST: if (editFragment == null)
if (listFragment == null) editFragment = new ConfigEditFragment();
listFragment = new ConfigListFragment(); return editFragment;
fragment = listFragment; case TAG_LIST:
break; if (listFragment == null)
default: listFragment = (ConfigListFragment) fm.findFragmentByTag(tag);
throw new IllegalArgumentException(); if (listFragment == null)
} listFragment = new ConfigListFragment();
if (fragment.getCurrentConfig() != getCurrentConfig()) return listFragment;
fragment.setCurrentConfig(getCurrentConfig()); default:
if (fm.findFragmentById(container) != fragment) { throw new IllegalArgumentException();
final FragmentTransaction transaction = fm.beginTransaction();
if (push) {
transaction.addToBackStack(null);
transaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
if (getActionBar() != null && (!isSplitLayout || isEditing))
getActionBar().setDisplayHomeAsUpEnabled(true);
} }
transaction.replace(container, fragment, null).commit();
} }
} }
} }

View File

@ -75,7 +75,7 @@ public class ConfigListFragment extends BaseConfigFragment {
Log.d(getClass().getSimpleName(), "onCurrentConfigChanged config=" + Log.d(getClass().getSimpleName(), "onCurrentConfigChanged config=" +
(config != null ? config.getName() : null)); (config != null ? config.getName() : null));
final BaseConfigActivity activity = ((BaseConfigActivity) getActivity()); final BaseConfigActivity activity = ((BaseConfigActivity) getActivity());
if (activity != null && activity.getCurrentConfig() != config) if (activity != null)
activity.setCurrentConfig(config); activity.setCurrentConfig(config);
if (listView != null) if (listView != null)
setConfigChecked(config); setConfigChecked(config);