Java tutorial
package android.support.v4.preference; import java.io.IOException; import java.util.ArrayList; import java.util.List; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import android.app.FragmentBreadCrumbs; import android.content.Context; import android.content.Intent; import android.content.res.Resources; import android.content.res.TypedArray; import android.content.res.XmlResourceParser; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.os.Parcel; import android.os.Parcelable; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentTransaction; import android.support.v4.app.ListActivity; import android.support.v4.preference.util.XmlUtils; import android.support.v4.preferencefragment.R; import android.text.TextUtils; import android.util.AttributeSet; import android.util.TypedValue; import android.util.Xml; import android.view.LayoutInflater; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; import android.widget.AbsListView; import android.widget.ArrayAdapter; import android.widget.BaseAdapter; import android.widget.Button; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.ListView; import android.widget.TextView; /* * Copyright (C) 2007 The Android Open Source Project * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * http://www.apache.org/licenses/LICENSE-2.0 * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * This is the base class for an activity to show a hierarchy of preferences * to the user. Prior to {@link android.os.Build.VERSION_CODES#HONEYCOMB} this class only allowed * the display of a single set of preference; this * functionality should now be found in the new {@link PreferenceFragment} class. If you are using * PreferenceActivity in its old mode, the documentation * there applies to the deprecated APIs here. * <p> * This activity shows one or more headers of preferences, each of which is associated with a * {@link PreferenceFragment} to display the preferences of that header. The actual layout and * display of these associations can however vary; currently there are two major approaches it may * take: * <ul> * <li>On a small screen it may display only the headers as a single list when first launched. * Selecting one of the header items will re-launch the activity with it only showing the * PreferenceFragment of that header. * <li>On a large screen in may display both the headers and current PreferenceFragment together as * panes. Selecting a header item switches to showing the correct PreferenceFragment for that item. * </ul> * <p> * Subclasses of PreferenceActivity should implement {@link #onBuildHeaders} to populate the header * list with the desired items. Doing this implicitly switches the class into its new "headers + * fragments" mode rather than the old style of just showing a single preferences list. <div * class="special reference"> * <h3>Developer Guides</h3> * <p> * For information about using {@code PreferenceActivity}, * read the <a href="{@docRoot}, read the <a href="{@docRoot} * guide/topics/ui/settings.html">Settings</a> guide. * </p> * </div> * <a name="SampleCode"></a> <h3>Sample Code</h3> * <p> * The following sample code shows a simple preference activity that has two different sets of * preferences. The implementation, consisting of the activity itself as well as its two preference * fragments is: * </p> * {@sample * development/samples/ApiDemos/src/com/example/android/apis/preference/PreferenceWithHeaders.java * activity} * <p> * The preference_headers resource describes the headers to be displayed and the fragments * associated with them. It is: {@sample development/samples/ApiDemos/res/xml/preference_headers.xml * headers} * <p> * The first header is shown by Prefs1Fragment, which populates itself from the following XML * resource: * </p> * {@sample development/samples/ApiDemos/res/xml/fragmented_preferences.xml preferences} * <p> * Note that this XML resource contains a preference screen holding another fragment, the * Prefs1FragmentInner implemented here. This allows the user to traverse down a hierarchy of * preferences; pressing back will pop each fragment off the stack to return to the previous * preferences. * <p> * See {@link PreferenceFragment} for information on implementing the fragments themselves. */ public abstract class PreferenceActivity extends ListActivity implements PreferenceManager.OnPreferenceTreeClickListener, PreferenceFragment.OnPreferenceStartFragmentCallback { private static final String TAG = "PreferenceActivity"; // Constants for state save/restore private static final String HEADERS_TAG = ":android:headers"; private static final String CUR_HEADER_TAG = ":android:cur_header"; private static final String PREFERENCES_TAG = ":android:preferences"; /** * When starting this activity, the invoking Intent can contain this extra * string to specify which fragment should be initially displayed. * <p/> * Starting from Key Lime Pie, when this argument is passed in, the PreferenceActivity will call * isValidFragment() to confirm that the fragment class name is valid for this activity. */ public static final String EXTRA_SHOW_FRAGMENT = ":android:show_fragment"; /** * When starting this activity and using {@link #EXTRA_SHOW_FRAGMENT}, * this extra can also be specified to supply a Bundle of arguments to pass * to that fragment when it is instantiated during the initial creation * of PreferenceActivity. */ public static final String EXTRA_SHOW_FRAGMENT_ARGUMENTS = ":android:show_fragment_args"; /** * When starting this activity and using {@link #EXTRA_SHOW_FRAGMENT}, * this extra can also be specify to supply the title to be shown for * that fragment. */ public static final String EXTRA_SHOW_FRAGMENT_TITLE = ":android:show_fragment_title"; /** * When starting this activity and using {@link #EXTRA_SHOW_FRAGMENT}, * this extra can also be specify to supply the short title to be shown for * that fragment. */ public static final String EXTRA_SHOW_FRAGMENT_SHORT_TITLE = ":android:show_fragment_short_title"; /** * When starting this activity, the invoking Intent can contain this extra * boolean that the header list should not be displayed. This is most often * used in conjunction with {@link #EXTRA_SHOW_FRAGMENT} to launch * the activity to display a specific fragment that the user has navigated * to. */ public static final String EXTRA_NO_HEADERS = ":android:no_headers"; private static final String BACK_STACK_PREFS = ":android:prefs"; // extras that allow any preference activity to be launched as part of a wizard // show Back and Next buttons? takes boolean parameter // Back will then return RESULT_CANCELED and Next RESULT_OK private static final String EXTRA_PREFS_SHOW_BUTTON_BAR = "extra_prefs_show_button_bar"; // add a Skip button? private static final String EXTRA_PREFS_SHOW_SKIP = "extra_prefs_show_skip"; // specify custom text for the Back or Next buttons, or cause a button to not appear // at all by setting it to null private static final String EXTRA_PREFS_SET_NEXT_TEXT = "extra_prefs_set_next_text"; private static final String EXTRA_PREFS_SET_BACK_TEXT = "extra_prefs_set_back_text"; // --- State for new mode when showing a list of headers + prefs fragment private final ArrayList<Header> mHeaders = new ArrayList<Header>(); private FrameLayout mListFooter; private ViewGroup mPrefsContainer; private FragmentBreadCrumbs mFragmentBreadCrumbs; private boolean mSinglePane; private Header mCurHeader; // --- State for old mode when showing a single preference list private PreferenceManager mPreferenceManager; private Bundle mSavedInstanceState; // --- Common state private Button mNextButton; /** * The starting request code given out to preference framework. */ private static final int FIRST_REQUEST_CODE = 100; private static final int MSG_BIND_PREFERENCES = 1; private static final int MSG_BUILD_HEADERS = 2; private Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { case MSG_BIND_PREFERENCES: { bindPreferences(); } break; case MSG_BUILD_HEADERS: { ArrayList<Header> oldHeaders = new ArrayList<Header>(mHeaders); mHeaders.clear(); onBuildHeaders(mHeaders); if (mAdapter instanceof BaseAdapter) { ((BaseAdapter) mAdapter).notifyDataSetChanged(); } Header header = onGetNewHeader(); if (header != null && header.fragment != null) { Header mappedHeader = findBestMatchingHeader(header, oldHeaders); if (mappedHeader == null || mCurHeader != mappedHeader) { switchToHeader(header); } } else if (mCurHeader != null) { Header mappedHeader = findBestMatchingHeader(mCurHeader, mHeaders); if (mappedHeader != null) { setSelectedHeader(mappedHeader); } } } break; } } }; private static class HeaderAdapter extends ArrayAdapter<Header> { private static class HeaderViewHolder { ImageView icon; TextView title; TextView summary; } private LayoutInflater mInflater; public HeaderAdapter(Context context, List<Header> objects) { super(context, 0, objects); mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); } @Override public View getView(int position, View convertView, ViewGroup parent) { HeaderViewHolder holder; View view; if (convertView == null) { view = mInflater.inflate(R.layout.preference_header_item, parent, false); holder = new HeaderViewHolder(); holder.icon = (ImageView) view.findViewById(R.id.icon); holder.title = (TextView) view.findViewById(R.id.title); holder.summary = (TextView) view.findViewById(R.id.summary); view.setTag(holder); } else { view = convertView; holder = (HeaderViewHolder) view.getTag(); } // All view fields must be updated every time, because the view may be recycled Header header = getItem(position); holder.icon.setImageResource(header.iconRes); holder.title.setText(header.getTitle(getContext().getResources())); CharSequence summary = header.getSummary(getContext().getResources()); if (!TextUtils.isEmpty(summary)) { holder.summary.setVisibility(View.VISIBLE); holder.summary.setText(summary); } else { holder.summary.setVisibility(View.GONE); } return view; } } /** * Default value for {@link Header#id Header.id} indicating that no * identifier value is set. All other values (including those below -1) * are valid. */ public static final long HEADER_ID_UNDEFINED = -1; /** * Description of a single Header item that the user can select. */ public static final class Header implements Parcelable { /** * Identifier for this header, to correlate with a new list when * it is updated. The default value is {@link PreferenceActivity#HEADER_ID_UNDEFINED}, * meaning no id. * * @attr ref android.R.styleable#PreferenceHeader_id */ public long id = HEADER_ID_UNDEFINED; /** * Resource ID of title of the header that is shown to the user. * * @attr ref android.R.styleable#PreferenceHeader_title */ public int titleRes; /** * Title of the header that is shown to the user. * * @attr ref android.R.styleable#PreferenceHeader_title */ public CharSequence title; /** * Resource ID of optional summary describing what this header controls. * * @attr ref android.R.styleable#PreferenceHeader_summary */ public int summaryRes; /** * Optional summary describing what this header controls. * * @attr ref android.R.styleable#PreferenceHeader_summary */ public CharSequence summary; /** * Resource ID of optional text to show as the title in the bread crumb. * * @attr ref android.R.styleable#PreferenceHeader_breadCrumbTitle */ public int breadCrumbTitleRes; /** * Optional text to show as the title in the bread crumb. * * @attr ref android.R.styleable#PreferenceHeader_breadCrumbTitle */ public CharSequence breadCrumbTitle; /** * Resource ID of optional text to show as the short title in the bread crumb. * * @attr ref android.R.styleable#PreferenceHeader_breadCrumbShortTitle */ public int breadCrumbShortTitleRes; /** * Optional text to show as the short title in the bread crumb. * * @attr ref android.R.styleable#PreferenceHeader_breadCrumbShortTitle */ public CharSequence breadCrumbShortTitle; /** * Optional icon resource to show for this header. * * @attr ref android.R.styleable#PreferenceHeader_icon */ public int iconRes; /** * Full class name of the fragment to display when this header is * selected. * * @attr ref android.R.styleable#PreferenceHeader_fragment */ public String fragment; /** * Optional arguments to supply to the fragment when it is * instantiated. */ public Bundle fragmentArguments; /** * Intent to launch when the preference is selected. */ public Intent intent; /** * Optional additional data for use by subclasses of PreferenceActivity. */ public Bundle extras; public Header() { // Empty } /** * Return the currently set title. If {@link #titleRes} is set, * this resource is loaded from <var>res</var> and returned. Otherwise {@link #title} is * returned. */ public CharSequence getTitle(Resources res) { if (titleRes != 0) { return res.getText(titleRes); } return title; } /** * Return the currently set summary. If {@link #summaryRes} is set, * this resource is loaded from <var>res</var> and returned. Otherwise {@link #summary} is * returned. */ public CharSequence getSummary(Resources res) { if (summaryRes != 0) { return res.getText(summaryRes); } return summary; } /** * Return the currently set bread crumb title. If {@link #breadCrumbTitleRes} is set, * this resource is loaded from <var>res</var> and returned. Otherwise * {@link #breadCrumbTitle} is returned. */ public CharSequence getBreadCrumbTitle(Resources res) { if (breadCrumbTitleRes != 0) { return res.getText(breadCrumbTitleRes); } return breadCrumbTitle; } /** * Return the currently set bread crumb short title. If {@link #breadCrumbShortTitleRes} is * set, * this resource is loaded from <var>res</var> and returned. Otherwise * {@link #breadCrumbShortTitle} is returned. */ public CharSequence getBreadCrumbShortTitle(Resources res) { if (breadCrumbShortTitleRes != 0) { return res.getText(breadCrumbShortTitleRes); } return breadCrumbShortTitle; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeLong(id); dest.writeInt(titleRes); TextUtils.writeToParcel(title, dest, flags); dest.writeInt(summaryRes); TextUtils.writeToParcel(summary, dest, flags); dest.writeInt(breadCrumbTitleRes); TextUtils.writeToParcel(breadCrumbTitle, dest, flags); dest.writeInt(breadCrumbShortTitleRes); TextUtils.writeToParcel(breadCrumbShortTitle, dest, flags); dest.writeInt(iconRes); dest.writeString(fragment); dest.writeBundle(fragmentArguments); if (intent != null) { dest.writeInt(1); intent.writeToParcel(dest, flags); } else { dest.writeInt(0); } dest.writeBundle(extras); } public void readFromParcel(Parcel in) { id = in.readLong(); titleRes = in.readInt(); title = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); summaryRes = in.readInt(); summary = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); breadCrumbTitleRes = in.readInt(); breadCrumbTitle = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); breadCrumbShortTitleRes = in.readInt(); breadCrumbShortTitle = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); iconRes = in.readInt(); fragment = in.readString(); fragmentArguments = in.readBundle(); if (in.readInt() != 0) { intent = Intent.CREATOR.createFromParcel(in); } extras = in.readBundle(); } Header(Parcel in) { readFromParcel(in); } public static final Creator<Header> CREATOR = new Creator<Header>() { public Header createFromParcel(Parcel source) { return new Header(source); } public Header[] newArray(int size) { return new Header[size]; } }; } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.preference_list_content); mListFooter = (FrameLayout) findViewById(R.id.list_footer); mPrefsContainer = (ViewGroup) findViewById(R.id.prefs_frame); boolean hidingHeaders = onIsHidingHeaders(); mSinglePane = hidingHeaders || !onIsMultiPane(); String initialFragment = getIntent().getStringExtra(EXTRA_SHOW_FRAGMENT); Bundle initialArguments = getIntent().getBundleExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS); int initialTitle = getIntent().getIntExtra(EXTRA_SHOW_FRAGMENT_TITLE, 0); int initialShortTitle = getIntent().getIntExtra(EXTRA_SHOW_FRAGMENT_SHORT_TITLE, 0); if (savedInstanceState != null) { // We are restarting from a previous saved state; used that to // initialize, instead of starting fresh. ArrayList<Header> headers = savedInstanceState.getParcelableArrayList(HEADERS_TAG); if (headers != null) { mHeaders.addAll(headers); int curHeader = savedInstanceState.getInt(CUR_HEADER_TAG, (int) HEADER_ID_UNDEFINED); if (curHeader >= 0 && curHeader < mHeaders.size()) { setSelectedHeader(mHeaders.get(curHeader)); } } } else { if (initialFragment != null && mSinglePane) { // If we are just showing a fragment, we want to run in // new fragment mode, but don't need to compute and show // the headers. switchToHeader(initialFragment, initialArguments); if (initialTitle != 0) { CharSequence initialTitleStr = getText(initialTitle); CharSequence initialShortTitleStr = initialShortTitle != 0 ? getText(initialShortTitle) : null; showBreadCrumbs(initialTitleStr, initialShortTitleStr); } } else { // We need to try to build the headers. onBuildHeaders(mHeaders); // If there are headers, then at this point we need to show // them and, depending on the screen, we may also show in-line // the currently selected preference fragment. if (mHeaders.size() > 0) { if (!mSinglePane) { if (initialFragment == null) { Header h = onGetInitialHeader(); switchToHeader(h); } else { switchToHeader(initialFragment, initialArguments); } } } } } // The default configuration is to only show the list view. Adjust // visibility for other configurations. if (initialFragment != null && mSinglePane) { // Single pane, showing just a prefs fragment. findViewById(R.id.headers).setVisibility(View.GONE); mPrefsContainer.setVisibility(View.VISIBLE); if (initialTitle != 0) { CharSequence initialTitleStr = getText(initialTitle); CharSequence initialShortTitleStr = initialShortTitle != 0 ? getText(initialShortTitle) : null; showBreadCrumbs(initialTitleStr, initialShortTitleStr); } } else if (mHeaders.size() > 0) { setListAdapter(new HeaderAdapter(this, mHeaders)); if (!mSinglePane) { // Multi-pane. getListView().setChoiceMode(AbsListView.CHOICE_MODE_SINGLE); if (mCurHeader != null) { setSelectedHeader(mCurHeader); } mPrefsContainer.setVisibility(View.VISIBLE); } } else { // If there are no headers, we are in the old "just show a screen // of preferences" mode. setContentView(R.layout.preference_list_content_single); mListFooter = (FrameLayout) findViewById(R.id.list_footer); mPrefsContainer = (ViewGroup) findViewById(R.id.prefs); mPreferenceManager = new PreferenceManager(this, FIRST_REQUEST_CODE); mPreferenceManager.setOnPreferenceTreeClickListener(this); } // see if we should show Back/Next buttons Intent intent = getIntent(); if (intent.getBooleanExtra(EXTRA_PREFS_SHOW_BUTTON_BAR, false)) { findViewById(R.id.button_bar).setVisibility(View.VISIBLE); Button backButton = (Button) findViewById(R.id.back_button); backButton.setOnClickListener(new OnClickListener() { public void onClick(View v) { setResult(RESULT_CANCELED); finish(); } }); Button skipButton = (Button) findViewById(R.id.skip_button); skipButton.setOnClickListener(new OnClickListener() { public void onClick(View v) { setResult(RESULT_OK); finish(); } }); mNextButton = (Button) findViewById(R.id.next_button); mNextButton.setOnClickListener(new OnClickListener() { public void onClick(View v) { setResult(RESULT_OK); finish(); } }); // set our various button parameters if (intent.hasExtra(EXTRA_PREFS_SET_NEXT_TEXT)) { String buttonText = intent.getStringExtra(EXTRA_PREFS_SET_NEXT_TEXT); if (TextUtils.isEmpty(buttonText)) { mNextButton.setVisibility(View.GONE); } else { mNextButton.setText(buttonText); } } if (intent.hasExtra(EXTRA_PREFS_SET_BACK_TEXT)) { String buttonText = intent.getStringExtra(EXTRA_PREFS_SET_BACK_TEXT); if (TextUtils.isEmpty(buttonText)) { backButton.setVisibility(View.GONE); } else { backButton.setText(buttonText); } } if (intent.getBooleanExtra(EXTRA_PREFS_SHOW_SKIP, false)) { skipButton.setVisibility(View.VISIBLE); } } } /** * Returns true if this activity is currently showing the header list. */ public boolean hasHeaders() { return getListView().getVisibility() == View.VISIBLE && mPreferenceManager == null; } /** * Returns the Header list * * @hide */ public List<Header> getHeaders() { return mHeaders; } /** * Returns true if this activity is showing multiple panes -- the headers * and a preference fragment. */ public boolean isMultiPane() { return hasHeaders() && mPrefsContainer.getVisibility() == View.VISIBLE; } /** * Called to determine if the activity should run in multi-pane mode. * The default implementation returns true if the screen is large * enough. */ public boolean onIsMultiPane() { // boolean preferMultiPane = getResources().getBoolean( // R.bool.preferences_prefer_dual_pane); // return preferMultiPane; return false; } /** * Called to determine whether the header list should be hidden. * The default implementation returns the * value given in {@link #EXTRA_NO_HEADERS} or false if it is not supplied. * This is set to false, for example, when the activity is being re-launched * to show a particular preference activity. */ public boolean onIsHidingHeaders() { return getIntent().getBooleanExtra(EXTRA_NO_HEADERS, false); } /** * Called to determine the initial header to be shown. The default * implementation simply returns the fragment of the first header. Note * that the returned Header object does not actually need to exist in * your header list -- whatever its fragment is will simply be used to * show for the initial UI. */ public Header onGetInitialHeader() { for (int i = 0; i < mHeaders.size(); i++) { Header h = mHeaders.get(i); if (h.fragment != null) { return h; } } throw new IllegalStateException("Must have at least one header with a fragment"); } /** * Called after the header list has been updated ({@link #onBuildHeaders} has been called and * returned due to {@link #invalidateHeaders()}) to * specify the header that should now be selected. The default implementation * returns null to keep whatever header is currently selected. */ public Header onGetNewHeader() { return null; } /** * Called when the activity needs its list of headers build. By * implementing this and adding at least one item to the list, you * will cause the activity to run in its modern fragment mode. Note * that this function may not always be called; for example, if the * activity has been asked to display a particular fragment without * the header list, there is no need to build the headers. * <p> * Typical implementations will use {@link #loadHeadersFromResource} to fill in the list from a * resource. * * @param target * The list in which to place the headers. */ public void onBuildHeaders(List<Header> target) { // Should be overloaded by subclasses } /** * Call when you need to change the headers being displayed. Will result * in onBuildHeaders() later being called to retrieve the new list. */ public void invalidateHeaders() { if (!mHandler.hasMessages(MSG_BUILD_HEADERS)) { mHandler.sendEmptyMessage(MSG_BUILD_HEADERS); } } /** * Parse the given XML file as a header description, adding each * parsed Header into the target list. * * @param resid * The XML resource to load and parse. * @param target * The list in which the parsed headers should be placed. */ public void loadHeadersFromResource(int resid, List<Header> target) { XmlResourceParser parser = null; try { parser = getResources().getXml(resid); AttributeSet attrs = Xml.asAttributeSet(parser); int type; while ((type = parser.next()) != XmlPullParser.END_DOCUMENT && type != XmlPullParser.START_TAG) { // Parse next until start tag is found } String nodeName = parser.getName(); if (!"preference-headers".equals(nodeName)) { throw new RuntimeException("XML document must start with <preference-headers> tag; found" + nodeName + " at " + parser.getPositionDescription()); } Bundle curBundle = null; final int outerDepth = parser.getDepth(); while ((type = parser.next()) != XmlPullParser.END_DOCUMENT && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { continue; } nodeName = parser.getName(); if ("header".equals(nodeName)) { Header header = new Header(); TypedArray sa = getResources().obtainAttributes(attrs, R.styleable.PreferenceHeader); header.id = sa.getResourceId(R.styleable.PreferenceHeader_id, (int) HEADER_ID_UNDEFINED); TypedValue tv = sa.peekValue(R.styleable.PreferenceHeader_title); if (tv != null && tv.type == TypedValue.TYPE_STRING) { if (tv.resourceId != 0) { header.titleRes = tv.resourceId; } else { header.title = tv.string; } } tv = sa.peekValue(R.styleable.PreferenceHeader_summary); if (tv != null && tv.type == TypedValue.TYPE_STRING) { if (tv.resourceId != 0) { header.summaryRes = tv.resourceId; } else { header.summary = tv.string; } } tv = sa.peekValue(R.styleable.PreferenceHeader_breadCrumbTitle); if (tv != null && tv.type == TypedValue.TYPE_STRING) { if (tv.resourceId != 0) { header.breadCrumbTitleRes = tv.resourceId; } else { header.breadCrumbTitle = tv.string; } } tv = sa.peekValue(R.styleable.PreferenceHeader_breadCrumbShortTitle); if (tv != null && tv.type == TypedValue.TYPE_STRING) { if (tv.resourceId != 0) { header.breadCrumbShortTitleRes = tv.resourceId; } else { header.breadCrumbShortTitle = tv.string; } } header.iconRes = sa.getResourceId(R.styleable.PreferenceHeader_icon, 0); header.fragment = sa.getString(R.styleable.PreferenceHeader_fragment); sa.recycle(); if (curBundle == null) { curBundle = new Bundle(); } final int innerDepth = parser.getDepth(); while ((type = parser.next()) != XmlPullParser.END_DOCUMENT && (type != XmlPullParser.END_TAG || parser.getDepth() > innerDepth)) { if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { continue; } String innerNodeName = parser.getName(); if (innerNodeName.equals("extra")) { getResources().parseBundleExtra("extra", attrs, curBundle); XmlUtils.skipCurrentTag(parser); } else if (innerNodeName.equals("intent")) { header.intent = Intent.parseIntent(getResources(), parser, attrs); } else { XmlUtils.skipCurrentTag(parser); } } if (curBundle.size() > 0) { header.fragmentArguments = curBundle; curBundle = null; } target.add(header); } else { XmlUtils.skipCurrentTag(parser); } } } catch (XmlPullParserException e) { throw new RuntimeException("Error parsing headers", e); } catch (IOException e) { throw new RuntimeException("Error parsing headers", e); } finally { if (parser != null) parser.close(); } } /** * Subclasses should override this method and verify that the given fragment is a valid type * to be attached to this activity. The default implementation returns <code>true</code> for * apps built for <code>android:targetSdkVersion</code> older than * {@link android.os.Build.VERSION_CODES#KITKAT}. For later versions, it will throw an * exception. * * @param fragmentName * the class name of the Fragment about to be attached to this activity. * @return true if the fragment class name is valid for this Activity and false otherwise. */ protected boolean isValidFragment(String fragmentName) { // if (getApplicationInfo().targetSdkVersion >= android.os.Build.VERSION_CODES.KITKAT) { // throw new RuntimeException( // "Subclasses of PreferenceActivity must override isValidFragment(String)" // + " to verify that the Fragment class is valid! " + this.getClass().getName() // + " has not checked if fragment " + fragmentName + " is valid."); // } else { return true; // } } /** * Set a footer that should be shown at the bottom of the header list. */ public void setListFooter(View view) { mListFooter.removeAllViews(); mListFooter.addView(view, new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT)); } @Override protected void onStop() { super.onStop(); if (mPreferenceManager != null) { mPreferenceManager.dispatchActivityStop(); } } @Override protected void onDestroy() { mHandler.removeMessages(MSG_BIND_PREFERENCES); mHandler.removeMessages(MSG_BUILD_HEADERS); super.onDestroy(); if (mPreferenceManager != null) { mPreferenceManager.dispatchActivityDestroy(); } } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); if (mHeaders.size() > 0) { outState.putParcelableArrayList(HEADERS_TAG, mHeaders); if (mCurHeader != null) { int index = mHeaders.indexOf(mCurHeader); if (index >= 0) { outState.putInt(CUR_HEADER_TAG, index); } } } if (mPreferenceManager != null) { final PreferenceScreen preferenceScreen = getPreferenceScreen(); if (preferenceScreen != null) { Bundle container = new Bundle(); preferenceScreen.saveHierarchyState(container); outState.putBundle(PREFERENCES_TAG, container); } } } @Override protected void onRestoreInstanceState(Bundle state) { if (mPreferenceManager != null) { Bundle container = state.getBundle(PREFERENCES_TAG); if (container != null) { final PreferenceScreen preferenceScreen = getPreferenceScreen(); if (preferenceScreen != null) { preferenceScreen.restoreHierarchyState(container); mSavedInstanceState = state; return; } } } // Only call this if we didn't save the instance state for later. // If we did save it, it will be restored when we bind the adapter. super.onRestoreInstanceState(state); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (mPreferenceManager != null) { mPreferenceManager.dispatchActivityResult(requestCode, resultCode, data); } } @Override public void onSupportContentChanged() { super.onSupportContentChanged(); if (mPreferenceManager != null) { postBindPreferences(); } } @Override protected void onListItemClick(ListView l, View v, int position, long id) { // if (!isResumed()) { // return; // } super.onListItemClick(l, v, position, id); if (mAdapter != null) { Object item = mAdapter.getItem(position); if (item instanceof Header) onHeaderClick((Header) item, position); } } /** * Called when the user selects an item in the header list. The default * implementation will call either * {@link #startWithFragment(String, Bundle, Fragment, int, int, int)} or * {@link #switchToHeader(Header)} as appropriate. * * @param header * The header that was selected. * @param position * The header's position in the list. */ public void onHeaderClick(Header header, int position) { if (header.fragment != null) { if (mSinglePane) { int titleRes = header.breadCrumbTitleRes; int shortTitleRes = header.breadCrumbShortTitleRes; if (titleRes == 0) { titleRes = header.titleRes; shortTitleRes = 0; } startWithFragment(header.fragment, header.fragmentArguments, null, 0, titleRes, shortTitleRes); } else { switchToHeader(header); } } else if (header.intent != null) { startActivity(header.intent); } } /** * Called by {@link #startWithFragment(String, Bundle, Fragment, int, int, int)} when * in single-pane mode, to build an Intent to launch a new activity showing * the selected fragment. The default implementation constructs an Intent * that re-launches the current activity with the appropriate arguments to * display the fragment. * * @param fragmentName * The name of the fragment to display. * @param args * Optional arguments to supply to the fragment. * @param titleRes * Optional resource ID of title to show for this item. * @param shortTitleRes * Optional resource ID of short title to show for this item. * @return Returns an Intent that can be launched to display the given * fragment. */ public Intent onBuildStartFragmentIntent(String fragmentName, Bundle args, int titleRes, int shortTitleRes) { Intent intent = new Intent(Intent.ACTION_MAIN); intent.setClass(this, getClass()); intent.putExtra(EXTRA_SHOW_FRAGMENT, fragmentName); intent.putExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS, args); intent.putExtra(EXTRA_SHOW_FRAGMENT_TITLE, titleRes); intent.putExtra(EXTRA_SHOW_FRAGMENT_SHORT_TITLE, shortTitleRes); intent.putExtra(EXTRA_NO_HEADERS, true); return intent; } /** * Like {@link #startWithFragment(String, Bundle, Fragment, int, int, int)} but uses a 0 * titleRes. */ public void startWithFragment(String fragmentName, Bundle args, Fragment resultTo, int resultRequestCode) { startWithFragment(fragmentName, args, resultTo, resultRequestCode, 0, 0); } /** * Start a new instance of this activity, showing only the given * preference fragment. When launched in this mode, the header list * will be hidden and the given preference fragment will be instantiated * and fill the entire activity. * * @param fragmentName * The name of the fragment to display. * @param args * Optional arguments to supply to the fragment. * @param resultTo * Option fragment that should receive the result of * the activity launch. * @param resultRequestCode * If resultTo is non-null, this is the request * code in which to report the result. * @param titleRes * Resource ID of string to display for the title of * this set of preferences. * @param shortTitleRes * Resource ID of string to display for the short title of * this set of preferences. */ public void startWithFragment(String fragmentName, Bundle args, Fragment resultTo, int resultRequestCode, int titleRes, int shortTitleRes) { Intent intent = onBuildStartFragmentIntent(fragmentName, args, titleRes, shortTitleRes); if (resultTo == null) { startActivity(intent); } else { resultTo.startActivityForResult(intent, resultRequestCode); } } /** * Change the base title of the bread crumbs for the current preferences. * This will normally be called for you. See {@link android.app.FragmentBreadCrumbs} for more * information. */ public void showBreadCrumbs(CharSequence title, CharSequence shortTitle) { // if (mFragmentBreadCrumbs == null) { // View crumbs = findViewById(android.R.id.title); // // For screens with a different kind of title, don't create breadcrumbs. // try { // mFragmentBreadCrumbs = (FragmentBreadCrumbs)crumbs; // } catch (ClassCastException e) { // setTitle(title); // return; // } // if (mFragmentBreadCrumbs == null) { // if (title != null) { // setTitle(title); // } // return; // } // if (mSinglePane) { // mFragmentBreadCrumbs.setVisibility(View.GONE); // // Hide the breadcrumb section completely for single-pane // // View bcSection = findViewById(R.id.breadcrumb_section); // // if (bcSection != null) bcSection.setVisibility(View.GONE); // setTitle(title); // } // mFragmentBreadCrumbs.setMaxVisible(2); // mFragmentBreadCrumbs.setActivity(this); // } // if (mFragmentBreadCrumbs.getVisibility() != View.VISIBLE) { setTitle(title); // } else { // mFragmentBreadCrumbs.setTitle(title, shortTitle); // mFragmentBreadCrumbs.setParentTitle(null, null, null); // } } /** * Should be called after onCreate to ensure that the breadcrumbs, if any, were created. * This prepends a title to the fragment breadcrumbs and attaches a listener to any clicks * on the parent entry. * * @param title * the title for the breadcrumb * @param shortTitle * the short title for the breadcrumb */ public void setParentTitle(CharSequence title, CharSequence shortTitle, OnClickListener listener) { if (mFragmentBreadCrumbs != null) { mFragmentBreadCrumbs.setParentTitle(title, shortTitle, listener); } } void setSelectedHeader(Header header) { mCurHeader = header; int index = mHeaders.indexOf(header); if (index >= 0) { getListView().setItemChecked(index, true); } else { getListView().clearChoices(); } showBreadCrumbs(header); } void showBreadCrumbs(Header header) { if (header != null) { CharSequence title = header.getBreadCrumbTitle(getResources()); if (title == null) title = header.getTitle(getResources()); if (title == null) title = getTitle(); showBreadCrumbs(title, header.getBreadCrumbShortTitle(getResources())); } else { showBreadCrumbs(getTitle(), null); } } private void switchToHeaderInner(String fragmentName, Bundle args, int direction) { getSupportFragmentManager().popBackStack(BACK_STACK_PREFS, FragmentManager.POP_BACK_STACK_INCLUSIVE); if (!isValidFragment(fragmentName)) { throw new IllegalArgumentException("Invalid fragment for this activity: " + fragmentName); } Fragment f = Fragment.instantiate(this, fragmentName, args); FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); transaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE); transaction.replace(R.id.prefs, f); transaction.commitAllowingStateLoss(); } /** * When in two-pane mode, switch the fragment pane to show the given * preference fragment. * * @param fragmentName * The name of the fragment to display. * @param args * Optional arguments to supply to the fragment. */ public void switchToHeader(String fragmentName, Bundle args) { setSelectedHeader(null); switchToHeaderInner(fragmentName, args, 0); } /** * When in two-pane mode, switch to the fragment pane to show the given * preference fragment. * * @param header * The new header to display. */ public void switchToHeader(Header header) { if (mCurHeader == header) { // This is the header we are currently displaying. Just make sure // to pop the stack up to its root state. getSupportFragmentManager().popBackStack(BACK_STACK_PREFS, FragmentManager.POP_BACK_STACK_INCLUSIVE); } else { if (header.fragment == null) { throw new IllegalStateException("can't switch to header that has no fragment"); } int direction = mHeaders.indexOf(header) - mHeaders.indexOf(mCurHeader); switchToHeaderInner(header.fragment, header.fragmentArguments, direction); setSelectedHeader(header); } } Header findBestMatchingHeader(Header cur, ArrayList<Header> from) { ArrayList<Header> matches = new ArrayList<Header>(); for (int j = 0; j < from.size(); j++) { Header oh = from.get(j); if (cur == oh || (cur.id != HEADER_ID_UNDEFINED && cur.id == oh.id)) { // Must be this one. matches.clear(); matches.add(oh); break; } if (cur.fragment != null) { if (cur.fragment.equals(oh.fragment)) { matches.add(oh); } } else if (cur.intent != null) { if (cur.intent.equals(oh.intent)) { matches.add(oh); } } else if (cur.title != null) { if (cur.title.equals(oh.title)) { matches.add(oh); } } } final int NM = matches.size(); if (NM == 1) { return matches.get(0); } else if (NM > 1) { for (int j = 0; j < NM; j++) { Header oh = matches.get(j); if (cur.fragmentArguments != null && cur.fragmentArguments.equals(oh.fragmentArguments)) { return oh; } if (cur.extras != null && cur.extras.equals(oh.extras)) { return oh; } if (cur.title != null && cur.title.equals(oh.title)) { return oh; } } } return null; } /** * Start a new fragment. * * @param fragment * The fragment to start * @param push * If true, the current fragment will be pushed onto the back stack. If false, * the current fragment will be replaced. */ public void startPreferenceFragment(Fragment fragment, boolean push) { FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); transaction.replace(R.id.prefs, fragment); if (push) { transaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN); transaction.addToBackStack(BACK_STACK_PREFS); } else { transaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE); } transaction.commitAllowingStateLoss(); } /** * Start a new fragment containing a preference panel. If the preferences * are being displayed in multi-pane mode, the given fragment class will * be instantiated and placed in the appropriate pane. If running in * single-pane mode, a new activity will be launched in which to show the * fragment. * * @param fragmentClass * Full name of the class implementing the fragment. * @param args * Any desired arguments to supply to the fragment. * @param titleRes * Optional resource identifier of the title of this * fragment. * @param titleText * Optional text of the title of this fragment. * @param resultTo * Optional fragment that result data should be sent to. * If non-null, resultTo.onActivityResult() will be called when this * preference panel is done. The launched panel must use * {@link #finishPreferencePanel(Fragment, int, Intent)} when done. * @param resultRequestCode * If resultTo is non-null, this is the caller's * request code to be received with the resut. */ public void startPreferencePanel(String fragmentClass, Bundle args, int titleRes, CharSequence titleText, Fragment resultTo, int resultRequestCode) { if (mSinglePane) { startWithFragment(fragmentClass, args, resultTo, resultRequestCode, titleRes, 0); } else { Fragment f = Fragment.instantiate(this, fragmentClass, args); if (resultTo != null) { f.setTargetFragment(resultTo, resultRequestCode); } FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); transaction.replace(R.id.prefs, f); if (titleRes != 0) { transaction.setBreadCrumbTitle(titleRes); } else if (titleText != null) { transaction.setBreadCrumbTitle(titleText); } transaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN); transaction.addToBackStack(BACK_STACK_PREFS); transaction.commitAllowingStateLoss(); } } /** * Called by a preference panel fragment to finish itself. * * @param caller * The fragment that is asking to be finished. * @param resultCode * Optional result code to send back to the original * launching fragment. * @param resultData * Optional result data to send back to the original * launching fragment. */ public void finishPreferencePanel(Fragment caller, int resultCode, Intent resultData) { if (mSinglePane) { setResult(resultCode, resultData); finish(); } else { // XXX be smarter about popping the stack. onBackPressed(); if (caller != null) { if (caller.getTargetFragment() != null) { caller.getTargetFragment().onActivityResult(caller.getTargetRequestCode(), resultCode, resultData); } } } } @Override public boolean onPreferenceStartFragment(PreferenceFragment caller, Preference pref) { startPreferencePanel(pref.getFragment(), pref.getExtras(), pref.getTitleRes(), pref.getTitle(), null, 0); return true; } /** * Posts a message to bind the preferences to the list view. * <p> * Binding late is preferred as any custom preference types created in {@link #onCreate(Bundle)} * are able to have their views recycled. */ private void postBindPreferences() { if (mHandler.hasMessages(MSG_BIND_PREFERENCES)) return; mHandler.obtainMessage(MSG_BIND_PREFERENCES).sendToTarget(); } private void bindPreferences() { final PreferenceScreen preferenceScreen = getPreferenceScreen(); if (preferenceScreen != null) { preferenceScreen.bind(getListView()); if (mSavedInstanceState != null) { super.onRestoreInstanceState(mSavedInstanceState); mSavedInstanceState = null; } } } /** * Returns the {@link PreferenceManager} used by this activity. * * @return The {@link PreferenceManager}. * @deprecated This function is not relevant for a modern fragment-based * PreferenceActivity. */ @Deprecated public PreferenceManager getPreferenceManager() { return mPreferenceManager; } private void requirePreferenceManager() { if (mPreferenceManager == null) { if (mAdapter == null) { throw new RuntimeException("This should be called after super.onCreate."); } throw new RuntimeException("Modern two-pane PreferenceActivity requires use of a PreferenceFragment"); } } /** * Sets the root of the preference hierarchy that this activity is showing. * * @param preferenceScreen * The root {@link PreferenceScreen} of the preference hierarchy. * @deprecated This function is not relevant for a modern fragment-based * PreferenceActivity. */ @Deprecated public void setPreferenceScreen(PreferenceScreen preferenceScreen) { requirePreferenceManager(); if (mPreferenceManager.setPreferences(preferenceScreen) && preferenceScreen != null) { postBindPreferences(); CharSequence title = getPreferenceScreen().getTitle(); // Set the title of the activity if (title != null) { setTitle(title); } } } /** * Gets the root of the preference hierarchy that this activity is showing. * * @return The {@link PreferenceScreen} that is the root of the preference * hierarchy. * @deprecated This function is not relevant for a modern fragment-based * PreferenceActivity. */ @Deprecated public PreferenceScreen getPreferenceScreen() { if (mPreferenceManager != null) { return mPreferenceManager.getPreferenceScreen(); } return null; } /** * Adds preferences from activities that match the given {@link Intent}. * * @param intent * The {@link Intent} to query activities. * @deprecated This function is not relevant for a modern fragment-based * PreferenceActivity. */ @Deprecated public void addPreferencesFromIntent(Intent intent) { requirePreferenceManager(); setPreferenceScreen(mPreferenceManager.inflateFromIntent(intent, getPreferenceScreen())); } /** * Inflates the given XML resource and adds the preference hierarchy to the current * preference hierarchy. * * @param preferencesResId * The XML resource ID to inflate. * @deprecated This function is not relevant for a modern fragment-based * PreferenceActivity. */ @Deprecated public void addPreferencesFromResource(int preferencesResId) { requirePreferenceManager(); setPreferenceScreen(mPreferenceManager.inflateFromResource(this, preferencesResId, getPreferenceScreen())); } /** * {@inheritDoc} * * @deprecated This function is not relevant for a modern fragment-based * PreferenceActivity. */ @Deprecated public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference) { return false; } /** * Finds a {@link Preference} based on its key. * * @param key * The key of the preference to retrieve. * @return The {@link Preference} with the key, or null. * @see PreferenceGroup#findPreference(CharSequence) * @deprecated This function is not relevant for a modern fragment-based * PreferenceActivity. */ @Deprecated public Preference findPreference(CharSequence key) { if (mPreferenceManager == null) { return null; } return mPreferenceManager.findPreference(key); } @Override protected void onNewIntent(Intent intent) { if (mPreferenceManager != null) { mPreferenceManager.dispatchNewIntent(intent); } } // give subclasses access to the Next button /** @hide */ protected boolean hasNextButton() { return mNextButton != null; } /** @hide */ protected Button getNextButton() { return mNextButton; } }