1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 * in compliance with the License. You may obtain a copy of the License at
6 *
7 * http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software distributed under the License
10 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11 * or implied. See the License for the specific language governing permissions and limitations under
12 * the License.
13 */
14package android.support.v17.leanback.widget;
15
16import android.os.Bundle;
17import android.os.Parcelable;
18import android.support.v4.util.LruCache;
19import android.util.SparseArray;
20import android.view.View;
21
22import java.util.Iterator;
23import java.util.Map;
24import java.util.Map.Entry;
25
26import static android.support.v17.leanback.widget.BaseGridView.SAVE_NO_CHILD;
27import static android.support.v17.leanback.widget.BaseGridView.SAVE_ON_SCREEN_CHILD;
28import static android.support.v17.leanback.widget.BaseGridView.SAVE_LIMITED_CHILD;
29import static android.support.v17.leanback.widget.BaseGridView.SAVE_ALL_CHILD;
30
31/**
32 * Maintains a bundle of states for a group of views. Each view must have a unique id to identify
33 * it. There are four different strategies {@link #SAVE_NO_CHILD} {@link #SAVE_ON_SCREEN_CHILD}
34 * {@link #SAVE_LIMITED_CHILD} {@link #SAVE_ALL_CHILD}.
35 * <p>
36 * This class serves purpose of nested "listview" e.g.  a vertical list of horizontal list.
37 * Vertical list maintains id->bundle mapping of all it's children (even the children is offscreen
38 * and being pruned).
39 * <p>
40 * The class is currently used within {@link GridLayoutManager}, but it might be used by other
41 * ViewGroup.
42 */
43class ViewsStateBundle {
44
45    public static final int LIMIT_DEFAULT = 100;
46    public static final int UNLIMITED = Integer.MAX_VALUE;
47
48    private int mSavePolicy;
49    private int mLimitNumber;
50
51    private LruCache<String, SparseArray<Parcelable>> mChildStates;
52
53    public ViewsStateBundle() {
54        mSavePolicy = SAVE_NO_CHILD;
55        mLimitNumber = LIMIT_DEFAULT;
56    }
57
58    public void clear() {
59        if (mChildStates != null) {
60            mChildStates.evictAll();
61        }
62    }
63
64    public void remove(int id) {
65        if (mChildStates != null && mChildStates.size() != 0) {
66            mChildStates.remove(getSaveStatesKey(id));
67        }
68    }
69
70    /**
71     * @return the saved views states
72     */
73    public final Bundle saveAsBundle() {
74        if (mChildStates == null || mChildStates.size() == 0) {
75            return null;
76        }
77        Map<String, SparseArray<Parcelable>> snapshot = mChildStates.snapshot();
78        Bundle bundle = new Bundle();
79        for (Iterator<Entry<String, SparseArray<Parcelable>>> i =
80                snapshot.entrySet().iterator(); i.hasNext(); ) {
81            Entry<String, SparseArray<Parcelable>> e = i.next();
82            bundle.putSparseParcelableArray(e.getKey(), e.getValue());
83        }
84        return bundle;
85    }
86
87    public final void loadFromBundle(Bundle savedBundle) {
88        if (mChildStates != null && savedBundle != null) {
89            mChildStates.evictAll();
90            for (Iterator<String> i = savedBundle.keySet().iterator(); i.hasNext(); ) {
91                String key = i.next();
92                mChildStates.put(key, savedBundle.getSparseParcelableArray(key));
93            }
94        }
95    }
96
97    /**
98     * @return the savePolicy, see {@link #SAVE_NO_CHILD} {@link #SAVE_ON_SCREEN_CHILD}
99     *         {@link #SAVE_LIMITED_CHILD} {@link #SAVE_ALL_CHILD}
100     */
101    public final int getSavePolicy() {
102        return mSavePolicy;
103    }
104
105    /**
106     * @return the limitNumber, only works when {@link #getSavePolicy()} is
107     *         {@link #SAVE_LIMITED_CHILD}
108     */
109    public final int getLimitNumber() {
110        return mLimitNumber;
111    }
112
113    /**
114     * @see ViewsStateBundle#getSavePolicy()
115     */
116    public final void setSavePolicy(int savePolicy) {
117        this.mSavePolicy = savePolicy;
118        applyPolicyChanges();
119    }
120
121    /**
122     * @see ViewsStateBundle#getLimitNumber()
123     */
124    public final void setLimitNumber(int limitNumber) {
125        this.mLimitNumber = limitNumber;
126        applyPolicyChanges();
127    }
128
129    protected void applyPolicyChanges() {
130        if (mSavePolicy == SAVE_LIMITED_CHILD) {
131            if (mLimitNumber <= 0) {
132                throw new IllegalArgumentException();
133            }
134            if (mChildStates == null || mChildStates.maxSize() != mLimitNumber) {
135                mChildStates = new LruCache<String, SparseArray<Parcelable>>(mLimitNumber);
136            }
137        } else if (mSavePolicy == SAVE_ALL_CHILD || mSavePolicy == SAVE_ON_SCREEN_CHILD) {
138            if (mChildStates == null || mChildStates.maxSize() != UNLIMITED) {
139                mChildStates = new LruCache<String, SparseArray<Parcelable>>(UNLIMITED);
140            }
141        } else {
142            mChildStates = null;
143        }
144    }
145
146    /**
147     * Load view from states, it's none operation if the there is no state associated with the id.
148     *
149     * @param view view where loads into
150     * @param id unique id for the view within this ViewsStateBundle
151     */
152    public final void loadView(View view, int id) {
153        if (mChildStates != null) {
154            String key = getSaveStatesKey(id);
155            // Once loaded the state, do not keep the state of child. The child state will
156            // be saved again either when child is offscreen or when the parent is saved.
157            SparseArray<Parcelable> container = mChildStates.remove(key);
158            if (container != null) {
159                view.restoreHierarchyState(container);
160            }
161        }
162    }
163
164    /**
165     * Save views regardless what's the current policy is.
166     *
167     * @param view view to save
168     * @param id unique id for the view within this ViewsStateBundle
169     */
170    protected final void saveViewUnchecked(View view, int id) {
171        if (mChildStates != null) {
172            String key = getSaveStatesKey(id);
173            SparseArray<Parcelable> container = new SparseArray<Parcelable>();
174            view.saveHierarchyState(container);
175            mChildStates.put(key, container);
176        }
177    }
178
179    /**
180     * The on screen view is saved when policy is not {@link #SAVE_NO_CHILD}.
181     *
182     * @param bundle   Bundle where we save the on screen view state.  If null,
183     *                 a new Bundle is created and returned.
184     * @param view     The view to save.
185     * @param id       Id of the view.
186     */
187    public final Bundle saveOnScreenView(Bundle bundle, View view, int id) {
188        if (mSavePolicy != SAVE_NO_CHILD) {
189            String key = getSaveStatesKey(id);
190            SparseArray<Parcelable> container = new SparseArray<Parcelable>();
191            view.saveHierarchyState(container);
192            if (bundle == null) {
193                bundle = new Bundle();
194            }
195            bundle.putSparseParcelableArray(key, container);
196        }
197        return bundle;
198    }
199
200    /**
201     * Save off screen views according to policy.
202     *
203     * @param view view to save
204     * @param id unique id for the view within this ViewsStateBundle
205     */
206    public final void saveOffscreenView(View view, int id) {
207        switch (mSavePolicy) {
208            case SAVE_LIMITED_CHILD:
209            case SAVE_ALL_CHILD:
210                saveViewUnchecked(view, id);
211                break;
212            case SAVE_ON_SCREEN_CHILD:
213                remove(id);
214                break;
215            default:
216                break;
217        }
218    }
219
220    static String getSaveStatesKey(int id) {
221        return Integer.toString(id);
222    }
223}
224