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