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