/*
* 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.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.View;
import com.zhuinden.statebundle.StateBundle;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
/**
* A delegate class that manages the {@link Backstack}'s Activity lifecycle integration,
* and provides view-state persistence for custom views that are associated with a key using {@link KeyContextWrapper}.
*
* This should be used in Activities to make sure that the {@link Backstack} survives both configuration changes and process death.
*/
public class BackstackDelegate {
private BackstackManager backstackManager;
private static final String UNINITIALIZED = "";
private String persistenceTag = UNINITIALIZED;
/**
* 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 BackstackDelegate#onCreate(Bundle, Object, ArrayList)}.
*
* @param keyFilter The custom {@link KeyFilter}.
*/
public void setKeyFilter(@NonNull KeyFilter keyFilter) {
if(backstackManager != null && backstackManager.getBackstack() != null) {
throw new IllegalStateException("If set, key filter must be set before calling `onCreate()`");
}
if(keyFilter == null) {
throw new IllegalArgumentException("Specified custom key filter should not 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 BackstackDelegate#onCreate(Bundle, Object, ArrayList)}.
*
* @param keyParceler The custom {@link KeyParceler}.
*/
public void setKeyParceler(@NonNull KeyParceler keyParceler) {
if(backstackManager != null && backstackManager.getBackstack() != null) {
throw new IllegalStateException("If set, key parceler must set before calling `onCreate()`");
}
if(keyParceler == null) {
throw new IllegalArgumentException("Specified custom key parceler should not be null!");
}
this.keyParceler = keyParceler;
}
/**
* Specifies a custom {@link BackstackManager.StateClearStrategy}, allowing a custom way of retaining saved state.
* The {@link DefaultStateClearStrategy} clears saved state for keys not found in the new state.
*
* If used, this method must be called before {@link BackstackDelegate#onCreate(Bundle, Object, ArrayList)}.
*
* @param stateClearStrategy The custom {@link BackstackManager.StateClearStrategy}.
*/
public void setStateClearStrategy(@NonNull BackstackManager.StateClearStrategy stateClearStrategy) {
if(backstackManager != null && backstackManager.getBackstack() != null) {
throw new IllegalStateException("If set, state clear strategy must be set before calling `onCreate()`");
}
if(stateClearStrategy == null) {
throw new IllegalArgumentException("Specified state clear strategy should not be null!");
}
this.stateClearStrategy = stateClearStrategy;
}
/**
* Adds a {@link BackstackManager.StateChangeCompletionListener}, which will be added to the {@link BackstackManager} when it is initialized.
* Please note that this should not be an anonymous inner class, because this is kept across configuration changes.
*
* @param stateChangeCompletionListener the state change completion listener
*/
public void addStateChangeCompletionListener(@NonNull BackstackManager.StateChangeCompletionListener stateChangeCompletionListener) {
if(backstackManager != null && backstackManager.getBackstack() != null) {
throw new IllegalStateException("If adding, completion listener must be added before calling `onCreate()`");
}
if(stateChangeCompletionListener == null) {
throw new IllegalArgumentException("Specified state change completion listener should not be null!");
}
this.stateChangeCompletionListeners.add(stateChangeCompletionListener);
}
private static final String HISTORY = "simplestack.HISTORY";
private StateChanger stateChanger;
private KeyFilter keyFilter = new DefaultKeyFilter();
private KeyParceler keyParceler = new DefaultKeyParceler();
private BackstackManager.StateClearStrategy stateClearStrategy = new DefaultStateClearStrategy();
private List<BackstackManager.StateChangeCompletionListener> stateChangeCompletionListeners = new LinkedList<>();
/**
* Persistence tag allows you to have multiple {@link BackstackDelegate}s in the same activity.
* This is required to make sure that the {@link Backstack} states do not overwrite each other in the saved instance state bundle.
* If used, this method must be called before {@link BackstackDelegate#onCreate(Bundle, Object, ArrayList)}.
* A persistence tag can only be set once on a given BackstackDelegate instance.
*
* @param persistenceTag a non-null persistence tag that uniquely identifies this {@link BackstackDelegate} inside the Activity.
*/
@SuppressWarnings("StringEquality")
public void setPersistenceTag(@NonNull String persistenceTag) {
if(backstackManager != null && backstackManager.getBackstack() != null) {
throw new IllegalStateException("Persistence tag should be set before calling `onCreate()`");
}
if(persistenceTag == null) {
throw new IllegalArgumentException("Null persistence tag is not allowed!");
}
if(this.persistenceTag == UNINITIALIZED) {
this.persistenceTag = persistenceTag;
} else if(!this.persistenceTag.equals(persistenceTag)) {
throw new IllegalStateException("The persistence tag cannot be set to a new value once it's already set!");
}
}
String getHistoryTag() {
return "".equals(persistenceTag) ? HISTORY : HISTORY + persistenceTag;
}
/**
* Creates the {@link BackstackDelegate}.
* If {@link StateChanger} is null, then the initialize {@link StateChange} is postponed until it is explicitly set.
* The {@link StateChanger} must be set at some point before {@link BackstackDelegate#onPostResume()}.
*
* @param stateChanger The {@link StateChanger} to be set. Allowed to be null at initialization.
*/
public BackstackDelegate(@Nullable StateChanger stateChanger) {
this.stateChanger = stateChanger;
}
/**
* The onCreate() delegate for the Activity.
* It initializes the backstack from either the non-configuration instance, the saved state, or creates a new one.
* Restores the {@link SavedState} that belongs to persisted view state.
* Begins an initialize {@link StateChange} if the {@link StateChanger} is set.
* Also registers a {@link Backstack.CompletionListener} that must be unregistered with {@link BackstackDelegate#onDestroy()}.
*
* @param savedInstanceState The Activity saved instance state bundle.
* @param nonConfigurationInstance The {@link NonConfigurationInstance} that is typically obtained with getLastCustomNonConfigurationInstance().
* @param initialKeys A list of the keys that are used to set as initial history of the backstack.
*/
public void onCreate(@Nullable Bundle savedInstanceState, @Nullable Object nonConfigurationInstance, @NonNull ArrayList<Object> initialKeys) {
if(nonConfigurationInstance != null && !(nonConfigurationInstance instanceof NonConfigurationInstance)) {
throw new IllegalArgumentException(
"The provided non configuration instance must be of type BackstackDelegate.NonConfigurationInstance!");
}
NonConfigurationInstance nonConfig = (NonConfigurationInstance) nonConfigurationInstance;
if(nonConfig != null) {
backstackManager = nonConfig.getBackstackManager();
}
if(backstackManager == null) {
backstackManager = new BackstackManager();
backstackManager.setKeyFilter(keyFilter);
backstackManager.setKeyParceler(keyParceler);
backstackManager.setStateClearStrategy(stateClearStrategy);
for(BackstackManager.StateChangeCompletionListener completionListener : stateChangeCompletionListeners) {
backstackManager.addStateChangeCompletionListener(completionListener);
}
backstackManager.setup(initialKeys);
if(savedInstanceState != null) {
backstackManager.fromBundle(savedInstanceState.<StateBundle>getParcelable(getHistoryTag()));
}
}
backstackManager.setStateChanger(stateChanger);
}
/**
* Sets the {@link StateChanger} to the {@link Backstack}. Removes the previous one if it there was already one set.
* This call begins an initialize {@link StateChange}.
*
* @param stateChanger The {@link StateChanger} to be set.
*/
public void setStateChanger(@Nullable StateChanger stateChanger) {
this.stateChanger = stateChanger;
backstackManager.setStateChanger(stateChanger);
}
/**
* The onRetainCustomNonConfigurationInstance() delegate for the Activity.
* This is required to make sure that the Backstack survives configuration change.
*
* @return a {@link NonConfigurationInstance} that contains the internal backstack instance.
*/
public NonConfigurationInstance onRetainCustomNonConfigurationInstance() {
return new NonConfigurationInstance(backstackManager);
}
/**
* The onBackPressed() delegate for the Activity.
* The call is delegated to {@link Backstack#goBack()}'.
*
* @return true if the {@link Backstack} handled the back press
*/
public boolean onBackPressed() {
return getBackstack().goBack();
}
/**
* The onSaveInstanceState() delegate for the Activity.
* This is required in order to save the {@link Backstack} and the {@link SavedState} persisted with {@link BackstackDelegate#persistViewToState(View)}
* into the Activity saved instance state bundle.
*
* @param outState the Bundle into which the backstack history and view states are saved.
*/
public void onSaveInstanceState(@NonNull Bundle outState) {
if(backstackManager == null) {
throw new IllegalStateException("You can call this method only after `onCreate()`");
}
outState.putParcelable(getHistoryTag(), backstackManager.toBundle());
}
/**
* The onPostResume() delegate for the Activity.
* It re-attaches the {@link StateChanger} if it is not already set.
*/
public void onPostResume() {
if(stateChanger == null) {
throw new IllegalStateException("State changer is still not set in `onPostResume`!");
}
if(backstackManager == null) {
throw new IllegalStateException("You can call this method only after `onCreate()`");
}
backstackManager.reattachStateChanger();
}
/**
* The onPause() delegate for the Activity.
* It removes the {@link StateChanger} if it is set.
*/
public void onPause() {
if(backstackManager == null) {
throw new IllegalStateException("You can call this method only after `onCreate()`");
}
backstackManager.detachStateChanger();
}
/**
* The onDestroy() delegate for the Activity.
* Forces any pending state change to execute with {@link Backstack#executePendingStateChange()}.
*/
public void onDestroy() {
getBackstack().executePendingStateChange();
}
// ----- get backstack
/**
* Returns the {@link Backstack} that belongs to this delegate.
* This method can only be invoked after {@link BackstackDelegate#onCreate(Bundle, Object, ArrayList)} has been called.
*
* @return the {@link Backstack} managed by this delegate.
*/
@NonNull
public Backstack getBackstack() {
if(backstackManager == null) {
throw new IllegalStateException("The backstack within the delegate must be initialized by `onCreate()`");
}
return backstackManager.getBackstack();
}
// ----- 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(backstackManager == null) {
throw new IllegalStateException("You can call this method only after `onCreate()`");
}
backstackManager.persistViewToState(view);
}
/**
* 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(backstackManager == null) {
throw new IllegalStateException("You can call this method only after `onCreate()`");
}
backstackManager.restoreViewFromState(view);
}
/**
* 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(backstackManager == null) {
throw new IllegalStateException("You can call this method only after `onCreate()`");
}
return backstackManager.getSavedState(key);
}
/**
* The class which stores the {@link BackstackManager} for surviving configuration change.
*/
public static class NonConfigurationInstance {
private BackstackManager backstackManager;
NonConfigurationInstance(BackstackManager backstackManager) {
this.backstackManager = backstackManager;
}
BackstackManager getBackstackManager() {
return backstackManager;
}
}
}