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