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 
17 package android.preference;
18 
19 import android.animation.LayoutTransition;
20 import android.annotation.Nullable;
21 import android.annotation.StringRes;
22 import android.annotation.XmlRes;
23 import android.app.Fragment;
24 import android.app.FragmentBreadCrumbs;
25 import android.app.FragmentManager;
26 import android.app.FragmentTransaction;
27 import android.app.ListActivity;
28 import android.compat.annotation.UnsupportedAppUsage;
29 import android.content.Context;
30 import android.content.Intent;
31 import android.content.res.Resources;
32 import android.content.res.TypedArray;
33 import android.content.res.XmlResourceParser;
34 import android.os.Bundle;
35 import android.os.Handler;
36 import android.os.Message;
37 import android.os.Parcel;
38 import android.os.Parcelable;
39 import android.text.TextUtils;
40 import android.util.AttributeSet;
41 import android.util.TypedValue;
42 import android.util.Xml;
43 import android.view.LayoutInflater;
44 import android.view.MenuItem;
45 import android.view.View;
46 import android.view.View.OnClickListener;
47 import android.view.ViewGroup;
48 import android.widget.AbsListView;
49 import android.widget.ArrayAdapter;
50 import android.widget.BaseAdapter;
51 import android.widget.Button;
52 import android.widget.FrameLayout;
53 import android.widget.ImageView;
54 import android.widget.ListView;
55 import android.widget.TextView;
56 
57 import com.android.internal.util.XmlUtils;
58 
59 import org.xmlpull.v1.XmlPullParser;
60 import org.xmlpull.v1.XmlPullParserException;
61 
62 import java.io.IOException;
63 import java.util.ArrayList;
64 import java.util.List;
65 
66 /**
67  * This is the base class for an activity to show a hierarchy of preferences
68  * to the user.  Prior to {@link android.os.Build.VERSION_CODES#HONEYCOMB}
69  * this class only allowed the display of a single set of preference; this
70  * functionality should now be found in the new {@link PreferenceFragment}
71  * class.  If you are using PreferenceActivity in its old mode, the documentation
72  * there applies to the deprecated APIs here.
73  *
74  * <p>This activity shows one or more headers of preferences, each of which
75  * is associated with a {@link PreferenceFragment} to display the preferences
76  * of that header.  The actual layout and display of these associations can
77  * however vary; currently there are two major approaches it may take:
78  *
79  * <ul>
80  * <li>On a small screen it may display only the headers as a single list when first launched.
81  * Selecting one of the header items will only show the PreferenceFragment of that header (on
82  * Android N and lower a new Activity is launched).
83  * <li>On a large screen it may display both the headers and current PreferenceFragment together as
84  * panes. Selecting a header item switches to showing the correct PreferenceFragment for that item.
85  * </ul>
86  *
87  * <p>Subclasses of PreferenceActivity should implement
88  * {@link #onBuildHeaders} to populate the header list with the desired
89  * items.  Doing this implicitly switches the class into its new "headers
90  * + fragments" mode rather than the old style of just showing a single
91  * preferences list.
92  *
93  * <div class="special reference">
94  * <h3>Developer Guides</h3>
95  * <p>For information about using {@code PreferenceActivity},
96  * read the <a href="{@docRoot}guide/topics/ui/settings.html">Settings</a>
97  * guide.</p>
98  * </div>
99  *
100  * @deprecated Use the <a href="{@docRoot}jetpack/androidx.html">AndroidX</a>
101  *      <a href="{@docRoot}reference/androidx/preference/package-summary.html">
102  *      Preference Library</a> for consistent behavior across all devices. For more information on
103  *      using the AndroidX Preference Library see
104  *      <a href="{@docRoot}guide/topics/ui/settings.html">Settings</a>.
105  */
106 @Deprecated
107 public abstract class PreferenceActivity extends ListActivity implements
108         PreferenceManager.OnPreferenceTreeClickListener,
109         PreferenceFragment.OnPreferenceStartFragmentCallback {
110 
111     private static final String TAG = "PreferenceActivity";
112 
113     // Constants for state save/restore
114     private static final String HEADERS_TAG = ":android:headers";
115     private static final String CUR_HEADER_TAG = ":android:cur_header";
116     private static final String PREFERENCES_TAG = ":android:preferences";
117 
118     /**
119      * When starting this activity, the invoking Intent can contain this extra
120      * string to specify which fragment should be initially displayed.
121      * <p/>Starting from Key Lime Pie, when this argument is passed in, the PreferenceActivity
122      * will call isValidFragment() to confirm that the fragment class name is valid for this
123      * activity.
124      */
125     public static final String EXTRA_SHOW_FRAGMENT = ":android:show_fragment";
126 
127     /**
128      * When starting this activity and using {@link #EXTRA_SHOW_FRAGMENT},
129      * this extra can also be specified to supply a Bundle of arguments to pass
130      * to that fragment when it is instantiated during the initial creation
131      * of PreferenceActivity.
132      */
133     public static final String EXTRA_SHOW_FRAGMENT_ARGUMENTS = ":android:show_fragment_args";
134 
135     /**
136      * When starting this activity and using {@link #EXTRA_SHOW_FRAGMENT},
137      * this extra can also be specify to supply the title to be shown for
138      * that fragment.
139      */
140     public static final String EXTRA_SHOW_FRAGMENT_TITLE = ":android:show_fragment_title";
141 
142     /**
143      * When starting this activity and using {@link #EXTRA_SHOW_FRAGMENT},
144      * this extra can also be specify to supply the short title to be shown for
145      * that fragment.
146      */
147     public static final String EXTRA_SHOW_FRAGMENT_SHORT_TITLE
148             = ":android:show_fragment_short_title";
149 
150     /**
151      * When starting this activity, the invoking Intent can contain this extra
152      * boolean that the header list should not be displayed.  This is most often
153      * used in conjunction with {@link #EXTRA_SHOW_FRAGMENT} to launch
154      * the activity to display a specific fragment that the user has navigated
155      * to.
156      */
157     public static final String EXTRA_NO_HEADERS = ":android:no_headers";
158 
159     private static final String BACK_STACK_PREFS = ":android:prefs";
160 
161     // extras that allow any preference activity to be launched as part of a wizard
162 
163     // show Back and Next buttons? takes boolean parameter
164     // Back will then return RESULT_CANCELED and Next RESULT_OK
165     private static final String EXTRA_PREFS_SHOW_BUTTON_BAR = "extra_prefs_show_button_bar";
166 
167     // add a Skip button?
168     private static final String EXTRA_PREFS_SHOW_SKIP = "extra_prefs_show_skip";
169 
170     // specify custom text for the Back or Next buttons, or cause a button to not appear
171     // at all by setting it to null
172     private static final String EXTRA_PREFS_SET_NEXT_TEXT = "extra_prefs_set_next_text";
173     private static final String EXTRA_PREFS_SET_BACK_TEXT = "extra_prefs_set_back_text";
174 
175     // --- State for new mode when showing a list of headers + prefs fragment
176 
177     private final ArrayList<Header> mHeaders = new ArrayList<Header>();
178 
179     private FrameLayout mListFooter;
180 
181     @UnsupportedAppUsage
182     private ViewGroup mPrefsContainer;
183 
184     // Backup of the original activity title. This is used when navigating back to the headers list
185     // in onBackPress to restore the title.
186     private CharSequence mActivityTitle;
187 
188     // Null if in legacy mode.
189     private ViewGroup mHeadersContainer;
190 
191     private FragmentBreadCrumbs mFragmentBreadCrumbs;
192 
193     private boolean mSinglePane;
194 
195     private Header mCurHeader;
196 
197     // --- State for old mode when showing a single preference list
198 
199     @UnsupportedAppUsage
200     private PreferenceManager mPreferenceManager;
201 
202     private Bundle mSavedInstanceState;
203 
204     // --- Common state
205 
206     private Button mNextButton;
207 
208     private int mPreferenceHeaderItemResId = 0;
209     private boolean mPreferenceHeaderRemoveEmptyIcon = false;
210 
211     /**
212      * The starting request code given out to preference framework.
213      */
214     private static final int FIRST_REQUEST_CODE = 100;
215 
216     private static final int MSG_BIND_PREFERENCES = 1;
217     private static final int MSG_BUILD_HEADERS = 2;
218     private Handler mHandler = new Handler() {
219         @Override
220         public void handleMessage(Message msg) {
221             switch (msg.what) {
222                 case MSG_BIND_PREFERENCES: {
223                     bindPreferences();
224                 } break;
225                 case MSG_BUILD_HEADERS: {
226                     ArrayList<Header> oldHeaders = new ArrayList<Header>(mHeaders);
227                     mHeaders.clear();
228                     onBuildHeaders(mHeaders);
229                     if (mAdapter instanceof BaseAdapter) {
230                         ((BaseAdapter) mAdapter).notifyDataSetChanged();
231                     }
232                     Header header = onGetNewHeader();
233                     if (header != null && header.fragment != null) {
234                         Header mappedHeader = findBestMatchingHeader(header, oldHeaders);
235                         if (mappedHeader == null || mCurHeader != mappedHeader) {
236                             switchToHeader(header);
237                         }
238                     } else if (mCurHeader != null) {
239                         Header mappedHeader = findBestMatchingHeader(mCurHeader, mHeaders);
240                         if (mappedHeader != null) {
241                             setSelectedHeader(mappedHeader);
242                         }
243                     }
244                 } break;
245             }
246         }
247     };
248 
249     private static class HeaderAdapter extends ArrayAdapter<Header> {
250         private static class HeaderViewHolder {
251             ImageView icon;
252             TextView title;
253             TextView summary;
254         }
255 
256         private LayoutInflater mInflater;
257         private int mLayoutResId;
258         private boolean mRemoveIconIfEmpty;
259 
HeaderAdapter(Context context, List<Header> objects, int layoutResId, boolean removeIconBehavior)260         public HeaderAdapter(Context context, List<Header> objects, int layoutResId,
261                 boolean removeIconBehavior) {
262             super(context, 0, objects);
263             mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
264             mLayoutResId = layoutResId;
265             mRemoveIconIfEmpty = removeIconBehavior;
266         }
267 
268         @Override
getView(int position, View convertView, ViewGroup parent)269         public View getView(int position, View convertView, ViewGroup parent) {
270             HeaderViewHolder holder;
271             View view;
272 
273             if (convertView == null) {
274                 view = mInflater.inflate(mLayoutResId, parent, false);
275                 holder = new HeaderViewHolder();
276                 holder.icon = (ImageView) view.findViewById(com.android.internal.R.id.icon);
277                 holder.title = (TextView) view.findViewById(com.android.internal.R.id.title);
278                 holder.summary = (TextView) view.findViewById(com.android.internal.R.id.summary);
279                 view.setTag(holder);
280             } else {
281                 view = convertView;
282                 holder = (HeaderViewHolder) view.getTag();
283             }
284 
285             // All view fields must be updated every time, because the view may be recycled
286             Header header = getItem(position);
287             if (mRemoveIconIfEmpty) {
288                 if (header.iconRes == 0) {
289                     holder.icon.setVisibility(View.GONE);
290                 } else {
291                     holder.icon.setVisibility(View.VISIBLE);
292                     holder.icon.setImageResource(header.iconRes);
293                 }
294             } else {
295                 holder.icon.setImageResource(header.iconRes);
296             }
297             holder.title.setText(header.getTitle(getContext().getResources()));
298             CharSequence summary = header.getSummary(getContext().getResources());
299             if (!TextUtils.isEmpty(summary)) {
300                 holder.summary.setVisibility(View.VISIBLE);
301                 holder.summary.setText(summary);
302             } else {
303                 holder.summary.setVisibility(View.GONE);
304             }
305 
306             return view;
307         }
308     }
309 
310     /**
311      * Default value for {@link Header#id Header.id} indicating that no
312      * identifier value is set.  All other values (including those below -1)
313      * are valid.
314      */
315     public static final long HEADER_ID_UNDEFINED = -1;
316 
317     /**
318      * Description of a single Header item that the user can select.
319      *
320      * @deprecated Use the <a href="{@docRoot}jetpack/androidx.html">AndroidX</a>
321      *      <a href="{@docRoot}reference/androidx/preference/package-summary.html">
322      *      Preference Library</a> for consistent behavior across all devices.
323      *      For more information on using the AndroidX Preference Library see
324      *      <a href="{@docRoot}guide/topics/ui/settings.html">Settings</a>.
325      */
326     @Deprecated
327     public static final class Header implements Parcelable {
328         /**
329          * Identifier for this header, to correlate with a new list when
330          * it is updated.  The default value is
331          * {@link PreferenceActivity#HEADER_ID_UNDEFINED}, meaning no id.
332          * @attr ref android.R.styleable#PreferenceHeader_id
333          */
334         public long id = HEADER_ID_UNDEFINED;
335 
336         /**
337          * Resource ID of title of the header that is shown to the user.
338          * @attr ref android.R.styleable#PreferenceHeader_title
339          */
340         @StringRes
341         public int titleRes;
342 
343         /**
344          * Title of the header that is shown to the user.
345          * @attr ref android.R.styleable#PreferenceHeader_title
346          */
347         public CharSequence title;
348 
349         /**
350          * Resource ID of optional summary describing what this header controls.
351          * @attr ref android.R.styleable#PreferenceHeader_summary
352          */
353         @StringRes
354         public int summaryRes;
355 
356         /**
357          * Optional summary describing what this header controls.
358          * @attr ref android.R.styleable#PreferenceHeader_summary
359          */
360         public CharSequence summary;
361 
362         /**
363          * Resource ID of optional text to show as the title in the bread crumb.
364          * @attr ref android.R.styleable#PreferenceHeader_breadCrumbTitle
365          */
366         @StringRes
367         public int breadCrumbTitleRes;
368 
369         /**
370          * Optional text to show as the title in the bread crumb.
371          * @attr ref android.R.styleable#PreferenceHeader_breadCrumbTitle
372          */
373         public CharSequence breadCrumbTitle;
374 
375         /**
376          * Resource ID of optional text to show as the short title in the bread crumb.
377          * @attr ref android.R.styleable#PreferenceHeader_breadCrumbShortTitle
378          */
379         @StringRes
380         public int breadCrumbShortTitleRes;
381 
382         /**
383          * Optional text to show as the short title in the bread crumb.
384          * @attr ref android.R.styleable#PreferenceHeader_breadCrumbShortTitle
385          */
386         public CharSequence breadCrumbShortTitle;
387 
388         /**
389          * Optional icon resource to show for this header.
390          * @attr ref android.R.styleable#PreferenceHeader_icon
391          */
392         public int iconRes;
393 
394         /**
395          * Full class name of the fragment to display when this header is
396          * selected.
397          * @attr ref android.R.styleable#PreferenceHeader_fragment
398          */
399         public String fragment;
400 
401         /**
402          * Optional arguments to supply to the fragment when it is
403          * instantiated.
404          */
405         public Bundle fragmentArguments;
406 
407         /**
408          * Intent to launch when the preference is selected.
409          */
410         public Intent intent;
411 
412         /**
413          * Optional additional data for use by subclasses of PreferenceActivity.
414          */
415         public Bundle extras;
416 
Header()417         public Header() {
418             // Empty
419         }
420 
421         /**
422          * Return the currently set title.  If {@link #titleRes} is set,
423          * this resource is loaded from <var>res</var> and returned.  Otherwise
424          * {@link #title} is returned.
425          */
getTitle(Resources res)426         public CharSequence getTitle(Resources res) {
427             if (titleRes != 0) {
428                 return res.getText(titleRes);
429             }
430             return title;
431         }
432 
433         /**
434          * Return the currently set summary.  If {@link #summaryRes} is set,
435          * this resource is loaded from <var>res</var> and returned.  Otherwise
436          * {@link #summary} is returned.
437          */
getSummary(Resources res)438         public CharSequence getSummary(Resources res) {
439             if (summaryRes != 0) {
440                 return res.getText(summaryRes);
441             }
442             return summary;
443         }
444 
445         /**
446          * Return the currently set bread crumb title.  If {@link #breadCrumbTitleRes} is set,
447          * this resource is loaded from <var>res</var> and returned.  Otherwise
448          * {@link #breadCrumbTitle} is returned.
449          */
getBreadCrumbTitle(Resources res)450         public CharSequence getBreadCrumbTitle(Resources res) {
451             if (breadCrumbTitleRes != 0) {
452                 return res.getText(breadCrumbTitleRes);
453             }
454             return breadCrumbTitle;
455         }
456 
457         /**
458          * Return the currently set bread crumb short title.  If
459          * {@link #breadCrumbShortTitleRes} is set,
460          * this resource is loaded from <var>res</var> and returned.  Otherwise
461          * {@link #breadCrumbShortTitle} is returned.
462          */
getBreadCrumbShortTitle(Resources res)463         public CharSequence getBreadCrumbShortTitle(Resources res) {
464             if (breadCrumbShortTitleRes != 0) {
465                 return res.getText(breadCrumbShortTitleRes);
466             }
467             return breadCrumbShortTitle;
468         }
469 
470         @Override
describeContents()471         public int describeContents() {
472             return 0;
473         }
474 
475         @Override
writeToParcel(Parcel dest, int flags)476         public void writeToParcel(Parcel dest, int flags) {
477             dest.writeLong(id);
478             dest.writeInt(titleRes);
479             TextUtils.writeToParcel(title, dest, flags);
480             dest.writeInt(summaryRes);
481             TextUtils.writeToParcel(summary, dest, flags);
482             dest.writeInt(breadCrumbTitleRes);
483             TextUtils.writeToParcel(breadCrumbTitle, dest, flags);
484             dest.writeInt(breadCrumbShortTitleRes);
485             TextUtils.writeToParcel(breadCrumbShortTitle, dest, flags);
486             dest.writeInt(iconRes);
487             dest.writeString(fragment);
488             dest.writeBundle(fragmentArguments);
489             if (intent != null) {
490                 dest.writeInt(1);
491                 intent.writeToParcel(dest, flags);
492             } else {
493                 dest.writeInt(0);
494             }
495             dest.writeBundle(extras);
496         }
497 
readFromParcel(Parcel in)498         public void readFromParcel(Parcel in) {
499             id = in.readLong();
500             titleRes = in.readInt();
501             title = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
502             summaryRes = in.readInt();
503             summary = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
504             breadCrumbTitleRes = in.readInt();
505             breadCrumbTitle = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
506             breadCrumbShortTitleRes = in.readInt();
507             breadCrumbShortTitle = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
508             iconRes = in.readInt();
509             fragment = in.readString();
510             fragmentArguments = in.readBundle();
511             if (in.readInt() != 0) {
512                 intent = Intent.CREATOR.createFromParcel(in);
513             }
514             extras = in.readBundle();
515         }
516 
Header(Parcel in)517         Header(Parcel in) {
518             readFromParcel(in);
519         }
520 
521         public static final @android.annotation.NonNull Creator<Header> CREATOR = new Creator<Header>() {
522             public Header createFromParcel(Parcel source) {
523                 return new Header(source);
524             }
525             public Header[] newArray(int size) {
526                 return new Header[size];
527             }
528         };
529     }
530 
531     @Override
onOptionsItemSelected(MenuItem item)532     public boolean onOptionsItemSelected(MenuItem item) {
533         if (item.getItemId() == android.R.id.home) {
534             // Override home navigation button to call onBackPressed (b/35152749).
535             onBackPressed();
536             return true;
537         }
538         return super.onOptionsItemSelected(item);
539     }
540 
541     @Override
onCreate(@ullable Bundle savedInstanceState)542     protected void onCreate(@Nullable Bundle savedInstanceState) {
543         super.onCreate(savedInstanceState);
544 
545         // Theming for the PreferenceActivity layout and for the Preference Header(s) layout
546         TypedArray sa = obtainStyledAttributes(null,
547                 com.android.internal.R.styleable.PreferenceActivity,
548                 com.android.internal.R.attr.preferenceActivityStyle,
549                 0);
550 
551         final int layoutResId = sa.getResourceId(
552                 com.android.internal.R.styleable.PreferenceActivity_layout,
553                 com.android.internal.R.layout.preference_list_content);
554 
555         mPreferenceHeaderItemResId = sa.getResourceId(
556                 com.android.internal.R.styleable.PreferenceActivity_headerLayout,
557                 com.android.internal.R.layout.preference_header_item);
558         mPreferenceHeaderRemoveEmptyIcon = sa.getBoolean(
559                 com.android.internal.R.styleable.PreferenceActivity_headerRemoveIconIfEmpty,
560                 false);
561 
562         sa.recycle();
563 
564         setContentView(layoutResId);
565 
566         mListFooter = (FrameLayout)findViewById(com.android.internal.R.id.list_footer);
567         mPrefsContainer = (ViewGroup) findViewById(com.android.internal.R.id.prefs_frame);
568         mHeadersContainer = (ViewGroup) findViewById(com.android.internal.R.id.headers);
569         boolean hidingHeaders = onIsHidingHeaders();
570         mSinglePane = hidingHeaders || !onIsMultiPane();
571         String initialFragment = getIntent().getStringExtra(EXTRA_SHOW_FRAGMENT);
572         Bundle initialArguments = getIntent().getBundleExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS);
573         int initialTitle = getIntent().getIntExtra(EXTRA_SHOW_FRAGMENT_TITLE, 0);
574         int initialShortTitle = getIntent().getIntExtra(EXTRA_SHOW_FRAGMENT_SHORT_TITLE, 0);
575         mActivityTitle = getTitle();
576 
577         if (savedInstanceState != null) {
578             // We are restarting from a previous saved state; used that to
579             // initialize, instead of starting fresh.
580             ArrayList<Header> headers = savedInstanceState.getParcelableArrayList(HEADERS_TAG);
581             if (headers != null) {
582                 mHeaders.addAll(headers);
583                 int curHeader = savedInstanceState.getInt(CUR_HEADER_TAG,
584                         (int) HEADER_ID_UNDEFINED);
585                 if (curHeader >= 0 && curHeader < mHeaders.size()) {
586                     setSelectedHeader(mHeaders.get(curHeader));
587                 } else if (!mSinglePane && initialFragment == null) {
588                     switchToHeader(onGetInitialHeader());
589                 }
590             } else {
591                 // This will for instance hide breadcrumbs for single pane.
592                 showBreadCrumbs(getTitle(), null);
593             }
594         } else {
595             if (!onIsHidingHeaders()) {
596                 onBuildHeaders(mHeaders);
597             }
598 
599             if (initialFragment != null) {
600                 switchToHeader(initialFragment, initialArguments);
601             } else if (!mSinglePane && mHeaders.size() > 0) {
602                 switchToHeader(onGetInitialHeader());
603             }
604         }
605 
606         if (mHeaders.size() > 0) {
607             setListAdapter(new HeaderAdapter(this, mHeaders, mPreferenceHeaderItemResId,
608                     mPreferenceHeaderRemoveEmptyIcon));
609             if (!mSinglePane) {
610                 getListView().setChoiceMode(AbsListView.CHOICE_MODE_SINGLE);
611             }
612         }
613 
614         if (mSinglePane && initialFragment != null && initialTitle != 0) {
615             CharSequence initialTitleStr = getText(initialTitle);
616             CharSequence initialShortTitleStr = initialShortTitle != 0
617                     ? getText(initialShortTitle) : null;
618             showBreadCrumbs(initialTitleStr, initialShortTitleStr);
619         }
620 
621         if (mHeaders.size() == 0 && initialFragment == null) {
622             // If there are no headers, we are in the old "just show a screen
623             // of preferences" mode.
624             setContentView(com.android.internal.R.layout.preference_list_content_single);
625             mListFooter = (FrameLayout) findViewById(com.android.internal.R.id.list_footer);
626             mPrefsContainer = (ViewGroup) findViewById(com.android.internal.R.id.prefs);
627             mPreferenceManager = new PreferenceManager(this, FIRST_REQUEST_CODE);
628             mPreferenceManager.setOnPreferenceTreeClickListener(this);
629             mHeadersContainer = null;
630         } else if (mSinglePane) {
631             // Single-pane so one of the header or prefs containers must be hidden.
632             if (initialFragment != null || mCurHeader != null) {
633                 mHeadersContainer.setVisibility(View.GONE);
634             } else {
635                 mPrefsContainer.setVisibility(View.GONE);
636             }
637 
638             // This animates our manual transitions between headers and prefs panel in single-pane.
639             // It also comes last so we don't animate any initial layout changes done above.
640             ViewGroup container = (ViewGroup) findViewById(
641                     com.android.internal.R.id.prefs_container);
642             container.setLayoutTransition(new LayoutTransition());
643         } else {
644             // Multi-pane
645             if (mHeaders.size() > 0 && mCurHeader != null) {
646                 setSelectedHeader(mCurHeader);
647             }
648         }
649 
650         // see if we should show Back/Next buttons
651         Intent intent = getIntent();
652         if (intent.getBooleanExtra(EXTRA_PREFS_SHOW_BUTTON_BAR, false)) {
653 
654             findViewById(com.android.internal.R.id.button_bar).setVisibility(View.VISIBLE);
655 
656             Button backButton = (Button)findViewById(com.android.internal.R.id.back_button);
657             backButton.setOnClickListener(new OnClickListener() {
658                 public void onClick(View v) {
659                     setResult(RESULT_CANCELED);
660                     finish();
661                 }
662             });
663             Button skipButton = (Button)findViewById(com.android.internal.R.id.skip_button);
664             skipButton.setOnClickListener(new OnClickListener() {
665                 public void onClick(View v) {
666                     setResult(RESULT_OK);
667                     finish();
668                 }
669             });
670             mNextButton = (Button)findViewById(com.android.internal.R.id.next_button);
671             mNextButton.setOnClickListener(new OnClickListener() {
672                 public void onClick(View v) {
673                     setResult(RESULT_OK);
674                     finish();
675                 }
676             });
677 
678             // set our various button parameters
679             if (intent.hasExtra(EXTRA_PREFS_SET_NEXT_TEXT)) {
680                 String buttonText = intent.getStringExtra(EXTRA_PREFS_SET_NEXT_TEXT);
681                 if (TextUtils.isEmpty(buttonText)) {
682                     mNextButton.setVisibility(View.GONE);
683                 }
684                 else {
685                     mNextButton.setText(buttonText);
686                 }
687             }
688             if (intent.hasExtra(EXTRA_PREFS_SET_BACK_TEXT)) {
689                 String buttonText = intent.getStringExtra(EXTRA_PREFS_SET_BACK_TEXT);
690                 if (TextUtils.isEmpty(buttonText)) {
691                     backButton.setVisibility(View.GONE);
692                 }
693                 else {
694                     backButton.setText(buttonText);
695                 }
696             }
697             if (intent.getBooleanExtra(EXTRA_PREFS_SHOW_SKIP, false)) {
698                 skipButton.setVisibility(View.VISIBLE);
699             }
700         }
701     }
702 
703     @Override
onBackPressed()704     public void onBackPressed() {
705         if (mCurHeader != null && mSinglePane && getFragmentManager().getBackStackEntryCount() == 0
706                 && getIntent().getStringExtra(EXTRA_SHOW_FRAGMENT) == null) {
707             mCurHeader = null;
708 
709             mPrefsContainer.setVisibility(View.GONE);
710             mHeadersContainer.setVisibility(View.VISIBLE);
711             if (mActivityTitle != null) {
712                 showBreadCrumbs(mActivityTitle, null);
713             }
714             getListView().clearChoices();
715         } else {
716             super.onBackPressed();
717         }
718     }
719 
720     /**
721      * Returns true if this activity is currently showing the header list.
722      */
hasHeaders()723     public boolean hasHeaders() {
724         return mHeadersContainer != null && mHeadersContainer.getVisibility() == View.VISIBLE;
725     }
726 
727     /**
728      * Returns the Header list
729      * @hide
730      */
731     @UnsupportedAppUsage
getHeaders()732     public List<Header> getHeaders() {
733         return mHeaders;
734     }
735 
736     /**
737      * Returns true if this activity is showing multiple panes -- the headers
738      * and a preference fragment.
739      */
isMultiPane()740     public boolean isMultiPane() {
741         return !mSinglePane;
742     }
743 
744     /**
745      * Called to determine if the activity should run in multi-pane mode.
746      * The default implementation returns true if the screen is large
747      * enough.
748      */
onIsMultiPane()749     public boolean onIsMultiPane() {
750         boolean preferMultiPane = getResources().getBoolean(
751                 com.android.internal.R.bool.preferences_prefer_dual_pane);
752         return preferMultiPane;
753     }
754 
755     /**
756      * Called to determine whether the header list should be hidden.
757      * The default implementation returns the
758      * value given in {@link #EXTRA_NO_HEADERS} or false if it is not supplied.
759      * This is set to false, for example, when the activity is being re-launched
760      * to show a particular preference activity.
761      */
onIsHidingHeaders()762     public boolean onIsHidingHeaders() {
763         return getIntent().getBooleanExtra(EXTRA_NO_HEADERS, false);
764     }
765 
766     /**
767      * Called to determine the initial header to be shown.  The default
768      * implementation simply returns the fragment of the first header.  Note
769      * that the returned Header object does not actually need to exist in
770      * your header list -- whatever its fragment is will simply be used to
771      * show for the initial UI.
772      */
onGetInitialHeader()773     public Header onGetInitialHeader() {
774         for (int i=0; i<mHeaders.size(); i++) {
775             Header h = mHeaders.get(i);
776             if (h.fragment != null) {
777                 return h;
778             }
779         }
780         throw new IllegalStateException("Must have at least one header with a fragment");
781     }
782 
783     /**
784      * Called after the header list has been updated ({@link #onBuildHeaders}
785      * has been called and returned due to {@link #invalidateHeaders()}) to
786      * specify the header that should now be selected.  The default implementation
787      * returns null to keep whatever header is currently selected.
788      */
onGetNewHeader()789     public Header onGetNewHeader() {
790         return null;
791     }
792 
793     /**
794      * Called when the activity needs its list of headers build.  By
795      * implementing this and adding at least one item to the list, you
796      * will cause the activity to run in its modern fragment mode.  Note
797      * that this function may not always be called; for example, if the
798      * activity has been asked to display a particular fragment without
799      * the header list, there is no need to build the headers.
800      *
801      * <p>Typical implementations will use {@link #loadHeadersFromResource}
802      * to fill in the list from a resource.
803      *
804      * @param target The list in which to place the headers.
805      */
onBuildHeaders(List<Header> target)806     public void onBuildHeaders(List<Header> target) {
807         // Should be overloaded by subclasses
808     }
809 
810     /**
811      * Call when you need to change the headers being displayed.  Will result
812      * in onBuildHeaders() later being called to retrieve the new list.
813      */
invalidateHeaders()814     public void invalidateHeaders() {
815         if (!mHandler.hasMessages(MSG_BUILD_HEADERS)) {
816             mHandler.sendEmptyMessage(MSG_BUILD_HEADERS);
817         }
818     }
819 
820     /**
821      * Parse the given XML file as a header description, adding each
822      * parsed Header into the target list.
823      *
824      * @param resid The XML resource to load and parse.
825      * @param target The list in which the parsed headers should be placed.
826      */
loadHeadersFromResource(@mlRes int resid, List<Header> target)827     public void loadHeadersFromResource(@XmlRes int resid, List<Header> target) {
828         XmlResourceParser parser = null;
829         try {
830             parser = getResources().getXml(resid);
831             AttributeSet attrs = Xml.asAttributeSet(parser);
832 
833             int type;
834             while ((type=parser.next()) != XmlPullParser.END_DOCUMENT
835                     && type != XmlPullParser.START_TAG) {
836                 // Parse next until start tag is found
837             }
838 
839             String nodeName = parser.getName();
840             if (!"preference-headers".equals(nodeName)) {
841                 throw new RuntimeException(
842                         "XML document must start with <preference-headers> tag; found"
843                                 + nodeName + " at " + parser.getPositionDescription());
844             }
845 
846             Bundle curBundle = null;
847 
848             final int outerDepth = parser.getDepth();
849             while ((type=parser.next()) != XmlPullParser.END_DOCUMENT
850                     && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
851                 if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
852                     continue;
853                 }
854 
855                 nodeName = parser.getName();
856                 if ("header".equals(nodeName)) {
857                     Header header = new Header();
858 
859                     TypedArray sa = obtainStyledAttributes(
860                             attrs, com.android.internal.R.styleable.PreferenceHeader);
861                     header.id = sa.getResourceId(
862                             com.android.internal.R.styleable.PreferenceHeader_id,
863                             (int)HEADER_ID_UNDEFINED);
864                     TypedValue tv = sa.peekValue(
865                             com.android.internal.R.styleable.PreferenceHeader_title);
866                     if (tv != null && tv.type == TypedValue.TYPE_STRING) {
867                         if (tv.resourceId != 0) {
868                             header.titleRes = tv.resourceId;
869                         } else {
870                             header.title = tv.string;
871                         }
872                     }
873                     tv = sa.peekValue(
874                             com.android.internal.R.styleable.PreferenceHeader_summary);
875                     if (tv != null && tv.type == TypedValue.TYPE_STRING) {
876                         if (tv.resourceId != 0) {
877                             header.summaryRes = tv.resourceId;
878                         } else {
879                             header.summary = tv.string;
880                         }
881                     }
882                     tv = sa.peekValue(
883                             com.android.internal.R.styleable.PreferenceHeader_breadCrumbTitle);
884                     if (tv != null && tv.type == TypedValue.TYPE_STRING) {
885                         if (tv.resourceId != 0) {
886                             header.breadCrumbTitleRes = tv.resourceId;
887                         } else {
888                             header.breadCrumbTitle = tv.string;
889                         }
890                     }
891                     tv = sa.peekValue(
892                             com.android.internal.R.styleable.PreferenceHeader_breadCrumbShortTitle);
893                     if (tv != null && tv.type == TypedValue.TYPE_STRING) {
894                         if (tv.resourceId != 0) {
895                             header.breadCrumbShortTitleRes = tv.resourceId;
896                         } else {
897                             header.breadCrumbShortTitle = tv.string;
898                         }
899                     }
900                     header.iconRes = sa.getResourceId(
901                             com.android.internal.R.styleable.PreferenceHeader_icon, 0);
902                     header.fragment = sa.getString(
903                             com.android.internal.R.styleable.PreferenceHeader_fragment);
904                     sa.recycle();
905 
906                     if (curBundle == null) {
907                         curBundle = new Bundle();
908                     }
909 
910                     final int innerDepth = parser.getDepth();
911                     while ((type=parser.next()) != XmlPullParser.END_DOCUMENT
912                             && (type != XmlPullParser.END_TAG || parser.getDepth() > innerDepth)) {
913                         if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
914                             continue;
915                         }
916 
917                         String innerNodeName = parser.getName();
918                         if (innerNodeName.equals("extra")) {
919                             getResources().parseBundleExtra("extra", attrs, curBundle);
920                             XmlUtils.skipCurrentTag(parser);
921 
922                         } else if (innerNodeName.equals("intent")) {
923                             header.intent = Intent.parseIntent(getResources(), parser, attrs);
924 
925                         } else {
926                             XmlUtils.skipCurrentTag(parser);
927                         }
928                     }
929 
930                     if (curBundle.size() > 0) {
931                         header.fragmentArguments = curBundle;
932                         curBundle = null;
933                     }
934 
935                     target.add(header);
936                 } else {
937                     XmlUtils.skipCurrentTag(parser);
938                 }
939             }
940 
941         } catch (XmlPullParserException e) {
942             throw new RuntimeException("Error parsing headers", e);
943         } catch (IOException e) {
944             throw new RuntimeException("Error parsing headers", e);
945         } finally {
946             if (parser != null) parser.close();
947         }
948     }
949 
950     /**
951      * Subclasses should override this method and verify that the given fragment is a valid type
952      * to be attached to this activity. The default implementation returns <code>true</code> for
953      * apps built for <code>android:targetSdkVersion</code> older than
954      * {@link android.os.Build.VERSION_CODES#KITKAT}. For later versions, it will throw an exception.
955      * @param fragmentName the class name of the Fragment about to be attached to this activity.
956      * @return true if the fragment class name is valid for this Activity and false otherwise.
957      */
isValidFragment(String fragmentName)958     protected boolean isValidFragment(String fragmentName) {
959         if (getApplicationInfo().targetSdkVersion  >= android.os.Build.VERSION_CODES.KITKAT) {
960             throw new RuntimeException(
961                     "Subclasses of PreferenceActivity must override isValidFragment(String)"
962                             + " to verify that the Fragment class is valid! "
963                             + this.getClass().getName()
964                             + " has not checked if fragment " + fragmentName + " is valid.");
965         } else {
966             return true;
967         }
968     }
969 
970     /**
971      * Set a footer that should be shown at the bottom of the header list.
972      */
setListFooter(View view)973     public void setListFooter(View view) {
974         mListFooter.removeAllViews();
975         mListFooter.addView(view, new FrameLayout.LayoutParams(
976                 FrameLayout.LayoutParams.MATCH_PARENT,
977                 FrameLayout.LayoutParams.WRAP_CONTENT));
978     }
979 
980     @Override
onStop()981     protected void onStop() {
982         super.onStop();
983 
984         if (mPreferenceManager != null) {
985             mPreferenceManager.dispatchActivityStop();
986         }
987     }
988 
989     @Override
onDestroy()990     protected void onDestroy() {
991         mHandler.removeMessages(MSG_BIND_PREFERENCES);
992         mHandler.removeMessages(MSG_BUILD_HEADERS);
993         super.onDestroy();
994 
995         if (mPreferenceManager != null) {
996             mPreferenceManager.dispatchActivityDestroy();
997         }
998     }
999 
1000     @Override
onSaveInstanceState(Bundle outState)1001     protected void onSaveInstanceState(Bundle outState) {
1002         super.onSaveInstanceState(outState);
1003 
1004         if (mHeaders.size() > 0) {
1005             outState.putParcelableArrayList(HEADERS_TAG, mHeaders);
1006             if (mCurHeader != null) {
1007                 int index = mHeaders.indexOf(mCurHeader);
1008                 if (index >= 0) {
1009                     outState.putInt(CUR_HEADER_TAG, index);
1010                 }
1011             }
1012         }
1013 
1014         if (mPreferenceManager != null) {
1015             final PreferenceScreen preferenceScreen = getPreferenceScreen();
1016             if (preferenceScreen != null) {
1017                 Bundle container = new Bundle();
1018                 preferenceScreen.saveHierarchyState(container);
1019                 outState.putBundle(PREFERENCES_TAG, container);
1020             }
1021         }
1022     }
1023 
1024     @Override
onRestoreInstanceState(Bundle state)1025     protected void onRestoreInstanceState(Bundle state) {
1026         if (mPreferenceManager != null) {
1027             Bundle container = state.getBundle(PREFERENCES_TAG);
1028             if (container != null) {
1029                 final PreferenceScreen preferenceScreen = getPreferenceScreen();
1030                 if (preferenceScreen != null) {
1031                     preferenceScreen.restoreHierarchyState(container);
1032                     mSavedInstanceState = state;
1033                     return;
1034                 }
1035             }
1036         }
1037 
1038         // Only call this if we didn't save the instance state for later.
1039         // If we did save it, it will be restored when we bind the adapter.
1040         super.onRestoreInstanceState(state);
1041 
1042         if (!mSinglePane) {
1043             // Multi-pane.
1044             if (mCurHeader != null) {
1045                 setSelectedHeader(mCurHeader);
1046             }
1047         }
1048     }
1049 
1050     @Override
onActivityResult(int requestCode, int resultCode, Intent data)1051     protected void onActivityResult(int requestCode, int resultCode, Intent data) {
1052         super.onActivityResult(requestCode, resultCode, data);
1053 
1054         if (mPreferenceManager != null) {
1055             mPreferenceManager.dispatchActivityResult(requestCode, resultCode, data);
1056         }
1057     }
1058 
1059     @Override
onContentChanged()1060     public void onContentChanged() {
1061         super.onContentChanged();
1062 
1063         if (mPreferenceManager != null) {
1064             postBindPreferences();
1065         }
1066     }
1067 
1068     @Override
onListItemClick(ListView l, View v, int position, long id)1069     protected void onListItemClick(ListView l, View v, int position, long id) {
1070         if (!isResumed()) {
1071             return;
1072         }
1073         super.onListItemClick(l, v, position, id);
1074 
1075         if (mAdapter != null) {
1076             Object item = mAdapter.getItem(position);
1077             if (item instanceof Header) onHeaderClick((Header) item, position);
1078         }
1079     }
1080 
1081     /**
1082      * Called when the user selects an item in the header list.  The default
1083      * implementation will call either
1084      * {@link #startWithFragment(String, Bundle, Fragment, int, int, int)}
1085      * or {@link #switchToHeader(Header)} as appropriate.
1086      *
1087      * @param header The header that was selected.
1088      * @param position The header's position in the list.
1089      */
onHeaderClick(Header header, int position)1090     public void onHeaderClick(Header header, int position) {
1091         if (header.fragment != null) {
1092             switchToHeader(header);
1093         } else if (header.intent != null) {
1094             startActivity(header.intent);
1095         }
1096     }
1097 
1098     /**
1099      * Called by {@link #startWithFragment(String, Bundle, Fragment, int, int, int)} when
1100      * in single-pane mode, to build an Intent to launch a new activity showing
1101      * the selected fragment.  The default implementation constructs an Intent
1102      * that re-launches the current activity with the appropriate arguments to
1103      * display the fragment.
1104      *
1105      * @param fragmentName The name of the fragment to display.
1106      * @param args Optional arguments to supply to the fragment.
1107      * @param titleRes Optional resource ID of title to show for this item.
1108      * @param shortTitleRes Optional resource ID of short title to show for this item.
1109      * @return Returns an Intent that can be launched to display the given
1110      * fragment.
1111      */
onBuildStartFragmentIntent(String fragmentName, Bundle args, @StringRes int titleRes, int shortTitleRes)1112     public Intent onBuildStartFragmentIntent(String fragmentName, Bundle args,
1113             @StringRes int titleRes, int shortTitleRes) {
1114         Intent intent = new Intent(Intent.ACTION_MAIN);
1115         intent.setClass(this, getClass());
1116         intent.putExtra(EXTRA_SHOW_FRAGMENT, fragmentName);
1117         intent.putExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS, args);
1118         intent.putExtra(EXTRA_SHOW_FRAGMENT_TITLE, titleRes);
1119         intent.putExtra(EXTRA_SHOW_FRAGMENT_SHORT_TITLE, shortTitleRes);
1120         intent.putExtra(EXTRA_NO_HEADERS, true);
1121         return intent;
1122     }
1123 
1124     /**
1125      * Like {@link #startWithFragment(String, Bundle, Fragment, int, int, int)}
1126      * but uses a 0 titleRes.
1127      */
startWithFragment(String fragmentName, Bundle args, Fragment resultTo, int resultRequestCode)1128     public void startWithFragment(String fragmentName, Bundle args,
1129             Fragment resultTo, int resultRequestCode) {
1130         startWithFragment(fragmentName, args, resultTo, resultRequestCode, 0, 0);
1131     }
1132 
1133     /**
1134      * Start a new instance of this activity, showing only the given
1135      * preference fragment.  When launched in this mode, the header list
1136      * will be hidden and the given preference fragment will be instantiated
1137      * and fill the entire activity.
1138      *
1139      * @param fragmentName The name of the fragment to display.
1140      * @param args Optional arguments to supply to the fragment.
1141      * @param resultTo Option fragment that should receive the result of
1142      * the activity launch.
1143      * @param resultRequestCode If resultTo is non-null, this is the request
1144      * code in which to report the result.
1145      * @param titleRes Resource ID of string to display for the title of
1146      * this set of preferences.
1147      * @param shortTitleRes Resource ID of string to display for the short title of
1148      * this set of preferences.
1149      */
startWithFragment(String fragmentName, Bundle args, Fragment resultTo, int resultRequestCode, @StringRes int titleRes, @StringRes int shortTitleRes)1150     public void startWithFragment(String fragmentName, Bundle args,
1151             Fragment resultTo, int resultRequestCode, @StringRes int titleRes,
1152             @StringRes int shortTitleRes) {
1153         Intent intent = onBuildStartFragmentIntent(fragmentName, args, titleRes, shortTitleRes);
1154         if (resultTo == null) {
1155             startActivity(intent);
1156         } else {
1157             resultTo.startActivityForResult(intent, resultRequestCode);
1158         }
1159     }
1160 
1161     /**
1162      * Change the base title of the bread crumbs for the current preferences.
1163      * This will normally be called for you.  See
1164      * {@link android.app.FragmentBreadCrumbs} for more information.
1165      */
showBreadCrumbs(CharSequence title, CharSequence shortTitle)1166     public void showBreadCrumbs(CharSequence title, CharSequence shortTitle) {
1167         if (mFragmentBreadCrumbs == null) {
1168             View crumbs = findViewById(android.R.id.title);
1169             // For screens with a different kind of title, don't create breadcrumbs.
1170             try {
1171                 mFragmentBreadCrumbs = (FragmentBreadCrumbs)crumbs;
1172             } catch (ClassCastException e) {
1173                 setTitle(title);
1174                 return;
1175             }
1176             if (mFragmentBreadCrumbs == null) {
1177                 if (title != null) {
1178                     setTitle(title);
1179                 }
1180                 return;
1181             }
1182             if (mSinglePane) {
1183                 mFragmentBreadCrumbs.setVisibility(View.GONE);
1184                 // Hide the breadcrumb section completely for single-pane
1185                 View bcSection = findViewById(com.android.internal.R.id.breadcrumb_section);
1186                 if (bcSection != null) bcSection.setVisibility(View.GONE);
1187                 setTitle(title);
1188             }
1189             mFragmentBreadCrumbs.setMaxVisible(2);
1190             mFragmentBreadCrumbs.setActivity(this);
1191         }
1192         if (mFragmentBreadCrumbs.getVisibility() != View.VISIBLE) {
1193             setTitle(title);
1194         } else {
1195             mFragmentBreadCrumbs.setTitle(title, shortTitle);
1196             mFragmentBreadCrumbs.setParentTitle(null, null, null);
1197         }
1198     }
1199 
1200     /**
1201      * Should be called after onCreate to ensure that the breadcrumbs, if any, were created.
1202      * This prepends a title to the fragment breadcrumbs and attaches a listener to any clicks
1203      * on the parent entry.
1204      * @param title the title for the breadcrumb
1205      * @param shortTitle the short title for the breadcrumb
1206      */
setParentTitle(CharSequence title, CharSequence shortTitle, OnClickListener listener)1207     public void setParentTitle(CharSequence title, CharSequence shortTitle,
1208             OnClickListener listener) {
1209         if (mFragmentBreadCrumbs != null) {
1210             mFragmentBreadCrumbs.setParentTitle(title, shortTitle, listener);
1211         }
1212     }
1213 
setSelectedHeader(Header header)1214     void setSelectedHeader(Header header) {
1215         mCurHeader = header;
1216         int index = mHeaders.indexOf(header);
1217         if (index >= 0) {
1218             getListView().setItemChecked(index, true);
1219         } else {
1220             getListView().clearChoices();
1221         }
1222         showBreadCrumbs(header);
1223     }
1224 
showBreadCrumbs(Header header)1225     void showBreadCrumbs(Header header) {
1226         if (header != null) {
1227             CharSequence title = header.getBreadCrumbTitle(getResources());
1228             if (title == null) title = header.getTitle(getResources());
1229             if (title == null) title = getTitle();
1230             showBreadCrumbs(title, header.getBreadCrumbShortTitle(getResources()));
1231         } else {
1232             showBreadCrumbs(getTitle(), null);
1233         }
1234     }
1235 
switchToHeaderInner(String fragmentName, Bundle args)1236     private void switchToHeaderInner(String fragmentName, Bundle args) {
1237         getFragmentManager().popBackStack(BACK_STACK_PREFS,
1238                 FragmentManager.POP_BACK_STACK_INCLUSIVE);
1239         if (!isValidFragment(fragmentName)) {
1240             throw new IllegalArgumentException("Invalid fragment for this activity: "
1241                     + fragmentName);
1242         }
1243 
1244         Fragment f = Fragment.instantiate(this, fragmentName, args);
1245         FragmentTransaction transaction = getFragmentManager().beginTransaction();
1246         transaction.setTransition(mSinglePane
1247                 ? FragmentTransaction.TRANSIT_NONE
1248                 : FragmentTransaction.TRANSIT_FRAGMENT_FADE);
1249         transaction.replace(com.android.internal.R.id.prefs, f);
1250         transaction.commitAllowingStateLoss();
1251 
1252         if (mSinglePane && mPrefsContainer.getVisibility() == View.GONE) {
1253             // We are transitioning from headers to preferences panel in single-pane so we need
1254             // to hide headers and show the prefs container.
1255             mPrefsContainer.setVisibility(View.VISIBLE);
1256             mHeadersContainer.setVisibility(View.GONE);
1257         }
1258     }
1259 
1260     /**
1261      * When in two-pane mode, switch the fragment pane to show the given
1262      * preference fragment.
1263      *
1264      * @param fragmentName The name of the fragment to display.
1265      * @param args Optional arguments to supply to the fragment.
1266      */
switchToHeader(String fragmentName, Bundle args)1267     public void switchToHeader(String fragmentName, Bundle args) {
1268         Header selectedHeader = null;
1269         for (int i = 0; i < mHeaders.size(); i++) {
1270             if (fragmentName.equals(mHeaders.get(i).fragment)) {
1271                 selectedHeader = mHeaders.get(i);
1272                 break;
1273             }
1274         }
1275         setSelectedHeader(selectedHeader);
1276         switchToHeaderInner(fragmentName, args);
1277     }
1278 
1279     /**
1280      * When in two-pane mode, switch to the fragment pane to show the given
1281      * preference fragment.
1282      *
1283      * @param header The new header to display.
1284      */
switchToHeader(Header header)1285     public void switchToHeader(Header header) {
1286         if (mCurHeader == header) {
1287             // This is the header we are currently displaying.  Just make sure
1288             // to pop the stack up to its root state.
1289             getFragmentManager().popBackStack(BACK_STACK_PREFS,
1290                     FragmentManager.POP_BACK_STACK_INCLUSIVE);
1291         } else {
1292             if (header.fragment == null) {
1293                 throw new IllegalStateException("can't switch to header that has no fragment");
1294             }
1295             switchToHeaderInner(header.fragment, header.fragmentArguments);
1296             setSelectedHeader(header);
1297         }
1298     }
1299 
findBestMatchingHeader(Header cur, ArrayList<Header> from)1300     Header findBestMatchingHeader(Header cur, ArrayList<Header> from) {
1301         ArrayList<Header> matches = new ArrayList<Header>();
1302         for (int j=0; j<from.size(); j++) {
1303             Header oh = from.get(j);
1304             if (cur == oh || (cur.id != HEADER_ID_UNDEFINED && cur.id == oh.id)) {
1305                 // Must be this one.
1306                 matches.clear();
1307                 matches.add(oh);
1308                 break;
1309             }
1310             if (cur.fragment != null) {
1311                 if (cur.fragment.equals(oh.fragment)) {
1312                     matches.add(oh);
1313                 }
1314             } else if (cur.intent != null) {
1315                 if (cur.intent.equals(oh.intent)) {
1316                     matches.add(oh);
1317                 }
1318             } else if (cur.title != null) {
1319                 if (cur.title.equals(oh.title)) {
1320                     matches.add(oh);
1321                 }
1322             }
1323         }
1324         final int NM = matches.size();
1325         if (NM == 1) {
1326             return matches.get(0);
1327         } else if (NM > 1) {
1328             for (int j=0; j<NM; j++) {
1329                 Header oh = matches.get(j);
1330                 if (cur.fragmentArguments != null &&
1331                         cur.fragmentArguments.equals(oh.fragmentArguments)) {
1332                     return oh;
1333                 }
1334                 if (cur.extras != null && cur.extras.equals(oh.extras)) {
1335                     return oh;
1336                 }
1337                 if (cur.title != null && cur.title.equals(oh.title)) {
1338                     return oh;
1339                 }
1340             }
1341         }
1342         return null;
1343     }
1344 
1345     /**
1346      * Start a new fragment.
1347      *
1348      * @param fragment The fragment to start
1349      * @param push If true, the current fragment will be pushed onto the back stack.  If false,
1350      * the current fragment will be replaced.
1351      */
startPreferenceFragment(Fragment fragment, boolean push)1352     public void startPreferenceFragment(Fragment fragment, boolean push) {
1353         FragmentTransaction transaction = getFragmentManager().beginTransaction();
1354         transaction.replace(com.android.internal.R.id.prefs, fragment);
1355         if (push) {
1356             transaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
1357             transaction.addToBackStack(BACK_STACK_PREFS);
1358         } else {
1359             transaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
1360         }
1361         transaction.commitAllowingStateLoss();
1362     }
1363 
1364     /**
1365      * Start a new fragment containing a preference panel.  If the preferences
1366      * are being displayed in multi-pane mode, the given fragment class will
1367      * be instantiated and placed in the appropriate pane.  If running in
1368      * single-pane mode, a new activity will be launched in which to show the
1369      * fragment.
1370      *
1371      * @param fragmentClass Full name of the class implementing the fragment.
1372      * @param args Any desired arguments to supply to the fragment.
1373      * @param titleRes Optional resource identifier of the title of this
1374      * fragment.
1375      * @param titleText Optional text of the title of this fragment.
1376      * @param resultTo Optional fragment that result data should be sent to.
1377      * If non-null, resultTo.onActivityResult() will be called when this
1378      * preference panel is done.  The launched panel must use
1379      * {@link #finishPreferencePanel(Fragment, int, Intent)} when done.
1380      * @param resultRequestCode If resultTo is non-null, this is the caller's
1381      * request code to be received with the result.
1382      */
startPreferencePanel(String fragmentClass, Bundle args, @StringRes int titleRes, CharSequence titleText, Fragment resultTo, int resultRequestCode)1383     public void startPreferencePanel(String fragmentClass, Bundle args, @StringRes int titleRes,
1384             CharSequence titleText, Fragment resultTo, int resultRequestCode) {
1385         Fragment f = Fragment.instantiate(this, fragmentClass, args);
1386         if (resultTo != null) {
1387             f.setTargetFragment(resultTo, resultRequestCode);
1388         }
1389         FragmentTransaction transaction = getFragmentManager().beginTransaction();
1390         transaction.replace(com.android.internal.R.id.prefs, f);
1391         if (titleRes != 0) {
1392             transaction.setBreadCrumbTitle(titleRes);
1393         } else if (titleText != null) {
1394             transaction.setBreadCrumbTitle(titleText);
1395         }
1396         transaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
1397         transaction.addToBackStack(BACK_STACK_PREFS);
1398         transaction.commitAllowingStateLoss();
1399     }
1400 
1401     /**
1402      * Called by a preference panel fragment to finish itself.
1403      *
1404      * @param caller The fragment that is asking to be finished.
1405      * @param resultCode Optional result code to send back to the original
1406      * launching fragment.
1407      * @param resultData Optional result data to send back to the original
1408      * launching fragment.
1409      */
finishPreferencePanel(Fragment caller, int resultCode, Intent resultData)1410     public void finishPreferencePanel(Fragment caller, int resultCode, Intent resultData) {
1411         // TODO: be smarter about popping the stack.
1412         onBackPressed();
1413         if (caller != null) {
1414             if (caller.getTargetFragment() != null) {
1415                 caller.getTargetFragment().onActivityResult(caller.getTargetRequestCode(),
1416                         resultCode, resultData);
1417             }
1418         }
1419     }
1420 
1421     @Override
onPreferenceStartFragment(PreferenceFragment caller, Preference pref)1422     public boolean onPreferenceStartFragment(PreferenceFragment caller, Preference pref) {
1423         startPreferencePanel(pref.getFragment(), pref.getExtras(), pref.getTitleRes(),
1424                 pref.getTitle(), null, 0);
1425         return true;
1426     }
1427 
1428     /**
1429      * Posts a message to bind the preferences to the list view.
1430      * <p>
1431      * Binding late is preferred as any custom preference types created in
1432      * {@link #onCreate(Bundle)} are able to have their views recycled.
1433      */
1434     @UnsupportedAppUsage
postBindPreferences()1435     private void postBindPreferences() {
1436         if (mHandler.hasMessages(MSG_BIND_PREFERENCES)) return;
1437         mHandler.obtainMessage(MSG_BIND_PREFERENCES).sendToTarget();
1438     }
1439 
bindPreferences()1440     private void bindPreferences() {
1441         final PreferenceScreen preferenceScreen = getPreferenceScreen();
1442         if (preferenceScreen != null) {
1443             preferenceScreen.bind(getListView());
1444             if (mSavedInstanceState != null) {
1445                 super.onRestoreInstanceState(mSavedInstanceState);
1446                 mSavedInstanceState = null;
1447             }
1448         }
1449     }
1450 
1451     /**
1452      * Returns the {@link PreferenceManager} used by this activity.
1453      * @return The {@link PreferenceManager}.
1454      *
1455      * @deprecated This function is not relevant for a modern fragment-based
1456      * PreferenceActivity.
1457      */
1458     @Deprecated
getPreferenceManager()1459     public PreferenceManager getPreferenceManager() {
1460         return mPreferenceManager;
1461     }
1462 
1463     @UnsupportedAppUsage
requirePreferenceManager()1464     private void requirePreferenceManager() {
1465         if (mPreferenceManager == null) {
1466             if (mAdapter == null) {
1467                 throw new RuntimeException("This should be called after super.onCreate.");
1468             }
1469             throw new RuntimeException(
1470                     "Modern two-pane PreferenceActivity requires use of a PreferenceFragment");
1471         }
1472     }
1473 
1474     /**
1475      * Sets the root of the preference hierarchy that this activity is showing.
1476      *
1477      * @param preferenceScreen The root {@link PreferenceScreen} of the preference hierarchy.
1478      *
1479      * @deprecated This function is not relevant for a modern fragment-based
1480      * PreferenceActivity.
1481      */
1482     @Deprecated
setPreferenceScreen(PreferenceScreen preferenceScreen)1483     public void setPreferenceScreen(PreferenceScreen preferenceScreen) {
1484         requirePreferenceManager();
1485 
1486         if (mPreferenceManager.setPreferences(preferenceScreen) && preferenceScreen != null) {
1487             postBindPreferences();
1488             CharSequence title = getPreferenceScreen().getTitle();
1489             // Set the title of the activity
1490             if (title != null) {
1491                 setTitle(title);
1492             }
1493         }
1494     }
1495 
1496     /**
1497      * Gets the root of the preference hierarchy that this activity is showing.
1498      *
1499      * @return The {@link PreferenceScreen} that is the root of the preference
1500      *         hierarchy.
1501      *
1502      * @deprecated This function is not relevant for a modern fragment-based
1503      * PreferenceActivity.
1504      */
1505     @Deprecated
getPreferenceScreen()1506     public PreferenceScreen getPreferenceScreen() {
1507         if (mPreferenceManager != null) {
1508             return mPreferenceManager.getPreferenceScreen();
1509         }
1510         return null;
1511     }
1512 
1513     /**
1514      * Adds preferences from activities that match the given {@link Intent}.
1515      *
1516      * @param intent The {@link Intent} to query activities.
1517      *
1518      * @deprecated This function is not relevant for a modern fragment-based
1519      * PreferenceActivity.
1520      */
1521     @Deprecated
addPreferencesFromIntent(Intent intent)1522     public void addPreferencesFromIntent(Intent intent) {
1523         requirePreferenceManager();
1524 
1525         setPreferenceScreen(mPreferenceManager.inflateFromIntent(intent, getPreferenceScreen()));
1526     }
1527 
1528     /**
1529      * Inflates the given XML resource and adds the preference hierarchy to the current
1530      * preference hierarchy.
1531      *
1532      * @param preferencesResId The XML resource ID to inflate.
1533      *
1534      * @deprecated This function is not relevant for a modern fragment-based
1535      * PreferenceActivity.
1536      */
1537     @Deprecated
addPreferencesFromResource(int preferencesResId)1538     public void addPreferencesFromResource(int preferencesResId) {
1539         requirePreferenceManager();
1540 
1541         setPreferenceScreen(mPreferenceManager.inflateFromResource(this, preferencesResId,
1542                 getPreferenceScreen()));
1543     }
1544 
1545     /**
1546      * {@inheritDoc}
1547      *
1548      * @deprecated This function is not relevant for a modern fragment-based
1549      * PreferenceActivity.
1550      */
1551     @Deprecated
onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference)1552     public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference) {
1553         return false;
1554     }
1555 
1556     /**
1557      * Finds a {@link Preference} based on its key.
1558      *
1559      * @param key The key of the preference to retrieve.
1560      * @return The {@link Preference} with the key, or null.
1561      * @see PreferenceGroup#findPreference(CharSequence)
1562      *
1563      * @deprecated This function is not relevant for a modern fragment-based
1564      * PreferenceActivity.
1565      */
1566     @Deprecated
findPreference(CharSequence key)1567     public Preference findPreference(CharSequence key) {
1568 
1569         if (mPreferenceManager == null) {
1570             return null;
1571         }
1572 
1573         return mPreferenceManager.findPreference(key);
1574     }
1575 
1576     @Override
onNewIntent(Intent intent)1577     protected void onNewIntent(Intent intent) {
1578         if (mPreferenceManager != null) {
1579             mPreferenceManager.dispatchNewIntent(intent);
1580         }
1581     }
1582 
1583     // give subclasses access to the Next button
1584     /** @hide */
hasNextButton()1585     protected boolean hasNextButton() {
1586         return mNextButton != null;
1587     }
1588     /** @hide */
getNextButton()1589     protected Button getNextButton() {
1590         return mNextButton;
1591     }
1592 }
1593