ViewsStateBundle.java

/*
 * Copyright (C) 2014 The Android Open Source Project
 *
 * 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 androidx.leanback.widget;

import static androidx.leanback.widget.BaseGridView.SAVE_ALL_CHILD;
import static androidx.leanback.widget.BaseGridView.SAVE_LIMITED_CHILD;
import static androidx.leanback.widget.BaseGridView.SAVE_NO_CHILD;
import static androidx.leanback.widget.BaseGridView.SAVE_ON_SCREEN_CHILD;

import android.os.Bundle;
import android.os.Parcelable;
import android.util.SparseArray;
import android.view.View;

import androidx.collection.LruCache;

import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;

/**
 * Maintains a bundle of states for a group of views. Each view must have a unique id to identify
 * it. There are four different strategies {@link #SAVE_NO_CHILD} {@link #SAVE_ON_SCREEN_CHILD}
 * {@link #SAVE_LIMITED_CHILD} {@link #SAVE_ALL_CHILD}.
 * <p>
 * This class serves purpose of nested "listview" e.g.  a vertical list of horizontal list.
 * Vertical list maintains id->bundle mapping of all its children (even the children is offscreen
 * and being pruned).
 * <p>
 * The class is currently used within {@link GridLayoutManager}, but it might be used by other
 * ViewGroup.
 */
class ViewsStateBundle {

    public static final int LIMIT_DEFAULT = 100;
    public static final int UNLIMITED = Integer.MAX_VALUE;

    private int mSavePolicy;
    private int mLimitNumber;

    private LruCache<String, SparseArray<Parcelable>> mChildStates;

    public ViewsStateBundle() {
        mSavePolicy = SAVE_NO_CHILD;
        mLimitNumber = LIMIT_DEFAULT;
    }

    public void clear() {
        if (mChildStates != null) {
            mChildStates.evictAll();
        }
    }

    public void remove(int id) {
        if (mChildStates != null && mChildStates.size() != 0) {
            mChildStates.remove(getSaveStatesKey(id));
        }
    }

    /**
     * @return the saved views states
     */
    public final Bundle saveAsBundle() {
        if (mChildStates == null || mChildStates.size() == 0) {
            return null;
        }
        Map<String, SparseArray<Parcelable>> snapshot = mChildStates.snapshot();
        Bundle bundle = new Bundle();
        for (Iterator<Entry<String, SparseArray<Parcelable>>> i =
                snapshot.entrySet().iterator(); i.hasNext(); ) {
            Entry<String, SparseArray<Parcelable>> e = i.next();
            bundle.putSparseParcelableArray(e.getKey(), e.getValue());
        }
        return bundle;
    }

    public final void loadFromBundle(Bundle savedBundle) {
        if (mChildStates != null && savedBundle != null) {
            mChildStates.evictAll();
            for (Iterator<String> i = savedBundle.keySet().iterator(); i.hasNext(); ) {
                String key = i.next();
                mChildStates.put(key, savedBundle.getSparseParcelableArray(key));
            }
        }
    }

    /**
     * @return the savePolicy, see {@link #SAVE_NO_CHILD} {@link #SAVE_ON_SCREEN_CHILD}
     *         {@link #SAVE_LIMITED_CHILD} {@link #SAVE_ALL_CHILD}
     */
    public final int getSavePolicy() {
        return mSavePolicy;
    }

    /**
     * @return the limitNumber, only works when {@link #getSavePolicy()} is
     *         {@link #SAVE_LIMITED_CHILD}
     */
    public final int getLimitNumber() {
        return mLimitNumber;
    }

    /**
     * @see ViewsStateBundle#getSavePolicy()
     */
    public final void setSavePolicy(int savePolicy) {
        this.mSavePolicy = savePolicy;
        applyPolicyChanges();
    }

    /**
     * @see ViewsStateBundle#getLimitNumber()
     */
    public final void setLimitNumber(int limitNumber) {
        this.mLimitNumber = limitNumber;
        applyPolicyChanges();
    }

    protected void applyPolicyChanges() {
        if (mSavePolicy == SAVE_LIMITED_CHILD) {
            if (mLimitNumber <= 0) {
                throw new IllegalArgumentException();
            }
            if (mChildStates == null || mChildStates.maxSize() != mLimitNumber) {
                mChildStates = new LruCache<String, SparseArray<Parcelable>>(mLimitNumber);
            }
        } else if (mSavePolicy == SAVE_ALL_CHILD || mSavePolicy == SAVE_ON_SCREEN_CHILD) {
            if (mChildStates == null || mChildStates.maxSize() != UNLIMITED) {
                mChildStates = new LruCache<String, SparseArray<Parcelable>>(UNLIMITED);
            }
        } else {
            mChildStates = null;
        }
    }

    /**
     * Load view from states, it's none operation if the there is no state associated with the id.
     *
     * @param view view where loads into
     * @param id unique id for the view within this ViewsStateBundle
     */
    public final void loadView(View view, int id) {
        if (mChildStates != null) {
            String key = getSaveStatesKey(id);
            // Once loaded the state, do not keep the state of child. The child state will
            // be saved again either when child is offscreen or when the parent is saved.
            SparseArray<Parcelable> container = mChildStates.remove(key);
            if (container != null) {
                view.restoreHierarchyState(container);
            }
        }
    }

    /**
     * Save views regardless what's the current policy is.
     *
     * @param view view to save
     * @param id unique id for the view within this ViewsStateBundle
     */
    protected final void saveViewUnchecked(View view, int id) {
        if (mChildStates != null) {
            String key = getSaveStatesKey(id);
            SparseArray<Parcelable> container = new SparseArray<Parcelable>();
            view.saveHierarchyState(container);
            mChildStates.put(key, container);
        }
    }

    /**
     * The on screen view is saved when policy is not {@link #SAVE_NO_CHILD}.
     *
     * @param bundle   Bundle where we save the on screen view state.  If null,
     *                 a new Bundle is created and returned.
     * @param view     The view to save.
     * @param id       Id of the view.
     */
    public final Bundle saveOnScreenView(Bundle bundle, View view, int id) {
        if (mSavePolicy != SAVE_NO_CHILD) {
            String key = getSaveStatesKey(id);
            SparseArray<Parcelable> container = new SparseArray<Parcelable>();
            view.saveHierarchyState(container);
            if (bundle == null) {
                bundle = new Bundle();
            }
            bundle.putSparseParcelableArray(key, container);
        }
        return bundle;
    }

    /**
     * Save off screen views according to policy.
     *
     * @param view view to save
     * @param id unique id for the view within this ViewsStateBundle
     */
    public final void saveOffscreenView(View view, int id) {
        switch (mSavePolicy) {
            case SAVE_LIMITED_CHILD:
            case SAVE_ALL_CHILD:
                saveViewUnchecked(view, id);
                break;
            case SAVE_ON_SCREEN_CHILD:
                remove(id);
                break;
            default:
                break;
        }
    }

    static String getSaveStatesKey(int id) {
        return Integer.toString(id);
    }
}