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