/* * Copyright 2017 Gabor Varadi * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.zhuinden.simplestack; import android.os.Parcelable; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.SparseArray; import android.view.View; import com.zhuinden.statebundle.StateBundle; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; /** * The backstack manager manages a {@link Backstack} internally, and wraps it with the ability of persisting view state and the backstack history itself. * * The backstack is created by {@link BackstackManager#setup(List)}, and initialized by {@link BackstackManager#setStateChanger(StateChanger)}. */ public class BackstackManager implements Bundleable { /** * Specifies the strategy to be used in order to delete {@link SavedState}s that are no longer needed after a {@link StateChange}, when there is no pending {@link StateChange} left. */ public interface StateClearStrategy { /** * Allows a hook to clear the {@link SavedState} for obsolete keys. * * @param keyStateMap the map that contains the keys and their corresponding retained saved state. * @param stateChange the last state change */ void clearStatesNotIn(@NonNull Map<Object, SavedState> keyStateMap, @NonNull StateChange stateChange); } public interface StateChangeCompletionListener { /** * Called when the state change for the {@link Backstack} is completed, before the state is cleared by {@link StateClearStrategy}.. * * @param stateChange the state change */ void stateChangeCompleted(@NonNull StateChange stateChange); } private static final String HISTORY_TAG = "HISTORY"; private static final String STATES_TAG = "STATES"; static String getHistoryTag() { return HISTORY_TAG; } static String getStatesTag() { return STATES_TAG; } private final StateChanger managedStateChanger = new StateChanger() { @Override public void handleStateChange(final StateChange stateChange, final Callback completionCallback) { stateChanger.handleStateChange(stateChange, new Callback() { @Override public void stateChangeComplete() { completionCallback.stateChangeComplete(); for(StateChangeCompletionListener stateChangeCompletionListener : stateChangeCompletionListeners) { stateChangeCompletionListener.stateChangeCompleted(stateChange); } if(!backstack.isStateChangePending()) { stateClearStrategy.clearStatesNotIn(keyStateMap, stateChange); } } }); } }; private KeyFilter keyFilter = new DefaultKeyFilter(); private KeyParceler keyParceler = new DefaultKeyParceler(); private StateClearStrategy stateClearStrategy = new DefaultStateClearStrategy(); private List<StateChangeCompletionListener> stateChangeCompletionListeners = new LinkedList<>(); /** * Specifies a custom {@link KeyFilter}, allowing keys to be filtered out if they should not be restored after process death. * * If used, this method must be called before {@link BackstackManager#setup(List)} . * * @param keyFilter The custom {@link KeyFilter}. */ public void setKeyFilter(KeyFilter keyFilter) { if(backstack != null) { throw new IllegalStateException("Custom key filter should be set before calling `setup()`"); } if(keyFilter == null) { throw new IllegalArgumentException("The key filter cannot be null!"); } this.keyFilter = keyFilter; } /** * Specifies a custom {@link KeyParceler}, allowing key parcellation strategies to be used for turning a key into Parcelable. * * If used, this method must be called before {@link BackstackManager#setup(List)} . * * @param keyParceler The custom {@link KeyParceler}. */ public void setKeyParceler(KeyParceler keyParceler) { if(backstack != null) { throw new IllegalStateException("Custom key parceler should be set before calling `setup()`"); } if(keyParceler == null) { throw new IllegalArgumentException("The key parceler cannot be null!"); } this.keyParceler = keyParceler; } /** * Specifies a custom {@link StateClearStrategy}, allowing a custom strategy for clearing the retained state of keys. * The {@link DefaultStateClearStrategy} clears the {@link SavedState} for keys that are not found in the new state. * * If used, this method must be called before {@link BackstackManager#setup(List)} . * * @param stateClearStrategy The custom {@link StateClearStrategy}. */ public void setStateClearStrategy(StateClearStrategy stateClearStrategy) { if(backstack != null) { throw new IllegalStateException("Custom state clear strategy should be set before calling `setup()`"); } if(stateClearStrategy == null) { throw new IllegalArgumentException("The state clear strategy cannot be null!"); } this.stateClearStrategy = stateClearStrategy; } Backstack backstack; Map<Object, SavedState> keyStateMap = new HashMap<>(); StateChanger stateChanger; /** * Setup creates the {@link Backstack} with the specified initial keys. * * @param initialKeys the initial keys of the backstack */ public void setup(@NonNull List<?> initialKeys) { backstack = new Backstack(initialKeys); } /** * Gets the managed {@link Backstack}. It can only be called after {@link BackstackManager#setup(List)}. * * @return the backstack */ public Backstack getBackstack() { checkBackstack("You must call `setup()` before calling `getBackstack()`"); return backstack; } private void initializeBackstack(StateChanger stateChanger) { if(stateChanger != null) { backstack.setStateChanger(managedStateChanger, Backstack.INITIALIZE); } } /** * Sets the {@link StateChanger} for the given {@link Backstack}. This can only be called after {@link BackstackManager#setup(List)}. * * @param stateChanger the state changer */ public void setStateChanger(@Nullable StateChanger stateChanger) { checkBackstack("You must call `setup()` before calling `setStateChanger()."); if(backstack.hasStateChanger()) { backstack.removeStateChanger(); } this.stateChanger = stateChanger; initializeBackstack(stateChanger); } /** * Detaches the {@link StateChanger} from the {@link Backstack}. This can only be called after {@link BackstackManager#setup(List)}. */ public void detachStateChanger() { checkBackstack("You must call `setup()` before calling `detachStateChanger().`"); if(backstack.hasStateChanger()) { backstack.removeStateChanger(); } } /** * Reattaches the {@link StateChanger} to the {@link Backstack}. This can only be called after {@link BackstackManager#setup(List)}. */ public void reattachStateChanger() { checkBackstack("You must call `setup()` before calling `reattachStateChanger().`"); if(!backstack.hasStateChanger()) { backstack.setStateChanger(managedStateChanger, Backstack.REATTACH); } } /** * Returns a {@link SavedState} instance for the given key. * If the state does not exist, then a new associated state is created. * * @param key The key to which the {@link SavedState} belongs. * @return the saved state that belongs to the given key. */ @NonNull public SavedState getSavedState(@NonNull Object key) { if(key == null) { throw new IllegalArgumentException("Key cannot be null!"); } if(!keyStateMap.containsKey(key)) { keyStateMap.put(key, SavedState.builder().setKey(key).build()); } return keyStateMap.get(key); } // ----- viewstate persistence /** * Provides the means to save the provided view's hierarchy state * and its optional StateBundle via {@link Bundleable} into a {@link SavedState}. * * @param view the view that belongs to a certain key */ public void persistViewToState(@Nullable View view) { if(view != null) { Object key = KeyContextWrapper.getKey(view.getContext()); if(key == null) { throw new IllegalArgumentException("The view [" + view + "] contained no key!"); } SparseArray<Parcelable> viewHierarchyState = new SparseArray<>(); view.saveHierarchyState(viewHierarchyState); StateBundle bundle = null; if(view instanceof Bundleable) { bundle = ((Bundleable) view).toBundle(); } SavedState previousSavedState = SavedState.builder() // .setKey(key) // .setViewHierarchyState(viewHierarchyState) // .setBundle(bundle) // .build(); keyStateMap.put(key, previousSavedState); } } /** * Restores the state of the view based on the currently stored {@link SavedState}, according to the view's key. * * @param view the view that belongs to a certain key */ public void restoreViewFromState(@NonNull View view) { if(view == null) { throw new IllegalArgumentException("You cannot restore state into null view!"); } Object newKey = KeyContextWrapper.getKey(view.getContext()); SavedState savedState = getSavedState(newKey); view.restoreHierarchyState(savedState.getViewHierarchyState()); if(view instanceof Bundleable) { ((Bundleable) view).fromBundle(savedState.getBundle()); } } /** * Allows adding a {@link StateChangeCompletionListener} that is called when the state change is completed, but before the state is cleared. * * Please note that a strong reference is kept to the listener, and the {@link BackstackManager} is typically preserved across configuration change. * It is recommended that it is NOT an anonymous inner class or normal inner class in an Activity, * because that could cause memory leaks, unless the listener is removed in onDestroy(). * Instead, it should be a class, or a static inner class. * * @param stateChangeCompletionListener the state change completion listener. */ public void addStateChangeCompletionListener(StateChangeCompletionListener stateChangeCompletionListener) { this.stateChangeCompletionListeners.add(stateChangeCompletionListener); } /** * Removes the provided {@link StateChangeCompletionListener}. * * @param stateChangeCompletionListener the state change completion listener. */ public void removeStateChangeCompletionListener(StateChangeCompletionListener stateChangeCompletionListener) { this.stateChangeCompletionListeners.remove(stateChangeCompletionListener); } /** * Removes all {@link StateChangeCompletionListener}s added to the {@link BackstackManager}. */ public void removeAllStateChangeCompletionListeners() { this.stateChangeCompletionListeners.clear(); } /** * Restores the BackstackManager from a StateBundle. * This can only be called after {@link BackstackManager#setup(List)}. * * @param stateBundle the state bundle obtained via {@link BackstackManager#toBundle()} */ @Override public void fromBundle(@Nullable StateBundle stateBundle) { checkBackstack("A backstack must be set up before it is restored!"); if(stateBundle != null) { List<Object> keys = new ArrayList<>(); List<Parcelable> parcelledKeys = stateBundle.getParcelableArrayList(getHistoryTag()); if(parcelledKeys != null) { for(Parcelable parcelledKey : parcelledKeys) { keys.add(keyParceler.fromParcelable(parcelledKey)); } } keys = keyFilter.filterHistory(new ArrayList<>(keys)); if(keys == null) { keys = Collections.emptyList(); // lenient against null } if(!keys.isEmpty()) { backstack.setInitialParameters(keys); } List<ParcelledState> savedStates = stateBundle.getParcelableArrayList(getStatesTag()); if(savedStates != null) { for(ParcelledState parcelledState : savedStates) { Object key = keyParceler.fromParcelable(parcelledState.parcelableKey); if(!keys.contains(key)) { continue; } SavedState savedState = SavedState.builder().setKey(key) .setViewHierarchyState(parcelledState.viewHierarchyState) .setBundle(parcelledState.bundle) .build(); keyStateMap.put(savedState.getKey(), savedState); } } } } private void checkBackstack(String message) { if(backstack == null) { throw new IllegalStateException(message); } } /** * Persists the backstack history and view state into a StateBundle. * * @return the state bundle */ @NonNull @Override public StateBundle toBundle() { StateBundle stateBundle = new StateBundle(); ArrayList<Parcelable> history = new ArrayList<>(); for(Object key : backstack.getHistory()) { history.add(keyParceler.toParcelable(key)); } stateBundle.putParcelableArrayList(getHistoryTag(), history); ArrayList<ParcelledState> states = new ArrayList<>(); for(SavedState savedState : keyStateMap.values()) { ParcelledState parcelledState = new ParcelledState(); parcelledState.parcelableKey = keyParceler.toParcelable(savedState.getKey()); parcelledState.viewHierarchyState = savedState.getViewHierarchyState(); parcelledState.bundle = savedState.getBundle(); states.add(parcelledState); } stateBundle.putParcelableArrayList(getStatesTag(), states); return stateBundle; } }