1/*
2 * Copyright (C) 2015 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
17package android.support.v7.preference;
18
19import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
20
21import android.content.Context;
22import android.content.res.TypedArray;
23import android.os.Bundle;
24import android.os.Handler;
25import android.support.annotation.RestrictTo;
26import android.support.v4.content.res.TypedArrayUtils;
27import android.support.v4.util.SimpleArrayMap;
28import android.text.TextUtils;
29import android.util.AttributeSet;
30
31import java.util.ArrayList;
32import java.util.Collections;
33import java.util.List;
34
35/**
36 * A container for multiple
37 * {@link Preference} objects. It is a base class for  Preference objects that are
38 * parents, such as {@link PreferenceCategory} and {@link PreferenceScreen}.
39 *
40 * <div class="special reference">
41 * <h3>Developer Guides</h3>
42 * <p>For information about building a settings UI with Preferences,
43 * read the <a href="{@docRoot}guide/topics/ui/settings.html">Settings</a>
44 * guide.</p>
45 * </div>
46 *
47 * @attr name android:orderingFromXml
48 */
49public abstract class PreferenceGroup extends Preference {
50    /**
51     * The container for child {@link Preference}s. This is sorted based on the
52     * ordering, please use {@link #addPreference(Preference)} instead of adding
53     * to this directly.
54     */
55    private List<Preference> mPreferenceList;
56
57    private boolean mOrderingAsAdded = true;
58
59    private int mCurrentPreferenceOrder = 0;
60
61    private boolean mAttachedToHierarchy = false;
62
63    private final SimpleArrayMap<String, Long> mIdRecycleCache = new SimpleArrayMap<>();
64    private final Handler mHandler = new Handler();
65    private final Runnable mClearRecycleCacheRunnable = new Runnable() {
66        @Override
67        public void run() {
68            synchronized (this) {
69                mIdRecycleCache.clear();
70            }
71        }
72    };
73
74    public PreferenceGroup(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
75        super(context, attrs, defStyleAttr, defStyleRes);
76
77        mPreferenceList = new ArrayList<>();
78
79        final TypedArray a = context.obtainStyledAttributes(
80                attrs, R.styleable.PreferenceGroup, defStyleAttr, defStyleRes);
81
82        mOrderingAsAdded =
83                TypedArrayUtils.getBoolean(a, R.styleable.PreferenceGroup_orderingFromXml,
84                        R.styleable.PreferenceGroup_orderingFromXml, true);
85
86        a.recycle();
87    }
88
89    public PreferenceGroup(Context context, AttributeSet attrs, int defStyleAttr) {
90        this(context, attrs, defStyleAttr, 0);
91    }
92
93    public PreferenceGroup(Context context, AttributeSet attrs) {
94        this(context, attrs, 0);
95    }
96
97    /**
98     * Whether to order the {@link Preference} children of this group as they
99     * are added. If this is false, the ordering will follow each Preference
100     * order and default to alphabetic for those without an order.
101     * <p>
102     * If this is called after preferences are added, they will not be
103     * re-ordered in the order they were added, hence call this method early on.
104     *
105     * @param orderingAsAdded Whether to order according to the order added.
106     * @see Preference#setOrder(int)
107     */
108    public void setOrderingAsAdded(boolean orderingAsAdded) {
109        mOrderingAsAdded = orderingAsAdded;
110    }
111
112    /**
113     * Whether this group is ordering preferences in the order they are added.
114     *
115     * @return Whether this group orders based on the order the children are added.
116     * @see #setOrderingAsAdded(boolean)
117     */
118    public boolean isOrderingAsAdded() {
119        return mOrderingAsAdded;
120    }
121
122    /**
123     * Called by the inflater to add an item to this group.
124     */
125    public void addItemFromInflater(Preference preference) {
126        addPreference(preference);
127    }
128
129    /**
130     * Returns the number of children {@link Preference}s.
131     * @return The number of preference children in this group.
132     */
133    public int getPreferenceCount() {
134        return mPreferenceList.size();
135    }
136
137    /**
138     * Returns the {@link Preference} at a particular index.
139     *
140     * @param index The index of the {@link Preference} to retrieve.
141     * @return The {@link Preference}.
142     */
143    public Preference getPreference(int index) {
144        return mPreferenceList.get(index);
145    }
146
147    /**
148     * Adds a {@link Preference} at the correct position based on the
149     * preference's order.
150     *
151     * @param preference The preference to add.
152     * @return Whether the preference is now in this group.
153     */
154    public boolean addPreference(Preference preference) {
155        if (mPreferenceList.contains(preference)) {
156            // Exists
157            return true;
158        }
159
160        if (preference.getOrder() == DEFAULT_ORDER) {
161            if (mOrderingAsAdded) {
162                preference.setOrder(mCurrentPreferenceOrder++);
163            }
164
165            if (preference instanceof PreferenceGroup) {
166                // TODO: fix (method is called tail recursively when inflating,
167                // so we won't end up properly passing this flag down to children
168                ((PreferenceGroup)preference).setOrderingAsAdded(mOrderingAsAdded);
169            }
170        }
171
172        int insertionIndex = Collections.binarySearch(mPreferenceList, preference);
173        if (insertionIndex < 0) {
174            insertionIndex = insertionIndex * -1 - 1;
175        }
176
177        if (!onPrepareAddPreference(preference)) {
178            return false;
179        }
180
181        synchronized(this) {
182            mPreferenceList.add(insertionIndex, preference);
183        }
184
185        final PreferenceManager preferenceManager = getPreferenceManager();
186        final String key = preference.getKey();
187        final long id;
188        if (key != null && mIdRecycleCache.containsKey(key)) {
189            id = mIdRecycleCache.get(key);
190            mIdRecycleCache.remove(key);
191        } else {
192            id = preferenceManager.getNextId();
193        }
194        preference.onAttachedToHierarchy(preferenceManager, id);
195        preference.assignParent(this);
196
197        if (mAttachedToHierarchy) {
198            preference.onAttached();
199        }
200
201        notifyHierarchyChanged();
202
203        return true;
204    }
205
206    /**
207     * Removes a {@link Preference} from this group.
208     *
209     * @param preference The preference to remove.
210     * @return Whether the preference was found and removed.
211     */
212    public boolean removePreference(Preference preference) {
213        final boolean returnValue = removePreferenceInt(preference);
214        notifyHierarchyChanged();
215        return returnValue;
216    }
217
218    private boolean removePreferenceInt(Preference preference) {
219        synchronized(this) {
220            preference.onPrepareForRemoval();
221            if (preference.getParent() == this) {
222                preference.assignParent(null);
223            }
224            boolean success = mPreferenceList.remove(preference);
225            if (success) {
226                // If this preference, or another preference with the same key, gets re-added
227                // immediately, we want it to have the same id so that it can be correctly tracked
228                // in the adapter by RecyclerView, to make it appear as if it has only been
229                // seamlessly updated. If the preference is not re-added by the time the handler
230                // runs, we take that as a signal that the preference will not be re-added soon
231                // in which case it does not need to retain the same id.
232
233                // If two (or more) preferences have the same (or null) key and both are removed
234                // and then re-added, only one id will be recycled and the second (and later)
235                // preferences will receive a newly generated id. This use pattern of the preference
236                // API is strongly discouraged.
237                final String key = preference.getKey();
238                if (key != null) {
239                    mIdRecycleCache.put(key, preference.getId());
240                    mHandler.removeCallbacks(mClearRecycleCacheRunnable);
241                    mHandler.post(mClearRecycleCacheRunnable);
242                }
243                if (mAttachedToHierarchy) {
244                    preference.onDetached();
245                }
246            }
247
248            return success;
249        }
250    }
251
252    /**
253     * Removes all {@link Preference Preferences} from this group.
254     */
255    public void removeAll() {
256        synchronized(this) {
257            List<Preference> preferenceList = mPreferenceList;
258            for (int i = preferenceList.size() - 1; i >= 0; i--) {
259                removePreferenceInt(preferenceList.get(0));
260            }
261        }
262        notifyHierarchyChanged();
263    }
264
265    /**
266     * Prepares a {@link Preference} to be added to the group.
267     *
268     * @param preference The preference to add.
269     * @return Whether to allow adding the preference (true), or not (false).
270     */
271    protected boolean onPrepareAddPreference(Preference preference) {
272        preference.onParentChanged(this, shouldDisableDependents());
273        return true;
274    }
275
276    /**
277     * Finds a {@link Preference} based on its key. If two {@link Preference}
278     * share the same key (not recommended), the first to appear will be
279     * returned (to retrieve the other preference with the same key, call this
280     * method on the first preference). If this preference has the key, it will
281     * not be returned.
282     * <p>
283     * This will recursively search for the preference into children that are
284     * also {@link PreferenceGroup PreferenceGroups}.
285     *
286     * @param key The key of the preference to retrieve.
287     * @return The {@link Preference} with the key, or null.
288     */
289    public Preference findPreference(CharSequence key) {
290        if (TextUtils.equals(getKey(), key)) {
291            return this;
292        }
293        final int preferenceCount = getPreferenceCount();
294        for (int i = 0; i < preferenceCount; i++) {
295            final Preference preference = getPreference(i);
296            final String curKey = preference.getKey();
297
298            if (curKey != null && curKey.equals(key)) {
299                return preference;
300            }
301
302            if (preference instanceof PreferenceGroup) {
303                final Preference returnedPreference = ((PreferenceGroup)preference)
304                        .findPreference(key);
305                if (returnedPreference != null) {
306                    return returnedPreference;
307                }
308            }
309        }
310
311        return null;
312    }
313
314    /**
315     * Whether this preference group should be shown on the same screen as its
316     * contained preferences.
317     *
318     * @return True if the contained preferences should be shown on the same
319     *         screen as this preference.
320     */
321    protected boolean isOnSameScreenAsChildren() {
322        return true;
323    }
324
325    /**
326     * Returns true if we're between {@link #onAttached()} and {@link #onPrepareForRemoval()}
327     * @hide
328     */
329    @RestrictTo(LIBRARY_GROUP)
330    public boolean isAttached() {
331        return mAttachedToHierarchy;
332    }
333
334    @Override
335    public void onAttached() {
336        super.onAttached();
337
338        // Mark as attached so if a preference is later added to this group, we
339        // can tell it we are already attached
340        mAttachedToHierarchy = true;
341
342        // Dispatch to all contained preferences
343        final int preferenceCount = getPreferenceCount();
344        for (int i = 0; i < preferenceCount; i++) {
345            getPreference(i).onAttached();
346        }
347    }
348
349    @Override
350    public void onDetached() {
351        super.onDetached();
352
353        // We won't be attached to the activity anymore
354        mAttachedToHierarchy = false;
355
356        // Dispatch to all contained preferences
357        final int preferenceCount = getPreferenceCount();
358        for (int i = 0; i < preferenceCount; i++) {
359            getPreference(i).onDetached();
360        }
361    }
362
363    @Override
364    public void notifyDependencyChange(boolean disableDependents) {
365        super.notifyDependencyChange(disableDependents);
366
367        // Child preferences have an implicit dependency on their containing
368        // group. Dispatch dependency change to all contained preferences.
369        final int preferenceCount = getPreferenceCount();
370        for (int i = 0; i < preferenceCount; i++) {
371            getPreference(i).onParentChanged(this, disableDependents);
372        }
373    }
374
375    void sortPreferences() {
376        synchronized (this) {
377            Collections.sort(mPreferenceList);
378        }
379    }
380
381    @Override
382    protected void dispatchSaveInstanceState(Bundle container) {
383        super.dispatchSaveInstanceState(container);
384
385        // Dispatch to all contained preferences
386        final int preferenceCount = getPreferenceCount();
387        for (int i = 0; i < preferenceCount; i++) {
388            getPreference(i).dispatchSaveInstanceState(container);
389        }
390    }
391
392    @Override
393    protected void dispatchRestoreInstanceState(Bundle container) {
394        super.dispatchRestoreInstanceState(container);
395
396        // Dispatch to all contained preferences
397        final int preferenceCount = getPreferenceCount();
398        for (int i = 0; i < preferenceCount; i++) {
399            getPreference(i).dispatchRestoreInstanceState(container);
400        }
401    }
402
403    /**
404     * Interface for PreferenceGroup Adapters to implement so that
405     * {@link android.support.v14.preference.PreferenceFragment#scrollToPreference(String)} and
406     * {@link android.support.v14.preference.PreferenceFragment#scrollToPreference(Preference)} or
407     * {@link PreferenceFragmentCompat#scrollToPreference(String)} and
408     * {@link PreferenceFragmentCompat#scrollToPreference(Preference)}
409     * can determine the correct scroll position to request.
410     */
411    public interface PreferencePositionCallback {
412
413        /**
414         * Return the adapter position of the first {@link Preference} with the specified key
415         * @param key Key of {@link Preference} to find
416         * @return Adapter position of the {@link Preference} or
417         *         {@link android.support.v7.widget.RecyclerView#NO_POSITION} if not found
418         */
419        int getPreferenceAdapterPosition(String key);
420
421        /**
422         * Return the adapter position of the specified {@link Preference} object
423         * @param preference {@link Preference} object to find
424         * @return Adapter position of the {@link Preference} or
425         *         {@link android.support.v7.widget.RecyclerView#NO_POSITION} if not found
426         */
427        int getPreferenceAdapterPosition(Preference preference);
428    }
429}
430