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:
parent
9026317b0e
commit
90cd59c866
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
Loading…
Reference in New Issue
Block a user