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