/* * 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. */ package android.preference; import com.android.internal.util.XmlUtils; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import android.app.Fragment; import android.app.ListActivity; import android.content.Context; import android.content.Intent; import android.content.res.Configuration; import android.content.res.TypedArray; import android.content.res.XmlResourceParser; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.text.TextUtils; import android.util.AttributeSet; import android.util.Log; import android.util.Xml; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.View.OnClickListener; import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.ImageView; import android.widget.ListView; import android.widget.TextView; import java.io.IOException; import java.util.ArrayList; import java.util.List; /** * 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. * *

This activity shows one or more headers of preferences, each of with * 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: * *

* *

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. * * *

Sample Code

* *

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:

* * {@sample development/samples/ApiDemos/src/com/example/android/apis/preference/PreferenceWithHeaders.java * activity} * *

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} * *

The first header is shown by Prefs1Fragment, which populates itself * from the following XML resource:

* * {@sample development/samples/ApiDemos/res/xml/fragmented_preferences.xml preferences} * *

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. * *

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"; 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. */ 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 specify 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, 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

mHeaders = new ArrayList
(); private HeaderAdapter mAdapter; private View mPrefsContainer; private boolean mSinglePane; // --- 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 = 0; private static final int MSG_BUILD_HEADERS = 1; private Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { case MSG_BIND_PREFERENCES: bindPreferences(); break; case MSG_BUILD_HEADERS: onBuildHeaders(mHeaders); mAdapter.notifyDataSetChanged(); break; } } }; private class HeaderViewHolder { ImageView icon; TextView title; TextView summary; } private class HeaderAdapter extends ArrayAdapter
{ private LayoutInflater mInflater; public HeaderAdapter(Context context, List
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(com.android.internal.R.layout.preference_list_item, parent, false); holder = new HeaderViewHolder(); holder.icon = (ImageView)view.findViewById( com.android.internal.R.id.icon); holder.title = (TextView)view.findViewById( com.android.internal.R.id.title); holder.summary = (TextView)view.findViewById( com.android.internal.R.id.summary); view.setTag(holder); } else { view = convertView; holder = (HeaderViewHolder)view.getTag(); } Header header = getItem(position); if (header.icon != null) holder.icon.setImageDrawable(header.icon); else if (header.iconRes != 0) holder.icon.setImageResource(header.iconRes); if (header.title != null) holder.title.setText(header.title); if (header.summary != null) holder.summary.setText(header.summary); return view; } } /** * Description of a single Header item that the user can select. */ public static class Header { /** * Title of the header that is shown to the user. * @attr ref android.R.styleable#PreferenceHeader_title */ CharSequence title; /** * Optional summary describing what this header controls. * @attr ref android.R.styleable#PreferenceHeader_summary */ CharSequence summary; /** * Optional icon resource to show for this header. * @attr ref android.R.styleable#PreferenceHeader_icon */ int iconRes; /** * Optional icon drawable to show for this header. (If this is non-null, * the iconRes will be ignored.) */ Drawable icon; /** * Full class name of the fragment to display when this header is * selected. * @attr ref android.R.styleable#PreferenceHeader_fragment */ String fragment; /** * Optional arguments to supply to the fragment when it is * instantiated. */ Bundle fragmentArguments; } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(com.android.internal.R.layout.preference_list_content); mPrefsContainer = findViewById(com.android.internal.R.id.prefs); boolean hidingHeaders = onIsHidingHeaders(); mSinglePane = hidingHeaders || !onIsMultiPane(); String initialFragment = getIntent().getStringExtra(EXTRA_SHOW_FRAGMENT); Bundle initialArguments = getIntent().getBundleExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS); 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. getListView().setVisibility(View.GONE); mPrefsContainer.setVisibility(View.VISIBLE); switchToHeader(initialFragment, initialArguments); } 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) { mAdapter = new HeaderAdapter(this, mHeaders); setListAdapter(mAdapter); if (!mSinglePane) { mPrefsContainer.setVisibility(View.VISIBLE); if (initialFragment != null) { Header h = onGetInitialHeader(); initialFragment = h.fragment; initialArguments = h.fragmentArguments; } switchToHeader(initialFragment, initialArguments); } // If there are no headers, we are in the old "just show a screen // of preferences" mode. } else { mPreferenceManager = new PreferenceManager(this, FIRST_REQUEST_CODE); mPreferenceManager.setOnPreferenceTreeClickListener(this); } } getListView().setScrollBarStyle(View.SCROLLBARS_INSIDE_OVERLAY); // see if we should show Back/Next buttons Intent intent = getIntent(); if (intent.getBooleanExtra(EXTRA_PREFS_SHOW_BUTTON_BAR, false)) { findViewById(com.android.internal.R.id.button_bar).setVisibility(View.VISIBLE); Button backButton = (Button)findViewById(com.android.internal.R.id.back_button); backButton.setOnClickListener(new OnClickListener() { public void onClick(View v) { setResult(RESULT_CANCELED); finish(); } }); Button skipButton = (Button)findViewById(com.android.internal.R.id.skip_button); skipButton.setOnClickListener(new OnClickListener() { public void onClick(View v) { setResult(RESULT_OK); finish(); } }); mNextButton = (Button)findViewById(com.android.internal.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); } } } /** * 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() { Configuration config = getResources().getConfiguration(); if ((config.screenLayout&Configuration.SCREENLAYOUT_SIZE_MASK) == Configuration.SCREENLAYOUT_SIZE_XLARGE && config.orientation == Configuration.ORIENTATION_LANDSCAPE) { return true; } return false; } /** * Called to determine whether the header list should be hidden. The * default implementation hides the list if the activity is being re-launched * when not in multi-pane mode. */ 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() { return mHeaders.get(0); } /** * 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. * *

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

target) { } /** * 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
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) { } String nodeName = parser.getName(); if (!"PreferenceHeaders".equals(nodeName)) { throw new RuntimeException( "XML document must start with tag; found" + nodeName + " at " + parser.getPositionDescription()); } 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, com.android.internal.R.styleable.PreferenceHeader); header.title = sa.getText( com.android.internal.R.styleable.PreferenceHeader_title); header.summary = sa.getText( com.android.internal.R.styleable.PreferenceHeader_summary); header.iconRes = sa.getResourceId( com.android.internal.R.styleable.PreferenceHeader_icon, 0); header.fragment = sa.getString( com.android.internal.R.styleable.PreferenceHeader_fragment); sa.recycle(); target.add(header); XmlUtils.skipCurrentTag(parser); } 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(); } } @Override protected void onStop() { super.onStop(); if (mPreferenceManager != null) { mPreferenceManager.dispatchActivityStop(); } } @Override protected void onDestroy() { super.onDestroy(); if (mPreferenceManager != null) { mPreferenceManager.dispatchActivityDestroy(); } } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); 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 onContentChanged() { super.onContentChanged(); if (mPreferenceManager != null) { postBindPreferences(); } } @Override protected void onListItemClick(ListView l, View v, int position, long id) { super.onListItemClick(l, v, position, id); if (mAdapter != null) { onHeaderClick(mHeaders.get(position), position); } } /** * Called when the user selects an item in the header list. The default * implementation will call either {@link #startWithFragment(String, Bundle)} * or {@link #switchToHeader(String, Bundle)} 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 (mSinglePane) { startWithFragment(header.fragment, header.fragmentArguments); } else { switchToHeader(header.fragment, header.fragmentArguments); } } /** * 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. */ public void startWithFragment(String fragmentName, Bundle args) { 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_NO_HEADERS, true); startActivity(intent); } /** * 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) { popBackStack(BACK_STACK_PREFS, POP_BACK_STACK_INCLUSIVE); Fragment f = Fragment.instantiate(this, fragmentName, args); openFragmentTransaction().replace(com.android.internal.R.id.prefs, f).commit(); } @Override public boolean onPreferenceStartFragment(PreferenceFragment caller, Preference pref) { Fragment f = Fragment.instantiate(this, pref.getFragment()); openFragmentTransaction().replace(com.android.internal.R.id.prefs, f) .addToBackStack(BACK_STACK_PREFS).commit(); return true; } /** * Posts a message to bind the preferences to the list view. *

* 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; } }