• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.graphics.Canvas;
24 import android.graphics.Rect;
25 import android.graphics.drawable.Drawable;
26 import android.os.Bundle;
27 import android.os.Handler;
28 import android.os.Message;
29 import android.util.TypedValue;
30 import android.view.ContextThemeWrapper;
31 import android.view.LayoutInflater;
32 import android.view.View;
33 import android.view.ViewGroup;
34 
35 import androidx.annotation.NonNull;
36 import androidx.annotation.Nullable;
37 import androidx.annotation.RestrictTo;
38 import androidx.annotation.XmlRes;
39 import androidx.fragment.app.DialogFragment;
40 import androidx.fragment.app.Fragment;
41 import androidx.preference.internal.AbstractMultiSelectListPreference;
42 import androidx.recyclerview.widget.LinearLayoutManager;
43 import androidx.recyclerview.widget.RecyclerView;
44 
45 /**
46  * Shows a hierarchy of {@link Preference} objects as
47  * lists. These preferences will
48  * automatically save to {@link android.content.SharedPreferences} as the user interacts with
49  * them. To retrieve an instance of {@link android.content.SharedPreferences} that the
50  * preference hierarchy in this fragment will use, call
51  * {@link PreferenceManager#getDefaultSharedPreferences(android.content.Context)}
52  * with a context in the same package as this fragment.
53  * <p>
54  * Furthermore, the preferences shown will follow the visual style of system
55  * preferences. It is easy to create a hierarchy of preferences (that can be
56  * shown on multiple screens) via XML. For these reasons, it is recommended to
57  * use this fragment (as a superclass) to deal with preferences in applications.
58  * <p>
59  * A {@link PreferenceScreen} object should be at the top of the preference
60  * hierarchy. Furthermore, subsequent {@link PreferenceScreen} in the hierarchy
61  * denote a screen break--that is the preferences contained within subsequent
62  * {@link PreferenceScreen} should be shown on another screen. The preference
63  * framework handles this by calling {@link #onNavigateToScreen(PreferenceScreen)}.
64  * <p>
65  * The preference hierarchy can be formed in multiple ways:
66  * <li> From an XML file specifying the hierarchy
67  * <li> From different {@link android.app.Activity Activities} that each specify its own
68  * preferences in an XML file via {@link android.app.Activity} meta-data
69  * <li> From an object hierarchy rooted with {@link PreferenceScreen}
70  * <p>
71  * To inflate from XML, use the {@link #addPreferencesFromResource(int)}. The
72  * root element should be a {@link PreferenceScreen}. Subsequent elements can point
73  * to actual {@link Preference} subclasses. As mentioned above, subsequent
74  * {@link PreferenceScreen} in the hierarchy will result in the screen break.
75  * <p>
76  * To specify an object hierarchy rooted with {@link PreferenceScreen}, use
77  * {@link #setPreferenceScreen(PreferenceScreen)}.
78  * <p>
79  * As a convenience, this fragment implements a click listener for any
80  * preference in the current hierarchy, see
81  * {@link #onPreferenceTreeClick(Preference)}.
82  *
83  * <div class="special reference">
84  * <h3>Developer Guides</h3>
85  * <p>For information about using {@code PreferenceFragment},
86  * read the <a href="{@docRoot}guide/topics/ui/settings.html">Settings</a>
87  * guide.</p>
88  * </div>
89  *
90  * <a name="SampleCode"></a>
91  * <h3>Sample Code</h3>
92  *
93  * <p>The following sample code shows a simple preference fragment that is
94  * populated from a resource.  The resource it loads is:</p>
95  *
96  * {@sample frameworks/support/samples/SupportPreferenceDemos/src/main/res/xml/preferences.xml preferences}
97  *
98  * <p>The fragment implementation itself simply populates the preferences
99  * when created.  Note that the preferences framework takes care of loading
100  * the current values out of the app preferences and writing them when changed:</p>
101  *
102  * {@sample frameworks/support/samples/SupportPreferenceDemos/src/main/java/com/example/android/supportpreference/FragmentSupportPreferencesCompat.java
103  *      support_fragment_compat}
104  *
105  * @see Preference
106  * @see PreferenceScreen
107  */
108 public abstract class PreferenceFragmentCompat extends Fragment implements
109         PreferenceManager.OnPreferenceTreeClickListener,
110         PreferenceManager.OnDisplayPreferenceDialogListener,
111         PreferenceManager.OnNavigateToScreenListener,
112         DialogPreference.TargetFragment {
113 
114     /**
115      * Fragment argument used to specify the tag of the desired root
116      * {@link androidx.preference.PreferenceScreen} object.
117      */
118     public static final String ARG_PREFERENCE_ROOT =
119             "androidx.preference.PreferenceFragmentCompat.PREFERENCE_ROOT";
120 
121     private static final String PREFERENCES_TAG = "android:preferences";
122 
123     private static final String DIALOG_FRAGMENT_TAG =
124             "androidx.preference.PreferenceFragment.DIALOG";
125 
126     private PreferenceManager mPreferenceManager;
127     private RecyclerView mList;
128     private boolean mHavePrefs;
129     private boolean mInitDone;
130 
131     private Context mStyledContext;
132 
133     private int mLayoutResId = R.layout.preference_list_fragment;
134 
135     private final DividerDecoration mDividerDecoration = new DividerDecoration();
136 
137     private static final int MSG_BIND_PREFERENCES = 1;
138     private Handler mHandler = new Handler() {
139         @Override
140         public void handleMessage(Message msg) {
141             switch (msg.what) {
142 
143                 case MSG_BIND_PREFERENCES:
144                     bindPreferences();
145                     break;
146             }
147         }
148     };
149 
150     final private Runnable mRequestFocus = new Runnable() {
151         @Override
152         public void run() {
153             mList.focusableViewAvailable(mList);
154         }
155     };
156 
157     private Runnable mSelectPreferenceRunnable;
158 
159     /**
160      * Interface that PreferenceFragment's containing activity should
161      * implement to be able to process preference items that wish to
162      * switch to a specified fragment.
163      */
164     public interface OnPreferenceStartFragmentCallback {
165         /**
166          * Called when the user has clicked on a Preference that has
167          * a fragment class name associated with it.  The implementation
168          * should instantiate and switch to an instance of the given
169          * fragment.
170          * @param caller The fragment requesting navigation.
171          * @param pref The preference requesting the fragment.
172          * @return true if the fragment creation has been handled
173          */
onPreferenceStartFragment(PreferenceFragmentCompat caller, Preference pref)174         boolean onPreferenceStartFragment(PreferenceFragmentCompat caller, Preference pref);
175     }
176 
177     /**
178      * Interface that PreferenceFragment's containing activity should
179      * implement to be able to process preference items that wish to
180      * switch to a new screen of preferences.
181      */
182     public interface OnPreferenceStartScreenCallback {
183         /**
184          * Called when the user has clicked on a PreferenceScreen item in order to navigate to a new
185          * screen of preferences.
186          * @param caller The fragment requesting navigation.
187          * @param pref The preference screen to navigate to.
188          * @return true if the screen navigation has been handled
189          */
onPreferenceStartScreen(PreferenceFragmentCompat caller, PreferenceScreen pref)190         boolean onPreferenceStartScreen(PreferenceFragmentCompat caller, PreferenceScreen pref);
191     }
192 
193     public interface OnPreferenceDisplayDialogCallback {
194 
195         /**
196          *
197          * @param caller The fragment containing the preference requesting the dialog.
198          * @param pref The preference requesting the dialog.
199          * @return true if the dialog creation has been handled.
200          */
onPreferenceDisplayDialog(@onNull PreferenceFragmentCompat caller, Preference pref)201         boolean onPreferenceDisplayDialog(@NonNull PreferenceFragmentCompat caller,
202                 Preference pref);
203     }
204 
205     @Override
onCreate(Bundle savedInstanceState)206     public void onCreate(Bundle savedInstanceState) {
207         super.onCreate(savedInstanceState);
208         final TypedValue tv = new TypedValue();
209         getActivity().getTheme().resolveAttribute(R.attr.preferenceTheme, tv, true);
210         final int theme = tv.resourceId;
211         if (theme == 0) {
212             throw new IllegalStateException("Must specify preferenceTheme in theme");
213         }
214         mStyledContext = new ContextThemeWrapper(getActivity(), theme);
215 
216         mPreferenceManager = new PreferenceManager(mStyledContext);
217         mPreferenceManager.setOnNavigateToScreenListener(this);
218         final Bundle args = getArguments();
219         final String rootKey;
220         if (args != null) {
221             rootKey = getArguments().getString(ARG_PREFERENCE_ROOT);
222         } else {
223             rootKey = null;
224         }
225         onCreatePreferences(savedInstanceState, rootKey);
226     }
227 
228     /**
229      * Called during {@link #onCreate(Bundle)} to supply the preferences for this fragment.
230      * Subclasses are expected to call {@link #setPreferenceScreen(PreferenceScreen)} either
231      * directly or via helper methods such as {@link #addPreferencesFromResource(int)}.
232      *
233      * @param savedInstanceState If the fragment is being re-created from
234      *                           a previous saved state, this is the state.
235      * @param rootKey If non-null, this preference fragment should be rooted at the
236      *                {@link androidx.preference.PreferenceScreen} with this key.
237      */
onCreatePreferences(Bundle savedInstanceState, String rootKey)238     public abstract void onCreatePreferences(Bundle savedInstanceState, String rootKey);
239 
240     @Override
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)241     public View onCreateView(LayoutInflater inflater, ViewGroup container,
242             Bundle savedInstanceState) {
243 
244         TypedArray a = mStyledContext.obtainStyledAttributes(null,
245                 R.styleable.PreferenceFragmentCompat,
246                 R.attr.preferenceFragmentCompatStyle,
247                 0);
248 
249         mLayoutResId = a.getResourceId(R.styleable.PreferenceFragmentCompat_android_layout,
250                 mLayoutResId);
251 
252         final Drawable divider = a.getDrawable(
253                 R.styleable.PreferenceFragmentCompat_android_divider);
254         final int dividerHeight = a.getDimensionPixelSize(
255                 R.styleable.PreferenceFragmentCompat_android_dividerHeight, -1);
256         final boolean allowDividerAfterLastItem = a.getBoolean(
257                 R.styleable.PreferenceFragmentCompat_allowDividerAfterLastItem, true);
258 
259         a.recycle();
260 
261         // Need to theme the inflater to pick up the preferenceFragmentListStyle
262         final TypedValue tv = new TypedValue();
263         getActivity().getTheme().resolveAttribute(R.attr.preferenceTheme, tv, true);
264         final int theme = tv.resourceId;
265 
266         final Context themedContext = new ContextThemeWrapper(inflater.getContext(), theme);
267         final LayoutInflater themedInflater = inflater.cloneInContext(themedContext);
268 
269         final View view = themedInflater.inflate(mLayoutResId, container, false);
270 
271         final View rawListContainer = view.findViewById(AndroidResources.ANDROID_R_LIST_CONTAINER);
272         if (!(rawListContainer instanceof ViewGroup)) {
273             throw new RuntimeException("Content has view with id attribute "
274                     + "'android.R.id.list_container' that is not a ViewGroup class");
275         }
276 
277         final ViewGroup listContainer = (ViewGroup) rawListContainer;
278 
279         final RecyclerView listView = onCreateRecyclerView(themedInflater, listContainer,
280                 savedInstanceState);
281         if (listView == null) {
282             throw new RuntimeException("Could not create RecyclerView");
283         }
284 
285         mList = listView;
286 
287         listView.addItemDecoration(mDividerDecoration);
288         setDivider(divider);
289         if (dividerHeight != -1) {
290             setDividerHeight(dividerHeight);
291         }
292         mDividerDecoration.setAllowDividerAfterLastItem(allowDividerAfterLastItem);
293 
294         listContainer.addView(mList);
295         mHandler.post(mRequestFocus);
296 
297         return view;
298     }
299 
300     /**
301      * Sets the drawable that will be drawn between each item in the list.
302      * <p>
303      * <strong>Note:</strong> If the drawable does not have an intrinsic
304      * height, you should also call {@link #setDividerHeight(int)}.
305      *
306      * @param divider the drawable to use
307      * @attr ref R.styleable#PreferenceFragmentCompat_android_divider
308      */
setDivider(Drawable divider)309     public void setDivider(Drawable divider) {
310         mDividerDecoration.setDivider(divider);
311     }
312 
313     /**
314      * Sets the height of the divider that will be drawn between each item in the list. Calling
315      * this will override the intrinsic height as set by {@link #setDivider(Drawable)}
316      *
317      * @param height The new height of the divider in pixels.
318      * @attr ref R.styleable#PreferenceFragmentCompat_android_dividerHeight
319      */
setDividerHeight(int height)320     public void setDividerHeight(int height) {
321         mDividerDecoration.setDividerHeight(height);
322     }
323 
324     @Override
onViewCreated(@onNull View view, @Nullable Bundle savedInstanceState)325     public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
326         super.onViewCreated(view, savedInstanceState);
327 
328         if (mHavePrefs) {
329             bindPreferences();
330             if (mSelectPreferenceRunnable != null) {
331                 mSelectPreferenceRunnable.run();
332                 mSelectPreferenceRunnable = null;
333             }
334         }
335 
336         mInitDone = true;
337     }
338 
339     @Override
onActivityCreated(Bundle savedInstanceState)340     public void onActivityCreated(Bundle savedInstanceState) {
341         super.onActivityCreated(savedInstanceState);
342 
343         if (savedInstanceState != null) {
344             Bundle container = savedInstanceState.getBundle(PREFERENCES_TAG);
345             if (container != null) {
346                 final PreferenceScreen preferenceScreen = getPreferenceScreen();
347                 if (preferenceScreen != null) {
348                     preferenceScreen.restoreHierarchyState(container);
349                 }
350             }
351         }
352     }
353 
354     @Override
onStart()355     public void onStart() {
356         super.onStart();
357         mPreferenceManager.setOnPreferenceTreeClickListener(this);
358         mPreferenceManager.setOnDisplayPreferenceDialogListener(this);
359     }
360 
361     @Override
onStop()362     public void onStop() {
363         super.onStop();
364         mPreferenceManager.setOnPreferenceTreeClickListener(null);
365         mPreferenceManager.setOnDisplayPreferenceDialogListener(null);
366     }
367 
368     @Override
onDestroyView()369     public void onDestroyView() {
370         mHandler.removeCallbacks(mRequestFocus);
371         mHandler.removeMessages(MSG_BIND_PREFERENCES);
372         if (mHavePrefs) {
373             unbindPreferences();
374         }
375         mList = null;
376         super.onDestroyView();
377     }
378 
379     @Override
onSaveInstanceState(Bundle outState)380     public void onSaveInstanceState(Bundle outState) {
381         super.onSaveInstanceState(outState);
382 
383         final PreferenceScreen preferenceScreen = getPreferenceScreen();
384         if (preferenceScreen != null) {
385             Bundle container = new Bundle();
386             preferenceScreen.saveHierarchyState(container);
387             outState.putBundle(PREFERENCES_TAG, container);
388         }
389     }
390 
391     /**
392      * Returns the {@link PreferenceManager} used by this fragment.
393      * @return The {@link PreferenceManager}.
394      */
getPreferenceManager()395     public PreferenceManager getPreferenceManager() {
396         return mPreferenceManager;
397     }
398 
399     /**
400      * Sets the root of the preference hierarchy that this fragment is showing.
401      *
402      * @param preferenceScreen The root {@link PreferenceScreen} of the preference hierarchy.
403      */
setPreferenceScreen(PreferenceScreen preferenceScreen)404     public void setPreferenceScreen(PreferenceScreen preferenceScreen) {
405         if (mPreferenceManager.setPreferences(preferenceScreen) && preferenceScreen != null) {
406             onUnbindPreferences();
407             mHavePrefs = true;
408             if (mInitDone) {
409                 postBindPreferences();
410             }
411         }
412     }
413 
414     /**
415      * Gets the root of the preference hierarchy that this fragment is showing.
416      *
417      * @return The {@link PreferenceScreen} that is the root of the preference
418      *         hierarchy.
419      */
getPreferenceScreen()420     public PreferenceScreen getPreferenceScreen() {
421         return mPreferenceManager.getPreferenceScreen();
422     }
423 
424     /**
425      * Inflates the given XML resource and adds the preference hierarchy to the current
426      * preference hierarchy.
427      *
428      * @param preferencesResId The XML resource ID to inflate.
429      */
addPreferencesFromResource(@mlRes int preferencesResId)430     public void addPreferencesFromResource(@XmlRes int preferencesResId) {
431         requirePreferenceManager();
432 
433         setPreferenceScreen(mPreferenceManager.inflateFromResource(mStyledContext,
434                 preferencesResId, getPreferenceScreen()));
435     }
436 
437     /**
438      * Inflates the given XML resource and replaces the current preference hierarchy (if any) with
439      * the preference hierarchy rooted at {@code key}.
440      *
441      * @param preferencesResId The XML resource ID to inflate.
442      * @param key The preference key of the {@link androidx.preference.PreferenceScreen}
443      *            to use as the root of the preference hierarchy, or null to use the root
444      *            {@link androidx.preference.PreferenceScreen}.
445      */
setPreferencesFromResource(@mlRes int preferencesResId, @Nullable String key)446     public void setPreferencesFromResource(@XmlRes int preferencesResId, @Nullable String key) {
447         requirePreferenceManager();
448 
449         final PreferenceScreen xmlRoot = mPreferenceManager.inflateFromResource(mStyledContext,
450                 preferencesResId, null);
451 
452         final Preference root;
453         if (key != null) {
454             root = xmlRoot.findPreference(key);
455             if (!(root instanceof PreferenceScreen)) {
456                 throw new IllegalArgumentException("Preference object with key " + key
457                         + " is not a PreferenceScreen");
458             }
459         } else {
460             root = xmlRoot;
461         }
462 
463         setPreferenceScreen((PreferenceScreen) root);
464     }
465 
466     /**
467      * {@inheritDoc}
468      */
469     @Override
onPreferenceTreeClick(Preference preference)470     public boolean onPreferenceTreeClick(Preference preference) {
471         if (preference.getFragment() != null) {
472             boolean handled = false;
473             if (getCallbackFragment() instanceof OnPreferenceStartFragmentCallback) {
474                 handled = ((OnPreferenceStartFragmentCallback) getCallbackFragment())
475                         .onPreferenceStartFragment(this, preference);
476             }
477             if (!handled && getActivity() instanceof OnPreferenceStartFragmentCallback){
478                 handled = ((OnPreferenceStartFragmentCallback) getActivity())
479                         .onPreferenceStartFragment(this, preference);
480             }
481             return handled;
482         }
483         return false;
484     }
485 
486     /**
487      * Called by
488      * {@link androidx.preference.PreferenceScreen#onClick()} in order to navigate to a
489      * new screen of preferences. Calls
490      * {@link PreferenceFragmentCompat.OnPreferenceStartScreenCallback#onPreferenceStartScreen}
491      * if the target fragment or containing activity implements
492      * {@link PreferenceFragmentCompat.OnPreferenceStartScreenCallback}.
493      * @param preferenceScreen The {@link androidx.preference.PreferenceScreen} to
494      *                         navigate to.
495      */
496     @Override
onNavigateToScreen(PreferenceScreen preferenceScreen)497     public void onNavigateToScreen(PreferenceScreen preferenceScreen) {
498         boolean handled = false;
499         if (getCallbackFragment() instanceof OnPreferenceStartScreenCallback) {
500             handled = ((OnPreferenceStartScreenCallback) getCallbackFragment())
501                     .onPreferenceStartScreen(this, preferenceScreen);
502         }
503         if (!handled && getActivity() instanceof OnPreferenceStartScreenCallback) {
504             ((OnPreferenceStartScreenCallback) getActivity())
505                     .onPreferenceStartScreen(this, preferenceScreen);
506         }
507     }
508 
509     /**
510      * Finds a {@link Preference} based on its key.
511      *
512      * @param key The key of the preference to retrieve.
513      * @return The {@link Preference} with the key, or null.
514      * @see androidx.preference.PreferenceGroup#findPreference(CharSequence)
515      */
516     @Override
findPreference(CharSequence key)517     public Preference findPreference(CharSequence key) {
518         if (mPreferenceManager == null) {
519             return null;
520         }
521         return mPreferenceManager.findPreference(key);
522     }
523 
requirePreferenceManager()524     private void requirePreferenceManager() {
525         if (mPreferenceManager == null) {
526             throw new RuntimeException("This should be called after super.onCreate.");
527         }
528     }
529 
postBindPreferences()530     private void postBindPreferences() {
531         if (mHandler.hasMessages(MSG_BIND_PREFERENCES)) return;
532         mHandler.obtainMessage(MSG_BIND_PREFERENCES).sendToTarget();
533     }
534 
bindPreferences()535     private void bindPreferences() {
536         final PreferenceScreen preferenceScreen = getPreferenceScreen();
537         if (preferenceScreen != null) {
538             getListView().setAdapter(onCreateAdapter(preferenceScreen));
539             preferenceScreen.onAttached();
540         }
541         onBindPreferences();
542     }
543 
unbindPreferences()544     private void unbindPreferences() {
545         final PreferenceScreen preferenceScreen = getPreferenceScreen();
546         if (preferenceScreen != null) {
547             preferenceScreen.onDetached();
548         }
549         onUnbindPreferences();
550     }
551 
552     /** @hide */
553     @RestrictTo(LIBRARY_GROUP)
onBindPreferences()554     protected void onBindPreferences() {
555     }
556 
557     /** @hide */
558     @RestrictTo(LIBRARY_GROUP)
onUnbindPreferences()559     protected void onUnbindPreferences() {
560     }
561 
getListView()562     public final RecyclerView getListView() {
563         return mList;
564     }
565 
566     /**
567      * Creates the {@link RecyclerView} used to display the preferences.
568      * Subclasses may override this to return a customized
569      * {@link RecyclerView}.
570      * @param inflater The LayoutInflater object that can be used to inflate the
571      *                 {@link RecyclerView}.
572      * @param parent The parent {@link android.view.View} that the RecyclerView will be attached to.
573      *               This method should not add the view itself, but this can be used to generate
574      *               the LayoutParams of the view.
575      * @param savedInstanceState If non-null, this view is being re-constructed from a previous
576      *                           saved state as given here
577      * @return A new RecyclerView object to be placed into the view hierarchy
578      */
onCreateRecyclerView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState)579     public RecyclerView onCreateRecyclerView(LayoutInflater inflater, ViewGroup parent,
580             Bundle savedInstanceState) {
581         RecyclerView recyclerView = (RecyclerView) inflater
582                 .inflate(R.layout.preference_recyclerview, parent, false);
583 
584         recyclerView.setLayoutManager(onCreateLayoutManager());
585         recyclerView.setAccessibilityDelegateCompat(
586                 new PreferenceRecyclerViewAccessibilityDelegate(recyclerView));
587 
588         return recyclerView;
589     }
590 
591     /**
592      * Called from {@link #onCreateRecyclerView} to create the
593      * {@link RecyclerView.LayoutManager} for the created
594      * {@link RecyclerView}.
595      * @return A new {@link RecyclerView.LayoutManager} instance.
596      */
onCreateLayoutManager()597     public RecyclerView.LayoutManager onCreateLayoutManager() {
598         return new LinearLayoutManager(getActivity());
599     }
600 
601     /**
602      * Creates the root adapter.
603      *
604      * @param preferenceScreen Preference screen object to create the adapter for.
605      * @return An adapter that contains the preferences contained in this {@link PreferenceScreen}.
606      */
onCreateAdapter(PreferenceScreen preferenceScreen)607     protected RecyclerView.Adapter onCreateAdapter(PreferenceScreen preferenceScreen) {
608         return new PreferenceGroupAdapter(preferenceScreen);
609     }
610 
611     /**
612      * Called when a preference in the tree requests to display a dialog. Subclasses should
613      * override this method to display custom dialogs or to handle dialogs for custom preference
614      * classes.
615      *
616      * @param preference The Preference object requesting the dialog.
617      */
618     @Override
onDisplayPreferenceDialog(Preference preference)619     public void onDisplayPreferenceDialog(Preference preference) {
620 
621         boolean handled = false;
622         if (getCallbackFragment() instanceof OnPreferenceDisplayDialogCallback) {
623             handled = ((OnPreferenceDisplayDialogCallback) getCallbackFragment())
624                     .onPreferenceDisplayDialog(this, preference);
625         }
626         if (!handled && getActivity() instanceof OnPreferenceDisplayDialogCallback) {
627             handled = ((OnPreferenceDisplayDialogCallback) getActivity())
628                     .onPreferenceDisplayDialog(this, preference);
629         }
630 
631         if (handled) {
632             return;
633         }
634 
635         // check if dialog is already showing
636         if (getFragmentManager().findFragmentByTag(DIALOG_FRAGMENT_TAG) != null) {
637             return;
638         }
639 
640         final DialogFragment f;
641         if (preference instanceof EditTextPreference) {
642             f = EditTextPreferenceDialogFragmentCompat.newInstance(preference.getKey());
643         } else if (preference instanceof ListPreference) {
644             f = ListPreferenceDialogFragmentCompat.newInstance(preference.getKey());
645         } else if (preference instanceof AbstractMultiSelectListPreference) {
646             f = MultiSelectListPreferenceDialogFragmentCompat.newInstance(preference.getKey());
647         } else {
648             throw new IllegalArgumentException("Tried to display dialog for unknown " +
649                     "preference type. Did you forget to override onDisplayPreferenceDialog()?");
650         }
651         f.setTargetFragment(this, 0);
652         f.show(getFragmentManager(), DIALOG_FRAGMENT_TAG);
653     }
654 
655     /**
656      * Basically a wrapper for getParentFragment which is v17+. Used by the leanback preference lib.
657      * @return Fragment to possibly use as a callback
658      * @hide
659      */
660     @RestrictTo(LIBRARY_GROUP)
getCallbackFragment()661     public Fragment getCallbackFragment() {
662         return null;
663     }
664 
scrollToPreference(final String key)665     public void scrollToPreference(final String key) {
666         scrollToPreferenceInternal(null, key);
667     }
668 
scrollToPreference(final Preference preference)669     public void scrollToPreference(final Preference preference) {
670         scrollToPreferenceInternal(preference, null);
671     }
672 
scrollToPreferenceInternal(final Preference preference, final String key)673     private void scrollToPreferenceInternal(final Preference preference, final String key) {
674         final Runnable r = new Runnable() {
675             @Override
676             public void run() {
677                 final RecyclerView.Adapter adapter = mList.getAdapter();
678                 if (!(adapter instanceof
679                         PreferenceGroup.PreferencePositionCallback)) {
680                     if (adapter != null) {
681                         throw new IllegalStateException("Adapter must implement "
682                                 + "PreferencePositionCallback");
683                     } else {
684                         // Adapter was set to null, so don't scroll I guess?
685                         return;
686                     }
687                 }
688                 final int position;
689                 if (preference != null) {
690                     position = ((PreferenceGroup.PreferencePositionCallback) adapter)
691                             .getPreferenceAdapterPosition(preference);
692                 } else {
693                     position = ((PreferenceGroup.PreferencePositionCallback) adapter)
694                             .getPreferenceAdapterPosition(key);
695                 }
696                 if (position != RecyclerView.NO_POSITION) {
697                     mList.scrollToPosition(position);
698                 } else {
699                     // Item not found, wait for an update and try again
700                     adapter.registerAdapterDataObserver(
701                             new ScrollToPreferenceObserver(adapter, mList, preference, key));
702                 }
703             }
704         };
705         if (mList == null) {
706             mSelectPreferenceRunnable = r;
707         } else {
708             r.run();
709         }
710     }
711 
712     private static class ScrollToPreferenceObserver extends RecyclerView.AdapterDataObserver {
713         private final RecyclerView.Adapter mAdapter;
714         private final RecyclerView mList;
715         private final Preference mPreference;
716         private final String mKey;
717 
ScrollToPreferenceObserver(RecyclerView.Adapter adapter, RecyclerView list, Preference preference, String key)718         public ScrollToPreferenceObserver(RecyclerView.Adapter adapter, RecyclerView list,
719                 Preference preference, String key) {
720             mAdapter = adapter;
721             mList = list;
722             mPreference = preference;
723             mKey = key;
724         }
725 
scrollToPreference()726         private void scrollToPreference() {
727             mAdapter.unregisterAdapterDataObserver(this);
728             final int position;
729             if (mPreference != null) {
730                 position = ((PreferenceGroup.PreferencePositionCallback) mAdapter)
731                         .getPreferenceAdapterPosition(mPreference);
732             } else {
733                 position = ((PreferenceGroup.PreferencePositionCallback) mAdapter)
734                         .getPreferenceAdapterPosition(mKey);
735             }
736             if (position != RecyclerView.NO_POSITION) {
737                 mList.scrollToPosition(position);
738             }
739         }
740 
741         @Override
onChanged()742         public void onChanged() {
743             scrollToPreference();
744         }
745 
746         @Override
onItemRangeChanged(int positionStart, int itemCount)747         public void onItemRangeChanged(int positionStart, int itemCount) {
748             scrollToPreference();
749         }
750 
751         @Override
onItemRangeChanged(int positionStart, int itemCount, Object payload)752         public void onItemRangeChanged(int positionStart, int itemCount, Object payload) {
753             scrollToPreference();
754         }
755 
756         @Override
onItemRangeInserted(int positionStart, int itemCount)757         public void onItemRangeInserted(int positionStart, int itemCount) {
758             scrollToPreference();
759         }
760 
761         @Override
onItemRangeRemoved(int positionStart, int itemCount)762         public void onItemRangeRemoved(int positionStart, int itemCount) {
763             scrollToPreference();
764         }
765 
766         @Override
onItemRangeMoved(int fromPosition, int toPosition, int itemCount)767         public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
768             scrollToPreference();
769         }
770     }
771 
772     private class DividerDecoration extends RecyclerView.ItemDecoration {
773 
774         private Drawable mDivider;
775         private int mDividerHeight;
776         private boolean mAllowDividerAfterLastItem = true;
777 
778         @Override
onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state)779         public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
780             if (mDivider == null) {
781                 return;
782             }
783             final int childCount = parent.getChildCount();
784             final int width = parent.getWidth();
785             for (int childViewIndex = 0; childViewIndex < childCount; childViewIndex++) {
786                 final View view = parent.getChildAt(childViewIndex);
787                 if (shouldDrawDividerBelow(view, parent)) {
788                     int top = (int) view.getY() + view.getHeight();
789                     mDivider.setBounds(0, top, width, top + mDividerHeight);
790                     mDivider.draw(c);
791                 }
792             }
793         }
794 
795         @Override
getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state)796         public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
797                 RecyclerView.State state) {
798             if (shouldDrawDividerBelow(view, parent)) {
799                 outRect.bottom = mDividerHeight;
800             }
801         }
802 
shouldDrawDividerBelow(View view, RecyclerView parent)803         private boolean shouldDrawDividerBelow(View view, RecyclerView parent) {
804             final RecyclerView.ViewHolder holder = parent.getChildViewHolder(view);
805             final boolean dividerAllowedBelow = holder instanceof PreferenceViewHolder
806                     && ((PreferenceViewHolder) holder).isDividerAllowedBelow();
807             if (!dividerAllowedBelow) {
808                 return false;
809             }
810             boolean nextAllowed = mAllowDividerAfterLastItem;
811             int index = parent.indexOfChild(view);
812             if (index < parent.getChildCount() - 1) {
813                 final View nextView = parent.getChildAt(index + 1);
814                 final RecyclerView.ViewHolder nextHolder = parent.getChildViewHolder(nextView);
815                 nextAllowed = nextHolder instanceof PreferenceViewHolder
816                         && ((PreferenceViewHolder) nextHolder).isDividerAllowedAbove();
817             }
818             return nextAllowed;
819         }
820 
setDivider(Drawable divider)821         public void setDivider(Drawable divider) {
822             if (divider != null) {
823                 mDividerHeight = divider.getIntrinsicHeight();
824             } else {
825                 mDividerHeight = 0;
826             }
827             mDivider = divider;
828             mList.invalidateItemDecorations();
829         }
830 
setDividerHeight(int dividerHeight)831         public void setDividerHeight(int dividerHeight) {
832             mDividerHeight = dividerHeight;
833             mList.invalidateItemDecorations();
834         }
835 
setAllowDividerAfterLastItem(boolean allowDividerAfterLastItem)836         public void setAllowDividerAfterLastItem(boolean allowDividerAfterLastItem) {
837             mAllowDividerAfterLastItem = allowDividerAfterLastItem;
838         }
839     }
840 }
841