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            SparseArray<Parcelable> container = mChildStates.get(key);
156            if (container != null) {
157                view.restoreHierarchyState(container);
158            }
159        }
160    }
161
162    /**
163     * Save views regardless what's the current policy is.
164     *
165     * @param view view to save
166     * @param id unique id for the view within this ViewsStateBundle
167     */
168    protected final void saveViewUnchecked(View view, int id) {
169        if (mChildStates != null) {
170            String key = getSaveStatesKey(id);
171            SparseArray<Parcelable> container = new SparseArray<Parcelable>();
172            view.saveHierarchyState(container);
173            mChildStates.put(key, container);
174        }
175    }
176
177    /**
178     * The on screen view is saved when policy is not {@link #SAVE_NO_CHILD}.
179     *
180     * @param view
181     * @param id
182     */
183    public final void saveOnScreenView(View view, int id) {
184        if (mSavePolicy != SAVE_NO_CHILD) {
185            saveViewUnchecked(view, id);
186        }
187    }
188
189    /**
190     * Save off screen views according to policy.
191     *
192     * @param view view to save
193     * @param id unique id for the view within this ViewsStateBundle
194     */
195    public final void saveOffscreenView(View view, int id) {
196        switch (mSavePolicy) {
197            case SAVE_LIMITED_CHILD:
198            case SAVE_ALL_CHILD:
199                saveViewUnchecked(view, id);
200                break;
201            case SAVE_ON_SCREEN_CHILD:
202                remove(id);
203                break;
204            default:
205                break;
206        }
207    }
208
209    static String getSaveStatesKey(int id) {
210        return Integer.toString(id);
211    }
212}
213