1 /*
2  * Copyright 2018 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 
17 package androidx.recyclerview.selection;
18 
19 import static androidx.core.util.Preconditions.checkArgument;
20 
21 import android.os.Bundle;
22 import android.os.Parcelable;
23 
24 import androidx.annotation.VisibleForTesting;
25 
26 import org.jspecify.annotations.NonNull;
27 import org.jspecify.annotations.Nullable;
28 
29 import java.util.ArrayList;
30 
31 /**
32  * Strategy for storing keys in saved state. Extend this class when using custom
33  * key types that aren't supported by default. Prefer use of builtin storage strategies:
34  * {@link #createStringStorage()}, {@link #createLongStorage()},
35  * {@link #createParcelableStorage(Class)}.
36  *
37  * <p>
38  * See
39  * {@link androidx.recyclerview.selection.SelectionTracker.Builder SelectionTracker.Builder}
40  * for more detailed advice on which key type to use for your selection keys.
41  *
42  * @param <K> Selection key type. Built in support is provided for String, Long, and Parcelable
43  *            types. Use the respective factory method to create a StorageStrategy instance
44  *            appropriate to the desired type.
45  *            {@link #createStringStorage()},
46  *            {@link #createParcelableStorage(Class)},
47  *            {@link #createLongStorage()}
48  */
49 public abstract class StorageStrategy<K> {
50 
51     @VisibleForTesting
52     static final String SELECTION_ENTRIES = "androidx.recyclerview.selection.entries";
53 
54     @VisibleForTesting
55     static final String SELECTION_KEY_TYPE = "androidx.recyclerview.selection.type";
56 
57     private final Class<K> mType;
58 
59     /**
60      * Creates a new instance.
61      *
62      * @param type the key type class that is being used.
63      */
StorageStrategy(@onNull Class<K> type)64     public StorageStrategy(@NonNull Class<K> type) {
65         checkArgument(type != null);
66         mType = type;
67     }
68 
69     /**
70      * Create a {@link Selection} from supplied {@link Bundle}.
71      *
72      * @param state Bundle instance that may contain parceled Selection instance.
73      */
asSelection(@onNull Bundle state)74     public abstract @Nullable Selection<K> asSelection(@NonNull Bundle state);
75 
76     /**
77      * Creates a {@link Bundle} from supplied {@link Selection}.
78      *
79      * @param selection The selection to asBundle.
80      */
asBundle(@onNull Selection<K> selection)81     public abstract @NonNull Bundle asBundle(@NonNull Selection<K> selection);
82 
getKeyTypeName()83     String getKeyTypeName() {
84         return mType.getCanonicalName();
85     }
86 
87     /**
88      * @return StorageStrategy suitable for use with {@link Parcelable} keys
89      * (like {@link android.net.Uri}).
90      */
createParcelableStorage( @onNull Class<K> type)91     public static <K extends Parcelable> @NonNull StorageStrategy<K> createParcelableStorage(
92             @NonNull Class<K> type) {
93         return new ParcelableStorageStrategy<>(type);
94     }
95 
96     /**
97      * @return StorageStrategy suitable for use with {@link String} keys.
98      */
createStringStorage()99     public static @NonNull StorageStrategy<String> createStringStorage() {
100         return new StringStorageStrategy();
101     }
102 
103     /**
104      * @return StorageStrategy suitable for use with {@link Long} keys.
105      */
createLongStorage()106     public static @NonNull StorageStrategy<Long> createLongStorage() {
107         return new LongStorageStrategy();
108     }
109 
110     private static class StringStorageStrategy extends StorageStrategy<String> {
111 
StringStorageStrategy()112         StringStorageStrategy() {
113             super(String.class);
114         }
115 
116         @Override
asSelection(@onNull Bundle state)117         public @Nullable Selection<String> asSelection(@NonNull Bundle state) {
118 
119             String keyType = state.getString(SELECTION_KEY_TYPE, null);
120             if (keyType == null || !keyType.equals(getKeyTypeName())) {
121                 return null;
122             }
123 
124             ArrayList<String> stored = state.getStringArrayList(SELECTION_ENTRIES);
125             if (stored == null) {
126                 return null;
127             }
128 
129             Selection<String> selection = new Selection<>();
130             selection.mSelection.addAll(stored);
131             return selection;
132         }
133 
134         @Override
asBundle(@onNull Selection<String> selection)135         public @NonNull Bundle asBundle(@NonNull Selection<String> selection) {
136 
137             Bundle bundle = new Bundle();
138 
139             bundle.putString(SELECTION_KEY_TYPE, getKeyTypeName());
140 
141             ArrayList<String> value = new ArrayList<>(selection.size());
142             value.addAll(selection.mSelection);
143             bundle.putStringArrayList(SELECTION_ENTRIES, value);
144 
145             return bundle;
146         }
147     }
148 
149     private static class LongStorageStrategy extends StorageStrategy<Long> {
150 
LongStorageStrategy()151         LongStorageStrategy() {
152             super(Long.class);
153         }
154 
155         @Override
asSelection(@onNull Bundle state)156         public @Nullable Selection<Long> asSelection(@NonNull Bundle state) {
157             String keyType = state.getString(SELECTION_KEY_TYPE, null);
158             if (keyType == null || !keyType.equals(getKeyTypeName())) {
159                 return null;
160             }
161 
162             long[] stored = state.getLongArray(SELECTION_ENTRIES);
163             if (stored == null) {
164                 return null;
165             }
166 
167             Selection<Long> selection = new Selection<>();
168             for (long key : stored) {
169                 selection.mSelection.add(key);
170             }
171             return selection;
172         }
173 
174         @Override
asBundle(@onNull Selection<Long> selection)175         public @NonNull Bundle asBundle(@NonNull Selection<Long> selection) {
176 
177             Bundle bundle = new Bundle();
178             bundle.putString(SELECTION_KEY_TYPE, getKeyTypeName());
179 
180             long[] value = new long[selection.size()];
181             int i = 0;
182             for (Long key : selection) {
183                 value[i++] = key;
184             }
185             bundle.putLongArray(SELECTION_ENTRIES, value);
186 
187             return bundle;
188         }
189     }
190 
191     private static class ParcelableStorageStrategy<K extends Parcelable>
192             extends StorageStrategy<K> {
193 
ParcelableStorageStrategy(@onNull Class<K> type)194         ParcelableStorageStrategy(@NonNull Class<K> type) {
195             super(type);
196             checkArgument(Parcelable.class.isAssignableFrom(type));
197         }
198 
199         @Override
200         @SuppressWarnings("deprecation")
asSelection(@onNull Bundle state)201         public @Nullable Selection<K> asSelection(@NonNull Bundle state) {
202 
203             String keyType = state.getString(SELECTION_KEY_TYPE, null);
204             if (keyType == null || !keyType.equals(getKeyTypeName())) {
205                 return null;
206             }
207 
208             ArrayList<K> stored = state.getParcelableArrayList(SELECTION_ENTRIES);
209             if (stored == null) {
210                 return null;
211             }
212 
213             Selection<K> selection = new Selection<>();
214             selection.mSelection.addAll(stored);
215             return selection;
216         }
217 
218         @Override
asBundle(@onNull Selection<K> selection)219         public @NonNull Bundle asBundle(@NonNull Selection<K> selection) {
220 
221             Bundle bundle = new Bundle();
222             bundle.putString(SELECTION_KEY_TYPE, getKeyTypeName());
223 
224             ArrayList<K> value = new ArrayList<>(selection.size());
225             value.addAll(selection.mSelection);
226             bundle.putParcelableArrayList(SELECTION_ENTRIES, value);
227 
228             return bundle;
229         }
230     }
231 }
232