• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.preference;
18 
19 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
20 
21 import android.content.Context;
22 import android.content.res.TypedArray;
23 import android.os.Bundle;
24 import android.os.Handler;
25 import android.os.Parcel;
26 import android.os.Parcelable;
27 import android.text.TextUtils;
28 import android.util.AttributeSet;
29 import android.util.Log;
30 
31 import androidx.annotation.RestrictTo;
32 import androidx.collection.SimpleArrayMap;
33 import androidx.core.content.res.TypedArrayUtils;
34 import androidx.recyclerview.widget.RecyclerView;
35 
36 import java.util.ArrayList;
37 import java.util.Collections;
38 import java.util.List;
39 
40 /**
41  * A container for multiple
42  * {@link Preference} objects. It is a base class for  Preference objects that are
43  * parents, such as {@link PreferenceCategory} and {@link PreferenceScreen}.
44  *
45  * <div class="special reference">
46  * <h3>Developer Guides</h3>
47  * <p>For information about building a settings UI with Preferences,
48  * read the <a href="{@docRoot}guide/topics/ui/settings.html">Settings</a>
49  * guide.</p>
50  * </div>
51  *
52  * @attr name android:orderingFromXml
53  * @attr name initialExpandedChildrenCount
54  */
55 public abstract class PreferenceGroup extends Preference {
56     private static final String TAG = "PreferenceGroup";
57 
58     /**
59      * The container for child {@link Preference}s. This is sorted based on the
60      * ordering, please use {@link #addPreference(Preference)} instead of adding
61      * to this directly.
62      */
63     private List<Preference> mPreferenceList;
64 
65     private boolean mOrderingAsAdded = true;
66 
67     private int mCurrentPreferenceOrder = 0;
68 
69     private boolean mAttachedToHierarchy = false;
70 
71     private int mInitialExpandedChildrenCount = Integer.MAX_VALUE;
72 
73     private final SimpleArrayMap<String, Long> mIdRecycleCache = new SimpleArrayMap<>();
74     private final Handler mHandler = new Handler();
75     private final Runnable mClearRecycleCacheRunnable = new Runnable() {
76         @Override
77         public void run() {
78             synchronized (this) {
79                 mIdRecycleCache.clear();
80             }
81         }
82     };
83 
PreferenceGroup(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)84     public PreferenceGroup(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
85         super(context, attrs, defStyleAttr, defStyleRes);
86 
87         mPreferenceList = new ArrayList<>();
88 
89         final TypedArray a = context.obtainStyledAttributes(
90                 attrs, R.styleable.PreferenceGroup, defStyleAttr, defStyleRes);
91 
92         mOrderingAsAdded =
93                 TypedArrayUtils.getBoolean(a, R.styleable.PreferenceGroup_orderingFromXml,
94                         R.styleable.PreferenceGroup_orderingFromXml, true);
95 
96         if (a.hasValue(R.styleable.PreferenceGroup_initialExpandedChildrenCount)) {
97             setInitialExpandedChildrenCount((TypedArrayUtils.getInt(
98                     a, R.styleable.PreferenceGroup_initialExpandedChildrenCount,
99                     R.styleable.PreferenceGroup_initialExpandedChildrenCount, Integer.MAX_VALUE)));
100         }
101         a.recycle();
102     }
103 
PreferenceGroup(Context context, AttributeSet attrs, int defStyleAttr)104     public PreferenceGroup(Context context, AttributeSet attrs, int defStyleAttr) {
105         this(context, attrs, defStyleAttr, 0);
106     }
107 
PreferenceGroup(Context context, AttributeSet attrs)108     public PreferenceGroup(Context context, AttributeSet attrs) {
109         this(context, attrs, 0);
110     }
111 
112     /**
113      * Whether to order the {@link Preference} children of this group as they
114      * are added. If this is false, the ordering will follow each Preference
115      * order and default to alphabetic for those without an order.
116      * <p>
117      * If this is called after preferences are added, they will not be
118      * re-ordered in the order they were added, hence call this method early on.
119      *
120      * @param orderingAsAdded Whether to order according to the order added.
121      * @see Preference#setOrder(int)
122      */
setOrderingAsAdded(boolean orderingAsAdded)123     public void setOrderingAsAdded(boolean orderingAsAdded) {
124         mOrderingAsAdded = orderingAsAdded;
125     }
126 
127     /**
128      * Whether this group is ordering preferences in the order they are added.
129      *
130      * @return Whether this group orders based on the order the children are added.
131      * @see #setOrderingAsAdded(boolean)
132      */
isOrderingAsAdded()133     public boolean isOrderingAsAdded() {
134         return mOrderingAsAdded;
135     }
136 
137     /**
138      * Sets the maximal number of children that are shown when the preference group is launched
139      * where the rest of the children will be hidden.
140      * If some children are hidden an expand button will be provided to show all the hidden
141      * children. Any child in any level of the hierarchy that is also a preference group (e.g.
142      * preference category) will not be counted towards the limit. But instead the children of such
143      * group will be counted.
144      * By default, all children will be shown, so the default value of this attribute is equal to
145      * Integer.MAX_VALUE.
146      * <p>
147      * Note: The group should have a key defined if an expandable preference is present to
148      * correctly persist state.
149      *
150      * @param expandedCount the number of children that is initially shown.
151      *
152      * @attr ref R.styleable#PreferenceGroup_initialExpandedChildrenCount
153      */
setInitialExpandedChildrenCount(int expandedCount)154     public void setInitialExpandedChildrenCount(int expandedCount) {
155         if (expandedCount != Integer.MAX_VALUE && !hasKey()) {
156             Log.e(TAG, this.getClass().getSimpleName()
157                     + " should have a key defined if it contains an expandable preference");
158         }
159         mInitialExpandedChildrenCount = expandedCount;
160     }
161 
162     /**
163      * Gets the maximal number of children that are initially shown.
164      *
165      * @return the maximal number of children that are initially shown.
166      *
167      * @attr ref R.styleable#PreferenceGroup_initialExpandedChildrenCount
168      */
getInitialExpandedChildrenCount()169     public int getInitialExpandedChildrenCount() {
170         return mInitialExpandedChildrenCount;
171     }
172 
173     /**
174      * Called by the inflater to add an item to this group.
175      */
addItemFromInflater(Preference preference)176     public void addItemFromInflater(Preference preference) {
177         addPreference(preference);
178     }
179 
180     /**
181      * Returns the number of children {@link Preference}s.
182      * @return The number of preference children in this group.
183      */
getPreferenceCount()184     public int getPreferenceCount() {
185         return mPreferenceList.size();
186     }
187 
188     /**
189      * Returns the {@link Preference} at a particular index.
190      *
191      * @param index The index of the {@link Preference} to retrieve.
192      * @return The {@link Preference}.
193      */
getPreference(int index)194     public Preference getPreference(int index) {
195         return mPreferenceList.get(index);
196     }
197 
198     /**
199      * Adds a {@link Preference} at the correct position based on the
200      * preference's order.
201      *
202      * @param preference The preference to add.
203      * @return Whether the preference is now in this group.
204      */
addPreference(Preference preference)205     public boolean addPreference(Preference preference) {
206         if (mPreferenceList.contains(preference)) {
207             return true;
208         }
209         if (preference.getKey() != null) {
210             PreferenceGroup root = this;
211             while (root.getParent() != null) {
212                 root = root.getParent();
213             }
214             final String key = preference.getKey();
215             if (root.findPreference(key) != null) {
216                 Log.e(TAG, "Found duplicated key: \"" + key
217                         + "\". This can cause unintended behaviour,"
218                         + " please use unique keys for every preference.");
219             }
220         }
221 
222         if (preference.getOrder() == DEFAULT_ORDER) {
223             if (mOrderingAsAdded) {
224                 preference.setOrder(mCurrentPreferenceOrder++);
225             }
226 
227             if (preference instanceof PreferenceGroup) {
228                 // TODO: fix (method is called tail recursively when inflating,
229                 // so we won't end up properly passing this flag down to children
230                 ((PreferenceGroup)preference).setOrderingAsAdded(mOrderingAsAdded);
231             }
232         }
233 
234         int insertionIndex = Collections.binarySearch(mPreferenceList, preference);
235         if (insertionIndex < 0) {
236             insertionIndex = insertionIndex * -1 - 1;
237         }
238 
239         if (!onPrepareAddPreference(preference)) {
240             return false;
241         }
242 
243         synchronized(this) {
244             mPreferenceList.add(insertionIndex, preference);
245         }
246 
247         final PreferenceManager preferenceManager = getPreferenceManager();
248         final String key = preference.getKey();
249         final long id;
250         if (key != null && mIdRecycleCache.containsKey(key)) {
251             id = mIdRecycleCache.get(key);
252             mIdRecycleCache.remove(key);
253         } else {
254             id = preferenceManager.getNextId();
255         }
256         preference.onAttachedToHierarchy(preferenceManager, id);
257         preference.assignParent(this);
258 
259         if (mAttachedToHierarchy) {
260             preference.onAttached();
261         }
262 
263         notifyHierarchyChanged();
264 
265         return true;
266     }
267 
268     /**
269      * Removes a {@link Preference} from this group.
270      *
271      * @param preference The preference to remove.
272      * @return Whether the preference was found and removed.
273      */
removePreference(Preference preference)274     public boolean removePreference(Preference preference) {
275         final boolean returnValue = removePreferenceInt(preference);
276         notifyHierarchyChanged();
277         return returnValue;
278     }
279 
removePreferenceInt(Preference preference)280     private boolean removePreferenceInt(Preference preference) {
281         synchronized(this) {
282             preference.onPrepareForRemoval();
283             if (preference.getParent() == this) {
284                 preference.assignParent(null);
285             }
286             boolean success = mPreferenceList.remove(preference);
287             if (success) {
288                 // If this preference, or another preference with the same key, gets re-added
289                 // immediately, we want it to have the same id so that it can be correctly tracked
290                 // in the adapter by RecyclerView, to make it appear as if it has only been
291                 // seamlessly updated. If the preference is not re-added by the time the handler
292                 // runs, we take that as a signal that the preference will not be re-added soon
293                 // in which case it does not need to retain the same id.
294 
295                 // If two (or more) preferences have the same (or null) key and both are removed
296                 // and then re-added, only one id will be recycled and the second (and later)
297                 // preferences will receive a newly generated id. This use pattern of the preference
298                 // API is strongly discouraged.
299                 final String key = preference.getKey();
300                 if (key != null) {
301                     mIdRecycleCache.put(key, preference.getId());
302                     mHandler.removeCallbacks(mClearRecycleCacheRunnable);
303                     mHandler.post(mClearRecycleCacheRunnable);
304                 }
305                 if (mAttachedToHierarchy) {
306                     preference.onDetached();
307                 }
308             }
309 
310             return success;
311         }
312     }
313 
314     /**
315      * Removes all {@link Preference Preferences} from this group.
316      */
removeAll()317     public void removeAll() {
318         synchronized(this) {
319             List<Preference> preferenceList = mPreferenceList;
320             for (int i = preferenceList.size() - 1; i >= 0; i--) {
321                 removePreferenceInt(preferenceList.get(0));
322             }
323         }
324         notifyHierarchyChanged();
325     }
326 
327     /**
328      * Prepares a {@link Preference} to be added to the group.
329      *
330      * @param preference The preference to add.
331      * @return Whether to allow adding the preference (true), or not (false).
332      */
onPrepareAddPreference(Preference preference)333     protected boolean onPrepareAddPreference(Preference preference) {
334         preference.onParentChanged(this, shouldDisableDependents());
335         return true;
336     }
337 
338     /**
339      * Finds a {@link Preference} based on its key. If two {@link Preference}
340      * share the same key (not recommended), the first to appear will be
341      * returned (to retrieve the other preference with the same key, call this
342      * method on the first preference). If this preference has the key, it will
343      * not be returned.
344      * <p>
345      * This will recursively search for the preference into children that are
346      * also {@link PreferenceGroup PreferenceGroups}.
347      *
348      * @param key The key of the preference to retrieve.
349      * @return The {@link Preference} with the key, or null.
350      */
findPreference(CharSequence key)351     public Preference findPreference(CharSequence key) {
352         if (TextUtils.equals(getKey(), key)) {
353             return this;
354         }
355         final int preferenceCount = getPreferenceCount();
356         for (int i = 0; i < preferenceCount; i++) {
357             final Preference preference = getPreference(i);
358             final String curKey = preference.getKey();
359 
360             if (curKey != null && curKey.equals(key)) {
361                 return preference;
362             }
363 
364             if (preference instanceof PreferenceGroup) {
365                 final Preference returnedPreference = ((PreferenceGroup)preference)
366                         .findPreference(key);
367                 if (returnedPreference != null) {
368                     return returnedPreference;
369                 }
370             }
371         }
372 
373         return null;
374     }
375 
376     /**
377      * Whether this preference group should be shown on the same screen as its
378      * contained preferences.
379      *
380      * @return True if the contained preferences should be shown on the same
381      *         screen as this preference.
382      */
isOnSameScreenAsChildren()383     protected boolean isOnSameScreenAsChildren() {
384         return true;
385     }
386 
387     /**
388      * Returns true if we're between {@link #onAttached()} and {@link #onPrepareForRemoval()}
389      * @hide
390      */
391     @RestrictTo(LIBRARY_GROUP)
isAttached()392     public boolean isAttached() {
393         return mAttachedToHierarchy;
394     }
395 
396     @Override
onAttached()397     public void onAttached() {
398         super.onAttached();
399 
400         // Mark as attached so if a preference is later added to this group, we
401         // can tell it we are already attached
402         mAttachedToHierarchy = true;
403 
404         // Dispatch to all contained preferences
405         final int preferenceCount = getPreferenceCount();
406         for (int i = 0; i < preferenceCount; i++) {
407             getPreference(i).onAttached();
408         }
409     }
410 
411     @Override
onDetached()412     public void onDetached() {
413         super.onDetached();
414 
415         // We won't be attached to the activity anymore
416         mAttachedToHierarchy = false;
417 
418         // Dispatch to all contained preferences
419         final int preferenceCount = getPreferenceCount();
420         for (int i = 0; i < preferenceCount; i++) {
421             getPreference(i).onDetached();
422         }
423     }
424 
425     @Override
notifyDependencyChange(boolean disableDependents)426     public void notifyDependencyChange(boolean disableDependents) {
427         super.notifyDependencyChange(disableDependents);
428 
429         // Child preferences have an implicit dependency on their containing
430         // group. Dispatch dependency change to all contained preferences.
431         final int preferenceCount = getPreferenceCount();
432         for (int i = 0; i < preferenceCount; i++) {
433             getPreference(i).onParentChanged(this, disableDependents);
434         }
435     }
436 
sortPreferences()437     void sortPreferences() {
438         synchronized (this) {
439             Collections.sort(mPreferenceList);
440         }
441     }
442 
443     @Override
dispatchSaveInstanceState(Bundle container)444     protected void dispatchSaveInstanceState(Bundle container) {
445         super.dispatchSaveInstanceState(container);
446 
447         // Dispatch to all contained preferences
448         final int preferenceCount = getPreferenceCount();
449         for (int i = 0; i < preferenceCount; i++) {
450             getPreference(i).dispatchSaveInstanceState(container);
451         }
452     }
453 
454     @Override
dispatchRestoreInstanceState(Bundle container)455     protected void dispatchRestoreInstanceState(Bundle container) {
456         super.dispatchRestoreInstanceState(container);
457 
458         // Dispatch to all contained preferences
459         final int preferenceCount = getPreferenceCount();
460         for (int i = 0; i < preferenceCount; i++) {
461             getPreference(i).dispatchRestoreInstanceState(container);
462         }
463     }
464 
465     @Override
onSaveInstanceState()466     protected Parcelable onSaveInstanceState() {
467         final Parcelable superState = super.onSaveInstanceState();
468         return new SavedState(superState, mInitialExpandedChildrenCount);
469     }
470 
471     @Override
onRestoreInstanceState(Parcelable state)472     protected void onRestoreInstanceState(Parcelable state) {
473         if (state == null || !state.getClass().equals(SavedState.class)) {
474             // Didn't save state for us in saveInstanceState
475             super.onRestoreInstanceState(state);
476             return;
477         }
478         SavedState groupState = (SavedState) state;
479         if (mInitialExpandedChildrenCount != groupState.mInitialExpandedChildrenCount) {
480             mInitialExpandedChildrenCount = groupState.mInitialExpandedChildrenCount;
481             notifyHierarchyChanged();
482         }
483         super.onRestoreInstanceState(groupState.getSuperState());
484     }
485 
486     /**
487      * Interface for PreferenceGroup Adapters to implement so that
488      * {@link androidx.preference.PreferenceFragment#scrollToPreference(String)} and
489      * {@link androidx.preference.PreferenceFragment#scrollToPreference(Preference)} or
490      * {@link PreferenceFragmentCompat#scrollToPreference(String)} and
491      * {@link PreferenceFragmentCompat#scrollToPreference(Preference)}
492      * can determine the correct scroll position to request.
493      */
494     public interface PreferencePositionCallback {
495 
496         /**
497          * Return the adapter position of the first {@link Preference} with the specified key
498          * @param key Key of {@link Preference} to find
499          * @return Adapter position of the {@link Preference} or
500          *         {@link RecyclerView#NO_POSITION} if not found
501          */
getPreferenceAdapterPosition(String key)502         int getPreferenceAdapterPosition(String key);
503 
504         /**
505          * Return the adapter position of the specified {@link Preference} object
506          * @param preference {@link Preference} object to find
507          * @return Adapter position of the {@link Preference} or
508          *         {@link RecyclerView#NO_POSITION} if not found
509          */
getPreferenceAdapterPosition(Preference preference)510         int getPreferenceAdapterPosition(Preference preference);
511     }
512 
513     /**
514      * A class for managing the instance state of a {@link PreferenceGroup}.
515      */
516     static class SavedState extends Preference.BaseSavedState {
517 
518         int mInitialExpandedChildrenCount;
519 
SavedState(Parcel source)520         SavedState(Parcel source) {
521             super(source);
522             mInitialExpandedChildrenCount = source.readInt();
523         }
524 
SavedState(Parcelable superState, int initialExpandedChildrenCount)525         SavedState(Parcelable superState, int initialExpandedChildrenCount) {
526             super(superState);
527             mInitialExpandedChildrenCount = initialExpandedChildrenCount;
528         }
529 
530         @Override
writeToParcel(Parcel dest, int flags)531         public void writeToParcel(Parcel dest, int flags) {
532             super.writeToParcel(dest, flags);
533             dest.writeInt(mInitialExpandedChildrenCount);
534         }
535 
536         public static final Parcelable.Creator<SavedState> CREATOR =
537                 new Parcelable.Creator<SavedState>() {
538                     @Override
539                     public SavedState createFromParcel(Parcel in) {
540                         return new SavedState(in);
541                     }
542 
543                     @Override
544                     public SavedState[] newArray(int size) {
545                         return new SavedState[size];
546                     }
547                 };
548     }
549 }
550