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 android.content.Context;
20import android.content.res.TypedArray;
21import android.os.Bundle;
22import android.support.v4.content.res.TypedArrayUtils;
23import android.text.TextUtils;
24import android.util.AttributeSet;
25
26import java.util.ArrayList;
27import java.util.Collections;
28import java.util.List;
29
30/**
31 * A container for multiple
32 * {@link Preference} objects. It is a base class for  Preference objects that are
33 * parents, such as {@link PreferenceCategory} and {@link PreferenceScreen}.
34 *
35 * <div class="special reference">
36 * <h3>Developer Guides</h3>
37 * <p>For information about building a settings UI with Preferences,
38 * read the <a href="{@docRoot}guide/topics/ui/settings.html">Settings</a>
39 * guide.</p>
40 * </div>
41 *
42 * @attr ref android.R.styleable#PreferenceGroup_orderingFromXml
43 */
44public abstract class PreferenceGroup extends Preference {
45    /**
46     * The container for child {@link Preference}s. This is sorted based on the
47     * ordering, please use {@link #addPreference(Preference)} instead of adding
48     * to this directly.
49     */
50    private List<Preference> mPreferenceList;
51
52    private boolean mOrderingAsAdded = true;
53
54    private int mCurrentPreferenceOrder = 0;
55
56    private boolean mAttachedToHierarchy = false;
57
58    public PreferenceGroup(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
59        super(context, attrs, defStyleAttr, defStyleRes);
60
61        mPreferenceList = new ArrayList<>();
62
63        final TypedArray a = context.obtainStyledAttributes(
64                attrs, R.styleable.PreferenceGroup, defStyleAttr, defStyleRes);
65
66        mOrderingAsAdded =
67                TypedArrayUtils.getBoolean(a, R.styleable.PreferenceGroup_orderingFromXml,
68                        R.styleable.PreferenceGroup_orderingFromXml, true);
69
70        a.recycle();
71    }
72
73    public PreferenceGroup(Context context, AttributeSet attrs, int defStyleAttr) {
74        this(context, attrs, defStyleAttr, 0);
75    }
76
77    public PreferenceGroup(Context context, AttributeSet attrs) {
78        this(context, attrs, 0);
79    }
80
81    /**
82     * Whether to order the {@link Preference} children of this group as they
83     * are added. If this is false, the ordering will follow each Preference
84     * order and default to alphabetic for those without an order.
85     * <p>
86     * If this is called after preferences are added, they will not be
87     * re-ordered in the order they were added, hence call this method early on.
88     *
89     * @param orderingAsAdded Whether to order according to the order added.
90     * @see Preference#setOrder(int)
91     */
92    public void setOrderingAsAdded(boolean orderingAsAdded) {
93        mOrderingAsAdded = orderingAsAdded;
94    }
95
96    /**
97     * Whether this group is ordering preferences in the order they are added.
98     *
99     * @return Whether this group orders based on the order the children are added.
100     * @see #setOrderingAsAdded(boolean)
101     */
102    public boolean isOrderingAsAdded() {
103        return mOrderingAsAdded;
104    }
105
106    /**
107     * Called by the inflater to add an item to this group.
108     */
109    public void addItemFromInflater(Preference preference) {
110        addPreference(preference);
111    }
112
113    /**
114     * Returns the number of children {@link Preference}s.
115     * @return The number of preference children in this group.
116     */
117    public int getPreferenceCount() {
118        return mPreferenceList.size();
119    }
120
121    /**
122     * Returns the {@link Preference} at a particular index.
123     *
124     * @param index The index of the {@link Preference} to retrieve.
125     * @return The {@link Preference}.
126     */
127    public Preference getPreference(int index) {
128        return mPreferenceList.get(index);
129    }
130
131    /**
132     * Adds a {@link Preference} at the correct position based on the
133     * preference's order.
134     *
135     * @param preference The preference to add.
136     * @return Whether the preference is now in this group.
137     */
138    public boolean addPreference(Preference preference) {
139        if (mPreferenceList.contains(preference)) {
140            // Exists
141            return true;
142        }
143
144        if (preference.getOrder() == DEFAULT_ORDER) {
145            if (mOrderingAsAdded) {
146                preference.setOrder(mCurrentPreferenceOrder++);
147            }
148
149            if (preference instanceof PreferenceGroup) {
150                // TODO: fix (method is called tail recursively when inflating,
151                // so we won't end up properly passing this flag down to children
152                ((PreferenceGroup)preference).setOrderingAsAdded(mOrderingAsAdded);
153            }
154        }
155
156        int insertionIndex = Collections.binarySearch(mPreferenceList, preference);
157        if (insertionIndex < 0) {
158            insertionIndex = insertionIndex * -1 - 1;
159        }
160
161        if (!onPrepareAddPreference(preference)) {
162            return false;
163        }
164
165        synchronized(this) {
166            mPreferenceList.add(insertionIndex, preference);
167        }
168
169        preference.onAttachedToHierarchy(getPreferenceManager());
170
171        if (mAttachedToHierarchy) {
172            preference.onAttached();
173        }
174
175        notifyHierarchyChanged();
176
177        return true;
178    }
179
180    /**
181     * Removes a {@link Preference} from this group.
182     *
183     * @param preference The preference to remove.
184     * @return Whether the preference was found and removed.
185     */
186    public boolean removePreference(Preference preference) {
187        final boolean returnValue = removePreferenceInt(preference);
188        notifyHierarchyChanged();
189        return returnValue;
190    }
191
192    private boolean removePreferenceInt(Preference preference) {
193        synchronized(this) {
194            preference.onPrepareForRemoval();
195            return mPreferenceList.remove(preference);
196        }
197    }
198
199    /**
200     * Removes all {@link Preference Preferences} from this group.
201     */
202    public void removeAll() {
203        synchronized(this) {
204            List<Preference> preferenceList = mPreferenceList;
205            for (int i = preferenceList.size() - 1; i >= 0; i--) {
206                removePreferenceInt(preferenceList.get(0));
207            }
208        }
209        notifyHierarchyChanged();
210    }
211
212    /**
213     * Prepares a {@link Preference} to be added to the group.
214     *
215     * @param preference The preference to add.
216     * @return Whether to allow adding the preference (true), or not (false).
217     */
218    protected boolean onPrepareAddPreference(Preference preference) {
219        preference.onParentChanged(this, shouldDisableDependents());
220        return true;
221    }
222
223    /**
224     * Finds a {@link Preference} based on its key. If two {@link Preference}
225     * share the same key (not recommended), the first to appear will be
226     * returned (to retrieve the other preference with the same key, call this
227     * method on the first preference). If this preference has the key, it will
228     * not be returned.
229     * <p>
230     * This will recursively search for the preference into children that are
231     * also {@link PreferenceGroup PreferenceGroups}.
232     *
233     * @param key The key of the preference to retrieve.
234     * @return The {@link Preference} with the key, or null.
235     */
236    public Preference findPreference(CharSequence key) {
237        if (TextUtils.equals(getKey(), key)) {
238            return this;
239        }
240        final int preferenceCount = getPreferenceCount();
241        for (int i = 0; i < preferenceCount; i++) {
242            final Preference preference = getPreference(i);
243            final String curKey = preference.getKey();
244
245            if (curKey != null && curKey.equals(key)) {
246                return preference;
247            }
248
249            if (preference instanceof PreferenceGroup) {
250                final Preference returnedPreference = ((PreferenceGroup)preference)
251                        .findPreference(key);
252                if (returnedPreference != null) {
253                    return returnedPreference;
254                }
255            }
256        }
257
258        return null;
259    }
260
261    /**
262     * Whether this preference group should be shown on the same screen as its
263     * contained preferences.
264     *
265     * @return True if the contained preferences should be shown on the same
266     *         screen as this preference.
267     */
268    protected boolean isOnSameScreenAsChildren() {
269        return true;
270    }
271
272    @Override
273    public void onAttached() {
274        super.onAttached();
275
276        // Mark as attached so if a preference is later added to this group, we
277        // can tell it we are already attached
278        mAttachedToHierarchy = true;
279
280        // Dispatch to all contained preferences
281        final int preferenceCount = getPreferenceCount();
282        for (int i = 0; i < preferenceCount; i++) {
283            getPreference(i).onAttached();
284        }
285    }
286
287    @Override
288    protected void onPrepareForRemoval() {
289        super.onPrepareForRemoval();
290
291        // We won't be attached to the activity anymore
292        mAttachedToHierarchy = false;
293    }
294
295    @Override
296    public void notifyDependencyChange(boolean disableDependents) {
297        super.notifyDependencyChange(disableDependents);
298
299        // Child preferences have an implicit dependency on their containing
300        // group. Dispatch dependency change to all contained preferences.
301        final int preferenceCount = getPreferenceCount();
302        for (int i = 0; i < preferenceCount; i++) {
303            getPreference(i).onParentChanged(this, disableDependents);
304        }
305    }
306
307    void sortPreferences() {
308        synchronized (this) {
309            Collections.sort(mPreferenceList);
310        }
311    }
312
313    @Override
314    protected void dispatchSaveInstanceState(Bundle container) {
315        super.dispatchSaveInstanceState(container);
316
317        // Dispatch to all contained preferences
318        final int preferenceCount = getPreferenceCount();
319        for (int i = 0; i < preferenceCount; i++) {
320            getPreference(i).dispatchSaveInstanceState(container);
321        }
322    }
323
324    @Override
325    protected void dispatchRestoreInstanceState(Bundle container) {
326        super.dispatchRestoreInstanceState(container);
327
328        // Dispatch to all contained preferences
329        final int preferenceCount = getPreferenceCount();
330        for (int i = 0; i < preferenceCount; i++) {
331            getPreference(i).dispatchRestoreInstanceState(container);
332        }
333    }
334
335}
336