diff options
Diffstat (limited to 'core/java/android/preference')
18 files changed, 5915 insertions, 0 deletions
diff --git a/core/java/android/preference/CheckBoxPreference.java b/core/java/android/preference/CheckBoxPreference.java new file mode 100644 index 0000000..4bebf87 --- /dev/null +++ b/core/java/android/preference/CheckBoxPreference.java @@ -0,0 +1,295 @@ +/* + * 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 android.content.Context; +import android.content.SharedPreferences; +import android.content.res.TypedArray; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.AttributeSet; +import android.view.View; +import android.widget.Checkable; +import android.widget.TextView; + +/** + * The {@link CheckBoxPreference} is a preference that provides checkbox widget + * functionality. + * <p> + * This preference will store a boolean into the SharedPreferences. + * + * @attr ref android.R.styleable#CheckBoxPreference_summaryOff + * @attr ref android.R.styleable#CheckBoxPreference_summaryOn + * @attr ref android.R.styleable#CheckBoxPreference_disableDependentsState + */ +public class CheckBoxPreference extends Preference { + + private CharSequence mSummaryOn; + private CharSequence mSummaryOff; + + private boolean mChecked; + + private boolean mDisableDependentsState; + + public CheckBoxPreference(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + TypedArray a = context.obtainStyledAttributes(attrs, + com.android.internal.R.styleable.CheckBoxPreference, defStyle, 0); + mSummaryOn = a.getString(com.android.internal.R.styleable.CheckBoxPreference_summaryOn); + mSummaryOff = a.getString(com.android.internal.R.styleable.CheckBoxPreference_summaryOff); + mDisableDependentsState = a.getBoolean( + com.android.internal.R.styleable.CheckBoxPreference_disableDependentsState, false); + a.recycle(); + } + + public CheckBoxPreference(Context context, AttributeSet attrs) { + this(context, attrs, com.android.internal.R.attr.checkBoxPreferenceStyle); + } + + public CheckBoxPreference(Context context) { + this(context, null); + } + + @Override + protected void onBindView(View view) { + super.onBindView(view); + + View checkboxView = view.findViewById(com.android.internal.R.id.checkbox); + if (checkboxView != null && checkboxView instanceof Checkable) { + ((Checkable) checkboxView).setChecked(mChecked); + } + + // Sync the summary view + TextView summaryView = (TextView) view.findViewById(com.android.internal.R.id.summary); + if (summaryView != null) { + boolean useDefaultSummary = true; + if (mChecked && mSummaryOn != null) { + summaryView.setText(mSummaryOn); + useDefaultSummary = false; + } else if (!mChecked && mSummaryOff != null) { + summaryView.setText(mSummaryOff); + useDefaultSummary = false; + } + + if (useDefaultSummary) { + final CharSequence summary = getSummary(); + if (summary != null) { + summaryView.setText(summary); + useDefaultSummary = false; + } + } + + int newVisibility = View.GONE; + if (!useDefaultSummary) { + // Someone has written to it + newVisibility = View.VISIBLE; + } + if (newVisibility != summaryView.getVisibility()) { + summaryView.setVisibility(newVisibility); + } + } + } + + @Override + protected void onClick() { + super.onClick(); + + boolean newValue = !isChecked(); + + if (!callChangeListener(newValue)) { + return; + } + + setChecked(newValue); + } + + /** + * Sets the checked state and saves it to the {@link SharedPreferences}. + * + * @param checked The checked state. + */ + public void setChecked(boolean checked) { + mChecked = checked; + + persistBoolean(checked); + + notifyDependencyChange(shouldDisableDependents()); + + notifyChanged(); + } + + /** + * Returns the checked state. + * + * @return The checked state. + */ + public boolean isChecked() { + return mChecked; + } + + @Override + public boolean shouldDisableDependents() { + boolean shouldDisable = mDisableDependentsState ? mChecked : !mChecked; + return shouldDisable || super.shouldDisableDependents(); + } + + /** + * Sets the summary to be shown when checked. + * + * @param summary The summary to be shown when checked. + */ + public void setSummaryOn(CharSequence summary) { + mSummaryOn = summary; + if (isChecked()) { + notifyChanged(); + } + } + + /** + * @see #setSummaryOn(CharSequence) + * @param summaryResId The summary as a resource. + */ + public void setSummaryOn(int summaryResId) { + setSummaryOn(getContext().getString(summaryResId)); + } + + /** + * Returns the summary to be shown when checked. + * @return The summary. + */ + public CharSequence getSummaryOn() { + return mSummaryOn; + } + + /** + * Sets the summary to be shown when unchecked. + * + * @param summary The summary to be shown when unchecked. + */ + public void setSummaryOff(CharSequence summary) { + mSummaryOff = summary; + if (!isChecked()) { + notifyChanged(); + } + } + + /** + * @see #setSummaryOff(CharSequence) + * @param summaryResId The summary as a resource. + */ + public void setSummaryOff(int summaryResId) { + setSummaryOff(getContext().getString(summaryResId)); + } + + /** + * Returns the summary to be shown when unchecked. + * @return The summary. + */ + public CharSequence getSummaryOff() { + return mSummaryOff; + } + + /** + * Returns whether dependents are disabled when this preference is on ({@code true}) + * or when this preference is off ({@code false}). + * + * @return Whether dependents are disabled when this preference is on ({@code true}) + * or when this preference is off ({@code false}). + */ + public boolean getDisableDependentsState() { + return mDisableDependentsState; + } + + /** + * Sets whether dependents are disabled when this preference is on ({@code true}) + * or when this preference is off ({@code false}). + * + * @param disableDependentsState The preference state that should disable dependents. + */ + public void setDisableDependentsState(boolean disableDependentsState) { + mDisableDependentsState = disableDependentsState; + } + + @Override + protected Object onGetDefaultValue(TypedArray a, int index) { + return a.getBoolean(index, false); + } + + @Override + protected void onSetInitialValue(boolean restoreValue, Object defaultValue) { + setChecked(restoreValue ? getPersistedBoolean(mChecked) + : (Boolean) defaultValue); + } + + @Override + protected Parcelable onSaveInstanceState() { + final Parcelable superState = super.onSaveInstanceState(); + if (isPersistent()) { + // No need to save instance state since it's persistent + return superState; + } + + final SavedState myState = new SavedState(superState); + myState.checked = isChecked(); + return myState; + } + + @Override + protected void onRestoreInstanceState(Parcelable state) { + if (state == null || !state.getClass().equals(SavedState.class)) { + // Didn't save state for us in onSaveInstanceState + super.onRestoreInstanceState(state); + return; + } + + SavedState myState = (SavedState) state; + super.onRestoreInstanceState(myState.getSuperState()); + setChecked(myState.checked); + } + + private static class SavedState extends BaseSavedState { + boolean checked; + + public SavedState(Parcel source) { + super(source); + checked = source.readInt() == 1; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeInt(checked ? 1 : 0); + } + + public SavedState(Parcelable superState) { + super(superState); + } + + public static final Parcelable.Creator<SavedState> CREATOR = + new Parcelable.Creator<SavedState>() { + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } + +} diff --git a/core/java/android/preference/DialogPreference.java b/core/java/android/preference/DialogPreference.java new file mode 100644 index 0000000..3eb65e2 --- /dev/null +++ b/core/java/android/preference/DialogPreference.java @@ -0,0 +1,445 @@ +/* + * 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 android.app.AlertDialog; +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.SharedPreferences; +import android.content.res.TypedArray; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.TextView; + +/** + * The {@link DialogPreference} class is a base class for preferences that are + * dialog-based. These preferences will, when clicked, open a dialog showing the + * actual preference controls. + * + * @attr ref android.R.styleable#DialogPreference_dialogTitle + * @attr ref android.R.styleable#DialogPreference_dialogMessage + * @attr ref android.R.styleable#DialogPreference_dialogIcon + * @attr ref android.R.styleable#DialogPreference_dialogLayout + * @attr ref android.R.styleable#DialogPreference_positiveButtonText + * @attr ref android.R.styleable#DialogPreference_negativeButtonText + */ +public abstract class DialogPreference extends Preference implements + DialogInterface.OnClickListener, DialogInterface.OnDismissListener, + PreferenceManager.OnActivityDestroyListener { + private AlertDialog.Builder mBuilder; + + private CharSequence mDialogTitle; + private CharSequence mDialogMessage; + private Drawable mDialogIcon; + private CharSequence mPositiveButtonText; + private CharSequence mNegativeButtonText; + private int mDialogLayoutResId; + + /** The dialog, if it is showing. */ + private Dialog mDialog; + + /** Which button was clicked. */ + private int mWhichButtonClicked; + + public DialogPreference(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + TypedArray a = context.obtainStyledAttributes(attrs, + com.android.internal.R.styleable.DialogPreference, defStyle, 0); + mDialogTitle = a.getString(com.android.internal.R.styleable.DialogPreference_dialogTitle); + if (mDialogTitle == null) { + // Fallback on the regular title of the preference + // (the one that is seen in the list) + mDialogTitle = getTitle(); + } + mDialogMessage = a.getString(com.android.internal.R.styleable.DialogPreference_dialogMessage); + mDialogIcon = a.getDrawable(com.android.internal.R.styleable.DialogPreference_dialogIcon); + mPositiveButtonText = a.getString(com.android.internal.R.styleable.DialogPreference_positiveButtonText); + mNegativeButtonText = a.getString(com.android.internal.R.styleable.DialogPreference_negativeButtonText); + mDialogLayoutResId = a.getResourceId(com.android.internal.R.styleable.DialogPreference_dialogLayout, + mDialogLayoutResId); + a.recycle(); + + } + + public DialogPreference(Context context, AttributeSet attrs) { + this(context, attrs, com.android.internal.R.attr.dialogPreferenceStyle); + } + + /** + * Sets the title of the dialog. This will be shown on subsequent dialogs. + * + * @param dialogTitle The title. + */ + public void setDialogTitle(CharSequence dialogTitle) { + mDialogTitle = dialogTitle; + } + + /** + * @see #setDialogTitle(CharSequence) + * @param dialogTitleResId The dialog title as a resource. + */ + public void setDialogTitle(int dialogTitleResId) { + setDialogTitle(getContext().getString(dialogTitleResId)); + } + + /** + * Returns the title to be shown on subsequent dialogs. + * @return The title. + */ + public CharSequence getDialogTitle() { + return mDialogTitle; + } + + /** + * Sets the message of the dialog. This will be shown on subsequent dialogs. + * <p> + * This message forms the content View of the dialog and conflicts with + * list-based dialogs, for example. If setting a custom View on a dialog via + * {@link #setDialogLayoutResource(int)}, include a text View with ID + * {@link android.R.id#message} and it will be populated with this message. + * + * @param dialogMessage The message. + */ + public void setDialogMessage(CharSequence dialogMessage) { + mDialogMessage = dialogMessage; + } + + /** + * @see #setDialogMessage(CharSequence) + * @param dialogMessageResId The dialog message as a resource. + */ + public void setDialogMessage(int dialogMessageResId) { + setDialogMessage(getContext().getString(dialogMessageResId)); + } + + /** + * Returns the message to be shown on subsequent dialogs. + * @return The message. + */ + public CharSequence getDialogMessage() { + return mDialogMessage; + } + + /** + * Sets the icon of the dialog. This will be shown on subsequent dialogs. + * + * @param dialogIcon The icon, as a {@link Drawable}. + */ + public void setDialogIcon(Drawable dialogIcon) { + mDialogIcon = dialogIcon; + } + + /** + * Sets the icon (resource ID) of the dialog. This will be shown on + * subsequent dialogs. + * + * @param dialogIconRes The icon, as a resource ID. + */ + public void setDialogIcon(int dialogIconRes) { + mDialogIcon = getContext().getResources().getDrawable(dialogIconRes); + } + + /** + * Returns the icon to be shown on subsequent dialogs. + * @return The icon, as a {@link Drawable}. + */ + public Drawable getDialogIcon() { + return mDialogIcon; + } + + /** + * Sets the text of the positive button of the dialog. This will be shown on + * subsequent dialogs. + * + * @param positiveButtonText The text of the positive button. + */ + public void setPositiveButtonText(CharSequence positiveButtonText) { + mPositiveButtonText = positiveButtonText; + } + + /** + * @see #setPositiveButtonText(CharSequence) + * @param positiveButtonTextResId The positive button text as a resource. + */ + public void setPositiveButtonText(int positiveButtonTextResId) { + setPositiveButtonText(getContext().getString(positiveButtonTextResId)); + } + + /** + * Returns the text of the positive button to be shown on subsequent + * dialogs. + * + * @return The text of the positive button. + */ + public CharSequence getPositiveButtonText() { + return mPositiveButtonText; + } + + /** + * Sets the text of the negative button of the dialog. This will be shown on + * subsequent dialogs. + * + * @param negativeButtonText The text of the negative button. + */ + public void setNegativeButtonText(CharSequence negativeButtonText) { + mNegativeButtonText = negativeButtonText; + } + + /** + * @see #setNegativeButtonText(CharSequence) + * @param negativeButtonTextResId The negative button text as a resource. + */ + public void setNegativeButtonText(int negativeButtonTextResId) { + setNegativeButtonText(getContext().getString(negativeButtonTextResId)); + } + + /** + * Returns the text of the negative button to be shown on subsequent + * dialogs. + * + * @return The text of the negative button. + */ + public CharSequence getNegativeButtonText() { + return mNegativeButtonText; + } + + /** + * Sets the layout resource that is inflated as the {@link View} to be shown + * as the content View of subsequent dialogs. + * + * @param dialogLayoutResId The layout resource ID to be inflated. + * @see #setDialogMessage(CharSequence) + */ + public void setDialogLayoutResource(int dialogLayoutResId) { + mDialogLayoutResId = dialogLayoutResId; + } + + /** + * Returns the layout resource that is used as the content View for + * subsequent dialogs. + * + * @return The layout resource. + */ + public int getDialogLayoutResource() { + return mDialogLayoutResId; + } + + /** + * Prepares the dialog builder to be shown when the preference is clicked. + * Use this to set custom properties on the dialog. + * <p> + * Do not {@link AlertDialog.Builder#create()} or + * {@link AlertDialog.Builder#show()}. + */ + protected void onPrepareDialogBuilder(AlertDialog.Builder builder) { + } + + @Override + protected void onClick() { + showDialog(null); + } + + /** + * Shows the dialog associated with this Preference. This is normally initiated + * automatically on clicking on the preference. Call this method if you need to + * show the dialog on some other event. + * + * @param state Optional instance state to restore on the dialog + */ + protected void showDialog(Bundle state) { + Context context = getContext(); + + mWhichButtonClicked = DialogInterface.BUTTON2; + + mBuilder = new AlertDialog.Builder(context) + .setTitle(mDialogTitle) + .setIcon(mDialogIcon) + .setPositiveButton(mPositiveButtonText, this) + .setNegativeButton(mNegativeButtonText, this); + + View contentView = onCreateDialogView(); + if (contentView != null) { + onBindDialogView(contentView); + mBuilder.setView(contentView); + } else { + mBuilder.setMessage(mDialogMessage); + } + + onPrepareDialogBuilder(mBuilder); + + getPreferenceManager().registerOnActivityDestroyListener(this); + + // Create the dialog + final Dialog dialog = mDialog = mBuilder.create(); + if (state != null) { + dialog.onRestoreInstanceState(state); + } + dialog.setOnDismissListener(this); + dialog.show(); + } + + /** + * Creates the content view for the dialog (if a custom content view is + * required). By default, it inflates the dialog layout resource if it is + * set. + * + * @return The content View for the dialog. + * @see #setLayoutResource(int) + */ + protected View onCreateDialogView() { + if (mDialogLayoutResId == 0) { + return null; + } + + LayoutInflater inflater = (LayoutInflater) getContext().getSystemService( + Context.LAYOUT_INFLATER_SERVICE); + return inflater.inflate(mDialogLayoutResId, null); + } + + /** + * Binds views in the content View of the dialog to data. + * <p> + * Make sure to call through to the superclass implementation. + * + * @param view The content View of the dialog, if it is custom. + */ + protected void onBindDialogView(View view) { + View dialogMessageView = view.findViewById(com.android.internal.R.id.message); + + if (dialogMessageView != null) { + final CharSequence message = getDialogMessage(); + int newVisibility = View.GONE; + + if (!TextUtils.isEmpty(message)) { + if (dialogMessageView instanceof TextView) { + ((TextView) dialogMessageView).setText(message); + } + + newVisibility = View.VISIBLE; + } + + if (dialogMessageView.getVisibility() != newVisibility) { + dialogMessageView.setVisibility(newVisibility); + } + } + } + + public void onClick(DialogInterface dialog, int which) { + mWhichButtonClicked = which; + } + + public void onDismiss(DialogInterface dialog) { + + getPreferenceManager().unregisterOnActivityDestroyListener(this); + + mDialog = null; + onDialogClosed(mWhichButtonClicked == DialogInterface.BUTTON1); + } + + /** + * Called when the dialog is dismissed and should be used to save data to + * the {@link SharedPreferences}. + * + * @param positiveResult Whether the positive button was clicked (true), or + * the negative button was clicked or the dialog was canceled (false). + */ + protected void onDialogClosed(boolean positiveResult) { + } + + /** + * {@inheritDoc} + */ + public void onActivityDestroy() { + + if (mDialog == null || !mDialog.isShowing()) { + return; + } + + mDialog.dismiss(); + } + + @Override + protected Parcelable onSaveInstanceState() { + final Parcelable superState = super.onSaveInstanceState(); + if (mDialog == null || !mDialog.isShowing()) { + return superState; + } + + final SavedState myState = new SavedState(superState); + myState.isDialogShowing = true; + myState.dialogBundle = mDialog.onSaveInstanceState(); + return myState; + } + + @Override + protected void onRestoreInstanceState(Parcelable state) { + if (state == null || !state.getClass().equals(SavedState.class)) { + // Didn't save state for us in onSaveInstanceState + super.onRestoreInstanceState(state); + return; + } + + SavedState myState = (SavedState) state; + super.onRestoreInstanceState(myState.getSuperState()); + if (myState.isDialogShowing) { + showDialog(myState.dialogBundle); + } + } + + private static class SavedState extends BaseSavedState { + boolean isDialogShowing; + Bundle dialogBundle; + + public SavedState(Parcel source) { + super(source); + isDialogShowing = source.readInt() == 1; + dialogBundle = source.readBundle(); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeInt(isDialogShowing ? 1 : 0); + dest.writeBundle(dialogBundle); + } + + public SavedState(Parcelable superState) { + super(superState); + } + + public static final Parcelable.Creator<SavedState> CREATOR = + new Parcelable.Creator<SavedState>() { + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } + +} diff --git a/core/java/android/preference/EditTextPreference.java b/core/java/android/preference/EditTextPreference.java new file mode 100644 index 0000000..be56003 --- /dev/null +++ b/core/java/android/preference/EditTextPreference.java @@ -0,0 +1,228 @@ +/* + * 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 android.content.Context; +import android.content.SharedPreferences; +import android.content.res.TypedArray; +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.widget.EditText; +import android.widget.LinearLayout; + +/** + * The {@link EditTextPreference} class is a preference that allows for string + * input. + * <p> + * It is a subclass of {@link DialogPreference} and shows the {@link EditText} + * in a dialog. This {@link EditText} can be modified either programmatically + * via {@link #getEditText()}, or through XML by setting any EditText + * attributes on the EditTextPreference. + * <p> + * This preference will store a string into the SharedPreferences. + * <p> + * See {@link android.R.styleable#EditText EditText Attributes}. + */ +public class EditTextPreference extends DialogPreference { + /** + * The edit text shown in the dialog. + */ + private EditText mEditText; + + private String mText; + + public EditTextPreference(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + mEditText = new EditText(context, attrs); + + // Give it an ID so it can be saved/restored + mEditText.setId(com.android.internal.R.id.edit); + + /* + * The preference framework and view framework both have an 'enabled' + * attribute. Most likely, the 'enabled' specified in this XML is for + * the preference framework, but it was also given to the view framework. + * We reset the enabled state. + */ + mEditText.setEnabled(true); + } + + public EditTextPreference(Context context, AttributeSet attrs) { + this(context, attrs, com.android.internal.R.attr.editTextPreferenceStyle); + } + + public EditTextPreference(Context context) { + this(context, null); + } + + /** + * Saves the text to the {@link SharedPreferences}. + * + * @param text The text to save + */ + public void setText(String text) { + final boolean wasBlocking = shouldDisableDependents(); + + mText = text; + + persistString(text); + + final boolean isBlocking = shouldDisableDependents(); + if (isBlocking != wasBlocking) { + notifyDependencyChange(isBlocking); + } + } + + /** + * Gets the text from the {@link SharedPreferences}. + * + * @return The current preference value. + */ + public String getText() { + return mText; + } + + @Override + protected void onBindDialogView(View view) { + super.onBindDialogView(view); + + EditText editText = mEditText; + editText.setText(getText()); + + ViewParent oldParent = editText.getParent(); + if (oldParent != view) { + if (oldParent != null) { + ((ViewGroup) oldParent).removeView(editText); + } + onAddEditTextToDialogView(view, editText); + } + } + + /** + * Adds the EditText widget of this preference to the dialog's view. + * + * @param dialogView The dialog view. + */ + protected void onAddEditTextToDialogView(View dialogView, EditText editText) { + ViewGroup container = (ViewGroup) dialogView + .findViewById(com.android.internal.R.id.edittext_container); + if (container != null) { + container.addView(editText, ViewGroup.LayoutParams.FILL_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + } + } + + @Override + protected void onDialogClosed(boolean positiveResult) { + super.onDialogClosed(positiveResult); + + if (positiveResult) { + String value = mEditText.getText().toString(); + if (callChangeListener(value)) { + setText(value); + } + } + } + + @Override + protected Object onGetDefaultValue(TypedArray a, int index) { + return a.getString(index); + } + + @Override + protected void onSetInitialValue(boolean restoreValue, Object defaultValue) { + setText(restoreValue ? getPersistedString(mText) : (String) defaultValue); + } + + @Override + public boolean shouldDisableDependents() { + return TextUtils.isEmpty(mText) || super.shouldDisableDependents(); + } + + /** + * Returns the {@link EditText} widget that will be shown in the dialog. + * + * @return The {@link EditText} widget that will be shown in the dialog. + */ + public EditText getEditText() { + return mEditText; + } + + @Override + protected Parcelable onSaveInstanceState() { + final Parcelable superState = super.onSaveInstanceState(); + if (isPersistent()) { + // No need to save instance state since it's persistent + return superState; + } + + final SavedState myState = new SavedState(superState); + myState.text = getText(); + return myState; + } + + @Override + protected void onRestoreInstanceState(Parcelable state) { + if (state == null || !state.getClass().equals(SavedState.class)) { + // Didn't save state for us in onSaveInstanceState + super.onRestoreInstanceState(state); + return; + } + + SavedState myState = (SavedState) state; + super.onRestoreInstanceState(myState.getSuperState()); + setText(myState.text); + } + + private static class SavedState extends BaseSavedState { + String text; + + public SavedState(Parcel source) { + super(source); + text = source.readString(); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeString(text); + } + + public SavedState(Parcelable superState) { + super(superState); + } + + public static final Parcelable.Creator<SavedState> CREATOR = + new Parcelable.Creator<SavedState>() { + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } + +} diff --git a/core/java/android/preference/GenericInflater.java b/core/java/android/preference/GenericInflater.java new file mode 100644 index 0000000..3003290 --- /dev/null +++ b/core/java/android/preference/GenericInflater.java @@ -0,0 +1,520 @@ +/* + * 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 java.io.IOException; +import java.lang.reflect.Constructor; +import java.util.HashMap; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import android.content.Context; +import android.content.res.XmlResourceParser; +import android.util.AttributeSet; +import android.util.Xml; +import android.view.ContextThemeWrapper; +import android.view.InflateException; +import android.view.LayoutInflater; + +// TODO: fix generics +/** + * Generic XML inflater. This has been adapted from {@link LayoutInflater} and + * quickly passed over to use generics. + * + * @hide + * @param T The type of the items to inflate + * @param P The type of parents (that is those items that contain other items). + * Must implement {@link GenericInflater.Parent} + */ +abstract class GenericInflater<T, P extends GenericInflater.Parent> { + private final boolean DEBUG = false; + + protected final Context mContext; + + // these are optional, set by the caller + private boolean mFactorySet; + private Factory<T> mFactory; + + private final Object[] mConstructorArgs = new Object[2]; + + private static final Class[] mConstructorSignature = new Class[] { + Context.class, AttributeSet.class}; + + private static final HashMap sConstructorMap = new HashMap(); + + private String mDefaultPackage; + + public interface Parent<T> { + public void addItemFromInflater(T child); + } + + public interface Factory<T> { + /** + * Hook you can supply that is called when inflating from a + * inflater. You can use this to customize the tag + * names available in your XML files. + * <p> + * Note that it is good practice to prefix these custom names with your + * package (i.e., com.coolcompany.apps) to avoid conflicts with system + * names. + * + * @param name Tag name to be inflated. + * @param context The context the item is being created in. + * @param attrs Inflation attributes as specified in XML file. + * @return Newly created item. Return null for the default behavior. + */ + public T onCreateItem(String name, Context context, AttributeSet attrs); + } + + private static class FactoryMerger<T> implements Factory<T> { + private final Factory<T> mF1, mF2; + + FactoryMerger(Factory<T> f1, Factory<T> f2) { + mF1 = f1; + mF2 = f2; + } + + public T onCreateItem(String name, Context context, AttributeSet attrs) { + T v = mF1.onCreateItem(name, context, attrs); + if (v != null) return v; + return mF2.onCreateItem(name, context, attrs); + } + } + + /** + * Create a new inflater instance associated with a + * particular Context. + * + * @param context The Context in which this inflater will + * create its items; most importantly, this supplies the theme + * from which the default values for their attributes are + * retrieved. + */ + protected GenericInflater(Context context) { + mContext = context; + } + + /** + * Create a new inflater instance that is a copy of an + * existing inflater, optionally with its Context + * changed. For use in implementing {@link #cloneInContext}. + * + * @param original The original inflater to copy. + * @param newContext The new Context to use. + */ + protected GenericInflater(GenericInflater<T,P> original, Context newContext) { + mContext = newContext; + mFactory = original.mFactory; + } + + /** + * Create a copy of the existing inflater object, with the copy + * pointing to a different Context than the original. This is used by + * {@link ContextThemeWrapper} to create a new inflater to go along + * with the new Context theme. + * + * @param newContext The new Context to associate with the new inflater. + * May be the same as the original Context if desired. + * + * @return Returns a brand spanking new inflater object associated with + * the given Context. + */ + public abstract GenericInflater cloneInContext(Context newContext); + + /** + * Sets the default package that will be searched for classes to construct + * for tag names that have no explicit package. + * + * @param defaultPackage The default package. This will be prepended to the + * tag name, so it should end with a period. + */ + public void setDefaultPackage(String defaultPackage) { + mDefaultPackage = defaultPackage; + } + + /** + * Returns the default package, or null if it is not set. + * + * @see #setDefaultPackage(String) + * @return The default package. + */ + public String getDefaultPackage() { + return mDefaultPackage; + } + + /** + * Return the context we are running in, for access to resources, class + * loader, etc. + */ + public Context getContext() { + return mContext; + } + + /** + * Return the current factory (or null). This is called on each element + * name. If the factory returns an item, add that to the hierarchy. If it + * returns null, proceed to call onCreateItem(name). + */ + public final Factory<T> getFactory() { + return mFactory; + } + + /** + * Attach a custom Factory interface for creating items while using this + * inflater. This must not be null, and can only be set + * once; after setting, you can not change the factory. This is called on + * each element name as the XML is parsed. If the factory returns an item, + * that is added to the hierarchy. If it returns null, the next factory + * default {@link #onCreateItem} method is called. + * <p> + * If you have an existing inflater and want to add your + * own factory to it, use {@link #cloneInContext} to clone the existing + * instance and then you can use this function (once) on the returned new + * instance. This will merge your own factory with whatever factory the + * original instance is using. + */ + public void setFactory(Factory<T> factory) { + if (mFactorySet) { + throw new IllegalStateException("" + + "A factory has already been set on this inflater"); + } + if (factory == null) { + throw new NullPointerException("Given factory can not be null"); + } + mFactorySet = true; + if (mFactory == null) { + mFactory = factory; + } else { + mFactory = new FactoryMerger<T>(factory, mFactory); + } + } + + + /** + * Inflate a new item hierarchy from the specified xml resource. Throws + * InflaterException if there is an error. + * + * @param resource ID for an XML resource to load (e.g., + * <code>R.layout.main_page</code>) + * @param root Optional parent of the generated hierarchy. + * @return The root of the inflated hierarchy. If root was supplied, + * this is the root item; otherwise it is the root of the inflated + * XML file. + */ + public T inflate(int resource, P root) { + return inflate(resource, root, root != null); + } + + /** + * Inflate a new hierarchy from the specified xml node. Throws + * InflaterException if there is an error. * + * <p> + * <em><strong>Important</strong></em> For performance + * reasons, inflation relies heavily on pre-processing of XML files + * that is done at build time. Therefore, it is not currently possible to + * use inflater with an XmlPullParser over a plain XML file at runtime. + * + * @param parser XML dom node containing the description of the + * hierarchy. + * @param root Optional parent of the generated hierarchy. + * @return The root of the inflated hierarchy. If root was supplied, + * this is the that; otherwise it is the root of the inflated + * XML file. + */ + public T inflate(XmlPullParser parser, P root) { + return inflate(parser, root, root != null); + } + + /** + * Inflate a new hierarchy from the specified xml resource. Throws + * InflaterException if there is an error. + * + * @param resource ID for an XML resource to load (e.g., + * <code>R.layout.main_page</code>) + * @param root Optional root to be the parent of the generated hierarchy (if + * <em>attachToRoot</em> is true), or else simply an object that + * provides a set of values for root of the returned + * hierarchy (if <em>attachToRoot</em> is false.) + * @param attachToRoot Whether the inflated hierarchy should be attached to + * the root parameter? + * @return The root of the inflated hierarchy. If root was supplied and + * attachToRoot is true, this is root; otherwise it is the root of + * the inflated XML file. + */ + public T inflate(int resource, P root, boolean attachToRoot) { + if (DEBUG) System.out.println("INFLATING from resource: " + resource); + XmlResourceParser parser = getContext().getResources().getXml(resource); + try { + return inflate(parser, root, attachToRoot); + } finally { + parser.close(); + } + } + + /** + * Inflate a new hierarchy from the specified XML node. Throws + * InflaterException if there is an error. + * <p> + * <em><strong>Important</strong></em> For performance + * reasons, inflation relies heavily on pre-processing of XML files + * that is done at build time. Therefore, it is not currently possible to + * use inflater with an XmlPullParser over a plain XML file at runtime. + * + * @param parser XML dom node containing the description of the + * hierarchy. + * @param root Optional to be the parent of the generated hierarchy (if + * <em>attachToRoot</em> is true), or else simply an object that + * provides a set of values for root of the returned + * hierarchy (if <em>attachToRoot</em> is false.) + * @param attachToRoot Whether the inflated hierarchy should be attached to + * the root parameter? + * @return The root of the inflated hierarchy. If root was supplied and + * attachToRoot is true, this is root; otherwise it is the root of + * the inflated XML file. + */ + public T inflate(XmlPullParser parser, P root, + boolean attachToRoot) { + synchronized (mConstructorArgs) { + final AttributeSet attrs = Xml.asAttributeSet(parser); + mConstructorArgs[0] = mContext; + T result = (T) root; + + try { + // Look for the root node. + int type; + while ((type = parser.next()) != parser.START_TAG + && type != parser.END_DOCUMENT) { + ; + } + + if (type != parser.START_TAG) { + throw new InflateException(parser.getPositionDescription() + + ": No start tag found!"); + } + + if (DEBUG) { + System.out.println("**************************"); + System.out.println("Creating root: " + + parser.getName()); + System.out.println("**************************"); + } + // Temp is the root that was found in the xml + T xmlRoot = createItemFromTag(parser, parser.getName(), + attrs); + + result = (T) onMergeRoots(root, attachToRoot, (P) xmlRoot); + + if (DEBUG) { + System.out.println("-----> start inflating children"); + } + // Inflate all children under temp + rInflate(parser, result, attrs); + if (DEBUG) { + System.out.println("-----> done inflating children"); + } + + } catch (InflateException e) { + throw e; + + } catch (XmlPullParserException e) { + InflateException ex = new InflateException(e.getMessage()); + ex.initCause(e); + throw ex; + } catch (IOException e) { + InflateException ex = new InflateException( + parser.getPositionDescription() + + ": " + e.getMessage()); + ex.initCause(e); + throw ex; + } + + return result; + } + } + + /** + * Low-level function for instantiating by name. This attempts to + * instantiate class of the given <var>name</var> found in this + * inflater's ClassLoader. + * + * <p> + * There are two things that can happen in an error case: either the + * exception describing the error will be thrown, or a null will be + * returned. You must deal with both possibilities -- the former will happen + * the first time createItem() is called for a class of a particular name, + * the latter every time there-after for that class name. + * + * @param name The full name of the class to be instantiated. + * @param attrs The XML attributes supplied for this instance. + * + * @return The newly instantied item, or null. + */ + public final T createItem(String name, String prefix, AttributeSet attrs) + throws ClassNotFoundException, InflateException { + Constructor constructor = (Constructor) sConstructorMap.get(name); + + try { + if (null == constructor) { + // Class not found in the cache, see if it's real, + // and try to add it + Class clazz = mContext.getClassLoader().loadClass( + prefix != null ? (prefix + name) : name); + constructor = clazz.getConstructor(mConstructorSignature); + sConstructorMap.put(name, constructor); + } + + Object[] args = mConstructorArgs; + args[1] = attrs; + return (T) constructor.newInstance(args); + + } catch (NoSuchMethodException e) { + InflateException ie = new InflateException(attrs + .getPositionDescription() + + ": Error inflating class " + + (prefix != null ? (prefix + name) : name)); + ie.initCause(e); + throw ie; + + } catch (ClassNotFoundException e) { + // If loadClass fails, we should propagate the exception. + throw e; + } catch (Exception e) { + InflateException ie = new InflateException(attrs + .getPositionDescription() + + ": Error inflating class " + + constructor.getClass().getName()); + ie.initCause(e); + throw ie; + } + } + + /** + * This routine is responsible for creating the correct subclass of item + * given the xml element name. Override it to handle custom item objects. If + * you override this in your subclass be sure to call through to + * super.onCreateItem(name) for names you do not recognize. + * + * @param name The fully qualified class name of the item to be create. + * @param attrs An AttributeSet of attributes to apply to the item. + * @return The item created. + */ + protected T onCreateItem(String name, AttributeSet attrs) throws ClassNotFoundException { + return createItem(name, mDefaultPackage, attrs); + } + + private final T createItemFromTag(XmlPullParser parser, String name, AttributeSet attrs) { + if (DEBUG) System.out.println("******** Creating item: " + name); + + try { + T item = (mFactory == null) ? null : mFactory.onCreateItem(name, mContext, attrs); + + if (item == null) { + if (-1 == name.indexOf('.')) { + item = onCreateItem(name, attrs); + } else { + item = createItem(name, null, attrs); + } + } + + if (DEBUG) System.out.println("Created item is: " + item); + return item; + + } catch (InflateException e) { + throw e; + + } catch (ClassNotFoundException e) { + InflateException ie = new InflateException(attrs + .getPositionDescription() + + ": Error inflating class " + name); + ie.initCause(e); + throw ie; + + } catch (Exception e) { + InflateException ie = new InflateException(attrs + .getPositionDescription() + + ": Error inflating class " + name); + ie.initCause(e); + throw ie; + } + } + + /** + * Recursive method used to descend down the xml hierarchy and instantiate + * items, instantiate their children, and then call onFinishInflate(). + */ + private void rInflate(XmlPullParser parser, T parent, final AttributeSet attrs) + throws XmlPullParserException, IOException { + final int depth = parser.getDepth(); + + int type; + while (((type = parser.next()) != parser.END_TAG || + parser.getDepth() > depth) && type != parser.END_DOCUMENT) { + + if (type != parser.START_TAG) { + continue; + } + + if (onCreateCustomFromTag(parser, parent, attrs)) { + continue; + } + + if (DEBUG) { + System.out.println("Now inflating tag: " + parser.getName()); + } + String name = parser.getName(); + + T item = createItemFromTag(parser, name, attrs); + + if (DEBUG) { + System.out + .println("Creating params from parent: " + parent); + } + + ((P) parent).addItemFromInflater(item); + + if (DEBUG) { + System.out.println("-----> start inflating children"); + } + rInflate(parser, item, attrs); + if (DEBUG) { + System.out.println("-----> done inflating children"); + } + } + + } + + /** + * Before this inflater tries to create an item from the tag, this method + * will be called. The parser will be pointing to the start of a tag, you + * must stop parsing and return when you reach the end of this element! + * + * @param parser XML dom node containing the description of the hierarchy. + * @param parent The item that should be the parent of whatever you create. + * @param attrs An AttributeSet of attributes to apply to the item. + * @return Whether you created a custom object (true), or whether this + * inflater should proceed to create an item. + */ + protected boolean onCreateCustomFromTag(XmlPullParser parser, T parent, + final AttributeSet attrs) throws XmlPullParserException { + return false; + } + + protected P onMergeRoots(P givenRoot, boolean attachToGivenRoot, P xmlRoot) { + return xmlRoot; + } +} diff --git a/core/java/android/preference/ListPreference.java b/core/java/android/preference/ListPreference.java new file mode 100644 index 0000000..6c98ded --- /dev/null +++ b/core/java/android/preference/ListPreference.java @@ -0,0 +1,277 @@ +/* + * 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 android.app.AlertDialog.Builder; +import android.content.Context; +import android.content.DialogInterface; +import android.content.res.TypedArray; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.AttributeSet; + +/** + * The {@link ListPreference} is a preference that displays a list of entries as + * a dialog. + * <p> + * This preference will store a string into the SharedPreferences. This string will be the value + * from the {@link #setEntryValues(CharSequence[])} array. + * + * @attr ref android.R.styleable#ListPreference_entries + * @attr ref android.R.styleable#ListPreference_entryValues + */ +public class ListPreference extends DialogPreference { + private CharSequence[] mEntries; + private CharSequence[] mEntryValues; + private String mValue; + private int mClickedDialogEntryIndex; + + public ListPreference(Context context, AttributeSet attrs) { + super(context, attrs); + + TypedArray a = context.obtainStyledAttributes(attrs, + com.android.internal.R.styleable.ListPreference, 0, 0); + mEntries = a.getTextArray(com.android.internal.R.styleable.ListPreference_entries); + mEntryValues = a.getTextArray(com.android.internal.R.styleable.ListPreference_entryValues); + a.recycle(); + } + + public ListPreference(Context context) { + this(context, null); + } + + /** + * Sets the human-readable entries to be shown in the list. This will be + * shown in subsequent dialogs. + * <p> + * Each entry must have a corresponding index in + * {@link #setEntryValues(CharSequence[])}. + * + * @param entries The entries. + * @see #setEntryValues(CharSequence[]) + */ + public void setEntries(CharSequence[] entries) { + mEntries = entries; + } + + /** + * @see #setEntries(CharSequence[]) + * @param entriesResId The entries array as a resource. + */ + public void setEntries(int entriesResId) { + setEntries(getContext().getResources().getTextArray(entriesResId)); + } + + /** + * The list of entries to be shown in the list in subsequent dialogs. + * + * @return The list as an array. + */ + public CharSequence[] getEntries() { + return mEntries; + } + + /** + * The array to find the value to save for a preference when an entry from + * entries is selected. If a user clicks on the second item in entries, the + * second item in this array will be saved to the preference. + * + * @param entryValues The array to be used as values to save for the preference. + */ + public void setEntryValues(CharSequence[] entryValues) { + mEntryValues = entryValues; + } + + /** + * @see #setEntryValues(CharSequence[]) + * @param entryValuesResId The entry values array as a resource. + */ + public void setEntryValues(int entryValuesResId) { + setEntryValues(getContext().getResources().getTextArray(entryValuesResId)); + } + + /** + * Returns the array of values to be saved for the preference. + * + * @return The array of values. + */ + public CharSequence[] getEntryValues() { + return mEntryValues; + } + + /** + * Sets the value of the key. This should be one of the entries in + * {@link #getEntryValues()}. + * + * @param value The value to set for the key. + */ + public void setValue(String value) { + mValue = value; + + persistString(value); + } + + /** + * Sets the value to the given index from the entry values. + * + * @param index The index of the value to set. + */ + public void setValueIndex(int index) { + if (mEntryValues != null) { + setValue(mEntryValues[index].toString()); + } + } + + /** + * Returns the value of the key. This should be one of the entries in + * {@link #getEntryValues()}. + * + * @return The value of the key. + */ + public String getValue() { + return mValue; + } + + /** + * Returns the entry corresponding to the current value. + * + * @return The entry corresponding to the current value, or null. + */ + public CharSequence getEntry() { + int index = getValueIndex(); + return index >= 0 && mEntries != null ? mEntries[index] : null; + } + + /** + * Returns the index of the given value (in the entry values array). + * + * @param value The value whose index should be returned. + * @return The index of the value, or -1 if not found. + */ + public int findIndexOfValue(String value) { + if (value != null && mEntryValues != null) { + for (int i = mEntryValues.length - 1; i >= 0; i--) { + if (mEntryValues[i].equals(value)) { + return i; + } + } + } + return -1; + } + + private int getValueIndex() { + return findIndexOfValue(mValue); + } + + @Override + protected void onPrepareDialogBuilder(Builder builder) { + super.onPrepareDialogBuilder(builder); + + if (mEntries == null || mEntryValues == null) { + throw new IllegalStateException( + "ListPreference requires an entries array and an entryValues array."); + } + + mClickedDialogEntryIndex = getValueIndex(); + builder.setSingleChoiceItems(mEntries, mClickedDialogEntryIndex, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + mClickedDialogEntryIndex = which; + } + }); + } + + @Override + protected void onDialogClosed(boolean positiveResult) { + super.onDialogClosed(positiveResult); + + if (positiveResult && mClickedDialogEntryIndex >= 0 && mEntryValues != null) { + String value = mEntryValues[mClickedDialogEntryIndex].toString(); + if (callChangeListener(value)) { + setValue(value); + } + } + } + + @Override + protected Object onGetDefaultValue(TypedArray a, int index) { + return a.getString(index); + } + + @Override + protected void onSetInitialValue(boolean restoreValue, Object defaultValue) { + setValue(restoreValue ? getPersistedString(mValue) : (String) defaultValue); + } + + @Override + protected Parcelable onSaveInstanceState() { + final Parcelable superState = super.onSaveInstanceState(); + if (isPersistent()) { + // No need to save instance state since it's persistent + return superState; + } + + final SavedState myState = new SavedState(superState); + myState.value = getValue(); + return myState; + } + + @Override + protected void onRestoreInstanceState(Parcelable state) { + if (state == null || !state.getClass().equals(SavedState.class)) { + // Didn't save state for us in onSaveInstanceState + super.onRestoreInstanceState(state); + return; + } + + SavedState myState = (SavedState) state; + super.onRestoreInstanceState(myState.getSuperState()); + setValue(myState.value); + } + + private static class SavedState extends BaseSavedState { + String value; + + public SavedState(Parcel source) { + super(source); + value = source.readString(); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeString(value); + } + + public SavedState(Parcelable superState) { + super(superState); + } + + public static final Parcelable.Creator<SavedState> CREATOR = + new Parcelable.Creator<SavedState>() { + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } + +} diff --git a/core/java/android/preference/OnDependencyChangeListener.java b/core/java/android/preference/OnDependencyChangeListener.java new file mode 100644 index 0000000..ce25e34 --- /dev/null +++ b/core/java/android/preference/OnDependencyChangeListener.java @@ -0,0 +1,32 @@ +/* + * 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; + +/** + * Interface definition for a callback to be invoked when this + * {@link Preference} changes with respect to enabling/disabling + * dependents. + */ +interface OnDependencyChangeListener { + /** + * Called when this preference has changed in a way that dependents should + * care to change their state. + * + * @param disablesDependent Whether the dependent should be disabled. + */ + void onDependencyChanged(Preference dependency, boolean disablesDependent); +} diff --git a/core/java/android/preference/Preference.java b/core/java/android/preference/Preference.java new file mode 100644 index 0000000..1db7525 --- /dev/null +++ b/core/java/android/preference/Preference.java @@ -0,0 +1,1577 @@ +/* + * 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 java.util.ArrayList; +import java.util.List; + +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.res.TypedArray; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; +import android.util.AttributeSet; +import com.android.internal.util.CharSequences; +import android.view.AbsSavedState; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ListView; +import android.widget.TextView; + +/** + * The {@link Preference} class represents the basic preference UI building + * block that is displayed by a {@link PreferenceActivity} in the form of a + * {@link ListView}. This class provides the {@link View} to be displayed in + * the activity and associates with a {@link SharedPreferences} to + * store/retrieve the preference data. + * <p> + * When specifying a preference hierarchy in XML, each tag name can point to a + * subclass of {@link Preference}, similar to the view hierarchy and layouts. + * <p> + * This class contains a {@code key} that will be used as the key into the + * {@link SharedPreferences}. It is up to the subclass to decide how to store + * the value. + * + * @attr ref android.R.styleable#Preference_key + * @attr ref android.R.styleable#Preference_title + * @attr ref android.R.styleable#Preference_summary + * @attr ref android.R.styleable#Preference_order + * @attr ref android.R.styleable#Preference_layout + * @attr ref android.R.styleable#Preference_widgetLayout + * @attr ref android.R.styleable#Preference_enabled + * @attr ref android.R.styleable#Preference_selectable + * @attr ref android.R.styleable#Preference_dependency + * @attr ref android.R.styleable#Preference_persistent + * @attr ref android.R.styleable#Preference_defaultValue + * @attr ref android.R.styleable#Preference_shouldDisableView + */ +public class Preference implements Comparable<Preference>, OnDependencyChangeListener { + /** + * Specify for {@link #setOrder(int)} if a specific order is not required. + */ + public static final int DEFAULT_ORDER = Integer.MAX_VALUE; + + private Context mContext; + private PreferenceManager mPreferenceManager; + + /** + * Set when added to hierarchy since we need a unique ID within that + * hierarchy. + */ + private long mId; + + private OnPreferenceChangeListener mOnChangeListener; + private OnPreferenceClickListener mOnClickListener; + + private int mOrder = DEFAULT_ORDER; + private CharSequence mTitle; + private CharSequence mSummary; + private String mKey; + private Intent mIntent; + private boolean mEnabled = true; + private boolean mSelectable = true; + private boolean mRequiresKey; + private boolean mPersistent = true; + private String mDependencyKey; + private Object mDefaultValue; + + /** + * @see #setShouldDisableView(boolean) + */ + private boolean mShouldDisableView = true; + + private int mLayoutResId = com.android.internal.R.layout.preference; + private int mWidgetLayoutResId; + private boolean mHasSpecifiedLayout = false; + + private OnPreferenceChangeInternalListener mListener; + + private List<Preference> mDependents; + + private boolean mBaseMethodCalled; + + /** + * Interface definition for a callback to be invoked when this + * {@link Preference Preference's} value has been changed by the user and is + * about to be set and/or persisted. This gives the client a chance + * to prevent setting and/or persisting the value. + */ + public interface OnPreferenceChangeListener { + /** + * Called when this preference has been changed by the user. This is + * called before the preference's state is about to be updated and + * before the state is persisted. + * + * @param preference This preference. + * @param newValue The new value of the preference. + * @return Whether or not to update this preference's state with the new value. + */ + boolean onPreferenceChange(Preference preference, Object newValue); + } + + /** + * Interface definition for a callback to be invoked when a preference is + * clicked. + */ + public interface OnPreferenceClickListener { + /** + * Called when a preference has been clicked. + * + * @param preference The preference that was clicked. + * @return Whether the click was handled. + */ + boolean onPreferenceClick(Preference preference); + } + + /** + * Interface definition for a callback to be invoked when this + * {@link Preference} is changed or if this is a group, there is an + * addition/removal of {@link Preference}(s). This is used internally. + */ + interface OnPreferenceChangeInternalListener { + /** + * Called when this preference has changed. + * + * @param preference This preference. + */ + void onPreferenceChange(Preference preference); + + /** + * Called when this group has added/removed {@link Preference}(s). + * + * @param preference This preference. + */ + void onPreferenceHierarchyChange(Preference preference); + } + + /** + * Perform inflation from XML and apply a class-specific base style. This + * constructor of {@link Preference} allows subclasses to use their own base + * style when they are inflating. For example, a {@link CheckBoxPreference}'s + * constructor would call this version of the super class constructor and + * supply {@code android.R.attr.checkBoxPreferenceStyle} for <var>defStyle</var>; + * this allows the theme's checkbox preference style to modify all of the base + * preference attributes as well as the {@link CheckBoxPreference} class's + * attributes. + * + * @param context The Context this is associated with, through which it can + * access the current theme, resources, {@link SharedPreferences}, + * etc. + * @param attrs The attributes of the XML tag that is inflating the preference. + * @param defStyle The default style to apply to this preference. If 0, no style + * will be applied (beyond what is included in the theme). This + * may either be an attribute resource, whose value will be + * retrieved from the current theme, or an explicit style + * resource. + * @see #Preference(Context, AttributeSet) + */ + public Preference(Context context, AttributeSet attrs, int defStyle) { + mContext = context; + + TypedArray a = context.obtainStyledAttributes(attrs, + com.android.internal.R.styleable.Preference); + if (a.hasValue(com.android.internal.R.styleable.Preference_layout) || + a.hasValue(com.android.internal.R.styleable.Preference_widgetLayout)) { + // This preference has a custom layout defined (not one taken from + // the default style) + mHasSpecifiedLayout = true; + } + a.recycle(); + + a = context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.Preference, + defStyle, 0); + for (int i = a.getIndexCount(); i >= 0; i--) { + int attr = a.getIndex(i); + switch (attr) { + case com.android.internal.R.styleable.Preference_key: + mKey = a.getString(attr); + break; + + case com.android.internal.R.styleable.Preference_title: + mTitle = a.getString(attr); + break; + + case com.android.internal.R.styleable.Preference_summary: + mSummary = a.getString(attr); + break; + + case com.android.internal.R.styleable.Preference_order: + mOrder = a.getInt(attr, mOrder); + break; + + case com.android.internal.R.styleable.Preference_layout: + mLayoutResId = a.getResourceId(attr, mLayoutResId); + break; + + case com.android.internal.R.styleable.Preference_widgetLayout: + mWidgetLayoutResId = a.getResourceId(attr, mWidgetLayoutResId); + break; + + case com.android.internal.R.styleable.Preference_enabled: + mEnabled = a.getBoolean(attr, true); + break; + + case com.android.internal.R.styleable.Preference_selectable: + mSelectable = a.getBoolean(attr, true); + break; + + case com.android.internal.R.styleable.Preference_persistent: + mPersistent = a.getBoolean(attr, mPersistent); + break; + + case com.android.internal.R.styleable.Preference_dependency: + mDependencyKey = a.getString(attr); + break; + + case com.android.internal.R.styleable.Preference_defaultValue: + mDefaultValue = onGetDefaultValue(a, attr); + break; + + case com.android.internal.R.styleable.Preference_shouldDisableView: + mShouldDisableView = a.getBoolean(attr, mShouldDisableView); + break; + } + } + a.recycle(); + } + + /** + * Constructor that is called when inflating a preference from XML. This is + * called when a preference is being constructed from an XML file, supplying + * attributes that were specified in the XML file. This version uses a + * default style of 0, so the only attribute values applied are those in the + * Context's Theme and the given AttributeSet. + * + * @param context The Context this is associated with, through which it can + * access the current theme, resources, {@link SharedPreferences}, + * etc. + * @param attrs The attributes of the XML tag that is inflating the + * preference. + * @see #Preference(Context, AttributeSet, int) + */ + public Preference(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + /** + * Constructor to create a Preference. + * + * @param context The context to store preference values. + */ + public Preference(Context context) { + this(context, null); + } + + /** + * Called when {@link Preference} is being inflated and the default value + * attribute needs to be read. Since different preference types have + * different value types, the subclass should get and return the default + * value which will be its value type. + * <p> + * For example, if the value type is String, the body of the method would + * proxy to {@link TypedArray#getString(int)}. + * + * @param a The set of attributes. + * @param index The index of the default value attribute. + * @return The default value of this preference type. + */ + protected Object onGetDefaultValue(TypedArray a, int index) { + return null; + } + + /** + * Sets an {@link Intent} to be used for + * {@link Context#startActivity(Intent)} when the preference is clicked. + * + * @param intent The intent associated with the preference. + */ + public void setIntent(Intent intent) { + mIntent = intent; + } + + /** + * Return the {@link Intent} associated with this preference. + * + * @return The {@link Intent} last set via {@link #setIntent(Intent)} or XML. + */ + public Intent getIntent() { + return mIntent; + } + + /** + * Sets the layout resource that is inflated as the {@link View} to be shown + * for this preference. In most cases, the default layout is sufficient for + * custom preferences and only the widget layout needs to be changed. + * <p> + * This layout should contain a {@link ViewGroup} with ID + * {@link android.R.id#widget_frame} to be the parent of the specific widget + * for this preference. It should similarly contain + * {@link android.R.id#title} and {@link android.R.id#summary}. + * + * @param layoutResId The layout resource ID to be inflated and returned as + * a {@link View}. + * @see #setWidgetLayoutResource(int) + */ + public void setLayoutResource(int layoutResId) { + + if (!mHasSpecifiedLayout) { + mHasSpecifiedLayout = true; + } + + mLayoutResId = layoutResId; + } + + /** + * Gets the layout resource that will be shown as the {@link View} for this preference. + * + * @return The layout resource ID. + */ + public int getLayoutResource() { + return mLayoutResId; + } + + /** + * Sets The layout for the controllable widget portion of a preference. This + * is inflated into the main layout. For example, a checkbox preference + * would specify a custom layout (consisting of just the CheckBox) here, + * instead of creating its own main layout. + * + * @param widgetLayoutResId The layout resource ID to be inflated into the + * main layout. + * @see #setLayoutResource(int) + */ + public void setWidgetLayoutResource(int widgetLayoutResId) { + mWidgetLayoutResId = widgetLayoutResId; + } + + /** + * Gets the layout resource for the controllable widget portion of a preference. + * + * @return The layout resource ID. + */ + public int getWidgetLayoutResource() { + return mWidgetLayoutResId; + } + + /** + * Gets the View that will be shown in the {@link PreferenceActivity}. + * + * @param convertView The old view to reuse, if possible. Note: You should + * check that this view is non-null and of an appropriate type + * before using. If it is not possible to convert this view to + * display the correct data, this method can create a new view. + * @param parent The parent that this view will eventually be attached to. + * @return Returns the same Preference object, for chaining multiple calls + * into a single statement. + * @see #onCreateView(ViewGroup) + * @see #onBindView(View) + */ + public View getView(View convertView, ViewGroup parent) { + if (convertView == null) { + convertView = onCreateView(parent); + } + onBindView(convertView); + return convertView; + } + + /** + * Creates the View to be shown for this preference in the + * {@link PreferenceActivity}. The default behavior is to inflate the main + * layout of this preference (see {@link #setLayoutResource(int)}. If + * changing this behavior, please specify a {@link ViewGroup} with ID + * {@link android.R.id#widget_frame}. + * <p> + * Make sure to call through to the superclass's implementation. + * + * @param parent The parent that this view will eventually be attached to. + * @return The View that displays this preference. + * @see #onBindView(View) + */ + protected View onCreateView(ViewGroup parent) { + final LayoutInflater layoutInflater = + (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + + final View layout = layoutInflater.inflate(mLayoutResId, parent, false); + + if (mWidgetLayoutResId != 0) { + final ViewGroup widgetFrame = (ViewGroup)layout.findViewById(com.android.internal.R.id.widget_frame); + layoutInflater.inflate(mWidgetLayoutResId, widgetFrame); + } + + return layout; + } + + /** + * Binds the created View to the data for the preference. + * <p> + * This is a good place to grab references to custom Views in the layout and + * set properties on them. + * <p> + * Make sure to call through to the superclass's implementation. + * + * @param view The View that shows this preference. + * @see #onCreateView(ViewGroup) + */ + protected void onBindView(View view) { + TextView textView = (TextView) view.findViewById(com.android.internal.R.id.title); + if (textView != null) { + textView.setText(getTitle()); + } + + textView = (TextView) view.findViewById(com.android.internal.R.id.summary); + if (textView != null) { + final CharSequence summary = getSummary(); + if (!TextUtils.isEmpty(summary)) { + if (textView.getVisibility() != View.VISIBLE) { + textView.setVisibility(View.VISIBLE); + } + + textView.setText(getSummary()); + } else { + if (textView.getVisibility() != View.GONE) { + textView.setVisibility(View.GONE); + } + } + } + + if (mShouldDisableView) { + setEnabledStateOnViews(view, mEnabled); + } + } + + /** + * Makes sure the view (and any children) get the enabled state changed. + */ + private void setEnabledStateOnViews(View v, boolean enabled) { + v.setEnabled(enabled); + + if (v instanceof ViewGroup) { + final ViewGroup vg = (ViewGroup) v; + for (int i = vg.getChildCount() - 1; i >= 0; i--) { + setEnabledStateOnViews(vg.getChildAt(i), enabled); + } + } + } + + /** + * Sets the order of this {@link Preference} with respect to other + * {@link Preference} on the same level. If this is not specified, the + * default behavior is to sort alphabetically. The + * {@link PreferenceGroup#setOrderingAsAdded(boolean)} can be used to order + * preferences based on the order they appear in the XML. + * + * @param order The order for this preference. A lower value will be shown + * first. Use {@link #DEFAULT_ORDER} to sort alphabetically or + * allow ordering from XML. + * @see PreferenceGroup#setOrderingAsAdded(boolean) + * @see #DEFAULT_ORDER + */ + public void setOrder(int order) { + if (order != mOrder) { + mOrder = order; + + // Reorder the list + notifyHierarchyChanged(); + } + } + + /** + * Gets the order of this {@link Preference}. + * + * @return The order of this {@link Preference}. + * @see #setOrder(int) + */ + public int getOrder() { + return mOrder; + } + + /** + * Sets the title for the preference. This title will be placed into the ID + * {@link android.R.id#title} within the View created by + * {@link #onCreateView(ViewGroup)}. + * + * @param title The title of the preference. + */ + public void setTitle(CharSequence title) { + if (title == null && mTitle != null || title != null && !title.equals(mTitle)) { + mTitle = title; + notifyChanged(); + } + } + + /** + * @see #setTitle(CharSequence) + * @param titleResId The title as a resource ID. + */ + public void setTitle(int titleResId) { + setTitle(mContext.getString(titleResId)); + } + + /** + * Returns the title of the preference. + * + * @return The title. + * @see #setTitle(CharSequence) + */ + public CharSequence getTitle() { + return mTitle; + } + + /** + * Returns the summary of the preference. + * + * @return The summary. + * @see #setSummary(CharSequence) + */ + public CharSequence getSummary() { + return mSummary; + } + + /** + * Sets the summary for the preference. This summary will be placed into the + * ID {@link android.R.id#summary} within the View created by + * {@link #onCreateView(ViewGroup)}. + * + * @param summary The summary of the preference. + */ + public void setSummary(CharSequence summary) { + if (summary == null && mSummary != null || summary != null && !summary.equals(mSummary)) { + mSummary = summary; + notifyChanged(); + } + } + + /** + * @see #setSummary(CharSequence) + * @param summaryResId The summary as a resource. + */ + public void setSummary(int summaryResId) { + setSummary(mContext.getString(summaryResId)); + } + + /** + * Sets whether this preference is enabled. If disabled, the preference will + * not handle clicks. + * + * @param enabled Whether the preference is enabled. + */ + public void setEnabled(boolean enabled) { + if (mEnabled != enabled) { + mEnabled = enabled; + + // Enabled state can change dependent preferences' states, so notify + notifyDependencyChange(shouldDisableDependents()); + + notifyChanged(); + } + } + + /** + * Whether this {@link Preference} should be enabled in the list. + * + * @return Whether this preference is enabled. + */ + public boolean isEnabled() { + return mEnabled; + } + + /** + * Sets whether this preference is selectable. + * + * @param selectable Whether the preference is selectable. + */ + public void setSelectable(boolean selectable) { + if (mSelectable != selectable) { + mSelectable = selectable; + notifyChanged(); + } + } + + /** + * Whether this {@link Preference} should be selectable in the list. + * + * @return Whether this preference is selectable. + */ + public boolean isSelectable() { + return mSelectable; + } + + /** + * Sets whether this {@link Preference} should disable its view when it gets + * disabled. + * <p> + * For example, set this and {@link #setEnabled(boolean)} to false for + * preferences that are only displaying information and 1) should not be + * clickable 2) should not have the view set to the disabled state. + * + * @param shouldDisableView Whether this preference should disable its view + * when the preference is disabled. + */ + public void setShouldDisableView(boolean shouldDisableView) { + mShouldDisableView = shouldDisableView; + notifyChanged(); + } + + /** + * @see #setShouldDisableView(boolean) + * @return Whether this preference should disable its view when it is disabled. + */ + public boolean getShouldDisableView() { + return mShouldDisableView; + } + + /** + * Returns a unique ID for this preference. This ID should be unique across all + * preferences in a hierarchy. + * + * @return A unique ID for this preference. + */ + long getId() { + return mId; + } + + /** + * Processes a click on the preference. This includes saving the value to + * the {@link SharedPreferences}. However, the overridden method should + * call {@link #callChangeListener(Object)} to make sure the client wants to + * update the preference's state with the new value. + */ + protected void onClick() { + } + + /** + * Sets the key for the preference which is used as a key to the + * {@link SharedPreferences}. This should be unique for the package. + * + * @param key The key for the preference. + * @see #getId() + */ + public void setKey(String key) { + mKey = key; + + if (mRequiresKey && !hasKey()) { + requireKey(); + } + } + + /** + * Gets the key for the preference, which is also the key used for storing + * values into SharedPreferences. + * + * @return The key. + */ + public String getKey() { + return mKey; + } + + /** + * Checks whether the key is present, and if it isn't throws an + * exception. This should be called by subclasses that store preferences in + * the {@link SharedPreferences}. + */ + void requireKey() { + if (mKey == null) { + throw new IllegalStateException("Preference does not have a key assigned."); + } + + mRequiresKey = true; + } + + /** + * Returns whether this {@link Preference} has a valid key. + * + * @return Whether the key exists and is not a blank string. + */ + public boolean hasKey() { + return !TextUtils.isEmpty(mKey); + } + + /** + * Returns whether this {@link Preference} is persistent. If it is persistent, it stores its value(s) into + * the persistent {@link SharedPreferences} storage. + * + * @return Whether this is persistent. + */ + public boolean isPersistent() { + return mPersistent; + } + + /** + * Convenience method of whether at the given time this method is called, + * the {@link Preference} should store/restore its value(s) into the + * {@link SharedPreferences}. This, at minimum, checks whether the + * {@link Preference} is persistent and it currently has a key. Before you + * save/restore from the {@link SharedPreferences}, check this first. + * + * @return Whether to persist the value. + */ + protected boolean shouldPersist() { + return mPreferenceManager != null && isPersistent() && hasKey(); + } + + /** + * Sets whether this {@link Preference} is persistent. If it is persistent, + * it stores its value(s) into the persistent {@link SharedPreferences} + * storage. + * + * @param persistent Whether it should store its value(s) into the {@link SharedPreferences}. + */ + public void setPersistent(boolean persistent) { + mPersistent = persistent; + } + + /** + * Call this method after the user changes the preference, but before the + * internal state is set. This allows the client to ignore the user value. + * + * @param newValue The new value of the preference. + * @return Whether or not the user value should be set as the preference + * value (and persisted). + */ + protected boolean callChangeListener(Object newValue) { + return mOnChangeListener == null ? true : mOnChangeListener.onPreferenceChange(this, newValue); + } + + /** + * Sets the callback to be invoked when this preference is changed by the + * user (but before the internal state has been updated). + * + * @param onPreferenceChangeListener The callback to be invoked. + */ + public void setOnPreferenceChangeListener(OnPreferenceChangeListener onPreferenceChangeListener) { + mOnChangeListener = onPreferenceChangeListener; + } + + /** + * Gets the callback to be invoked when this preference is changed by the + * user (but before the internal state has been updated). + * + * @return The callback to be invoked. + */ + public OnPreferenceChangeListener getOnPreferenceChangeListener() { + return mOnChangeListener; + } + + /** + * Sets the callback to be invoked when this preference is clicked. + * + * @param onPreferenceClickListener The callback to be invoked. + */ + public void setOnPreferenceClickListener(OnPreferenceClickListener onPreferenceClickListener) { + mOnClickListener = onPreferenceClickListener; + } + + /** + * Gets the callback to be invoked when this preference is clicked. + * + * @return The callback to be invoked. + */ + public OnPreferenceClickListener getOnPreferenceClickListener() { + return mOnClickListener; + } + + /** + * Called when a click should be performed. + * + * @param preferenceScreen Optional {@link PreferenceScreen} whose hierarchy click + * listener should be called in the proper order (between other + * processing). + */ + void performClick(PreferenceScreen preferenceScreen) { + + if (!isEnabled()) { + return; + } + + onClick(); + + if (mOnClickListener != null && mOnClickListener.onPreferenceClick(this)) { + return; + } + + PreferenceManager preferenceManager = getPreferenceManager(); + if (preferenceManager != null) { + PreferenceManager.OnPreferenceTreeClickListener listener = preferenceManager + .getOnPreferenceTreeClickListener(); + if (preferenceScreen != null && listener != null + && listener.onPreferenceTreeClick(preferenceScreen, this)) { + return; + } + } + + if (mIntent != null) { + Context context = getContext(); + context.startActivity(mIntent); + } + } + + /** + * Returns the context of this preference. Each preference in a preference hierarchy can be + * from different context (for example, if multiple activities provide preferences into a single + * {@link PreferenceActivity}). This context will be used to save the preference valus. + * + * @return The context of this preference. + */ + public Context getContext() { + return mContext; + } + + /** + * Returns the {@link SharedPreferences} where this preference can read its + * value(s). Usually, it's easier to use one of the helper read methods: + * {@link #getPersistedBoolean(boolean)}, {@link #getPersistedFloat(float)}, + * {@link #getPersistedInt(int)}, {@link #getPersistedLong(long)}, + * {@link #getPersistedString(String)}. To save values, see + * {@link #getEditor()}. + * <p> + * In some cases, writes to the {@link #getEditor()} will not be committed + * right away and hence not show up in the returned + * {@link SharedPreferences}, this is intended behavior to improve + * performance. + * + * @return The {@link SharedPreferences} where this preference reads its + * value(s), or null if it isn't attached to a preference hierarchy. + * @see #getEditor() + */ + public SharedPreferences getSharedPreferences() { + if (mPreferenceManager == null) { + return null; + } + + return mPreferenceManager.getSharedPreferences(); + } + + /** + * Returns an {@link SharedPreferences.Editor} where this preference can + * save its value(s). Usually it's easier to use one of the helper save + * methods: {@link #persistBoolean(boolean)}, {@link #persistFloat(float)}, + * {@link #persistInt(int)}, {@link #persistLong(long)}, + * {@link #persistString(String)}. To read values, see + * {@link #getSharedPreferences()}. If {@link #shouldCommit()} returns + * true, it is this Preference's responsibility to commit. + * <p> + * In some cases, writes to this will not be committed right away and hence + * not show up in the shared preferences, this is intended behavior to + * improve performance. + * + * @return A {@link SharedPreferences.Editor} where this preference saves + * its value(s), or null if it isn't attached to a preference + * hierarchy. + * @see #shouldCommit() + * @see #getSharedPreferences() + */ + public SharedPreferences.Editor getEditor() { + if (mPreferenceManager == null) { + return null; + } + + return mPreferenceManager.getEditor(); + } + + /** + * Returns whether the {@link Preference} should commit its saved value(s) in + * {@link #getEditor()}. This may return false in situations where batch + * committing is being done (by the manager) to improve performance. + * + * @return Whether the Preference should commit its saved value(s). + * @see #getEditor() + */ + public boolean shouldCommit() { + if (mPreferenceManager == null) { + return false; + } + + return mPreferenceManager.shouldCommit(); + } + + /** + * Compares preferences based on order (if set), otherwise alphabetically on title. + * <p> + * {@inheritDoc} + */ + public int compareTo(Preference another) { + if (mOrder != DEFAULT_ORDER + || (mOrder == DEFAULT_ORDER && another.mOrder != DEFAULT_ORDER)) { + // Do order comparison + return mOrder - another.mOrder; + } else if (mTitle == null) { + return 1; + } else if (another.mTitle == null) { + return -1; + } else { + // Do name comparison + return CharSequences.compareToIgnoreCase(mTitle, another.mTitle); + } + } + + /** + * Sets the internal change listener. + * + * @param listener The listener. + * @see #notifyChanged() + */ + final void setOnPreferenceChangeInternalListener(OnPreferenceChangeInternalListener listener) { + mListener = listener; + } + + /** + * Should be called when the data of this {@link Preference} has changed. + */ + protected void notifyChanged() { + if (mListener != null) { + mListener.onPreferenceChange(this); + } + } + + /** + * Should be called this is a group a {@link Preference} has been + * added/removed from this group, or the ordering should be + * re-evaluated. + */ + protected void notifyHierarchyChanged() { + if (mListener != null) { + mListener.onPreferenceHierarchyChange(this); + } + } + + /** + * Gets the {@link PreferenceManager} that manages this preference's tree. + * + * @return The {@link PreferenceManager}. + */ + public PreferenceManager getPreferenceManager() { + return mPreferenceManager; + } + + /** + * Called when this preference has been attached to a preference hierarchy. + * Make sure to call the super implementation. + * + * @param preferenceManager The preference manager of the hierarchy. + */ + protected void onAttachedToHierarchy(PreferenceManager preferenceManager) { + mPreferenceManager = preferenceManager; + + mId = preferenceManager.getNextId(); + + dispatchSetInitialValue(); + } + + /** + * Called when the preference hierarchy has been attached to the + * {@link PreferenceActivity}. This can also be called when this + * {@link Preference} has been attached to a group that was already attached + * to the {@link PreferenceActivity}. + */ + protected void onAttachedToActivity() { + // At this point, the hierarchy that this preference is in is connected + // with all other preferences. + registerDependency(); + } + + private void registerDependency() { + + if (TextUtils.isEmpty(mDependencyKey)) return; + + Preference preference = findPreferenceInHierarchy(mDependencyKey); + if (preference != null) { + preference.registerDependent(this); + } else { + throw new IllegalStateException("Dependency \"" + mDependencyKey + + "\" not found for preference \"" + mKey + "\" (title: \"" + mTitle + "\""); + } + } + + private void unregisterDependency() { + if (mDependencyKey != null) { + final Preference oldDependency = findPreferenceInHierarchy(mDependencyKey); + if (oldDependency != null) { + oldDependency.unregisterDependent(this); + } + } + } + + /** + * Find a Preference in this hierarchy (the whole thing, + * even above/below your {@link PreferenceScreen} screen break) with the given + * key. + * <p> + * This only functions after we have been attached to a hierarchy. + * + * @param key The key of the {@link Preference} to find. + * @return The {@link Preference} object of a preference + * with the given key. + */ + protected Preference findPreferenceInHierarchy(String key) { + if (TextUtils.isEmpty(key) || mPreferenceManager == null) { + return null; + } + + return mPreferenceManager.findPreference(key); + } + + /** + * Adds a dependent Preference on this preference so we can notify it. + * Usually, the dependent preference registers itself (it's good for it to + * know it depends on something), so please use + * {@link Preference#setDependency(String)} on the dependent preference. + * + * @param dependent The dependent Preference that will be enabled/disabled + * according to the state of this preference. + */ + private void registerDependent(Preference dependent) { + if (mDependents == null) { + mDependents = new ArrayList<Preference>(); + } + + mDependents.add(dependent); + + dependent.onDependencyChanged(this, shouldDisableDependents()); + } + + /** + * Removes a dependent Preference on this preference. + * + * @param dependent The dependent Preference that will be enabled/disabled + * according to the state of this preference. + * @return Returns the same Preference object, for chaining multiple calls + * into a single statement. + */ + private void unregisterDependent(Preference dependent) { + if (mDependents != null) { + mDependents.remove(dependent); + } + } + + /** + * Notifies any listening dependents of a change that affects the + * dependency. + * + * @param disableDependents Whether this {@link Preference} should disable + * its dependents. + */ + public void notifyDependencyChange(boolean disableDependents) { + final List<Preference> dependents = mDependents; + + if (dependents == null) { + return; + } + + final int dependentsCount = dependents.size(); + for (int i = 0; i < dependentsCount; i++) { + dependents.get(i).onDependencyChanged(this, disableDependents); + } + } + + /** + * Called when the dependency changes. + * + * @param dependency The preference that this preference depends on. + * @param disableDependent Whether to disable this preference. + */ + public void onDependencyChanged(Preference dependency, boolean disableDependent) { + setEnabled(!disableDependent); + } + + /** + * Should return whether this preference's dependents should currently be + * disabled. + * + * @return True if the dependents should be disabled, otherwise false. + */ + public boolean shouldDisableDependents() { + return !isEnabled(); + } + + /** + * Sets the key of a Preference that this Preference will depend on. If that + * Preference is not set or is off, this Preference will be disabled. + * + * @param dependencyKey The key of the Preference that this depends on. + */ + public void setDependency(String dependencyKey) { + // Unregister the old dependency, if we had one + unregisterDependency(); + + // Register the new + mDependencyKey = dependencyKey; + registerDependency(); + } + + /** + * Returns the key of the dependency on this preference. + * + * @return The key of the dependency. + * @see #setDependency(String) + */ + public String getDependency() { + return mDependencyKey; + } + + /** + * Called when this Preference is being removed from the hierarchy. You + * should remove any references to this Preference that you know about. Make + * sure to call through to the superclass implementation. + */ + protected void onPrepareForRemoval() { + unregisterDependency(); + } + + /** + * Sets the default value for the preference, which will be set either if + * persistence is off or persistence is on and the preference is not found + * in the persistent storage. + * + * @param defaultValue The default value. + */ + public void setDefaultValue(Object defaultValue) { + mDefaultValue = defaultValue; + } + + private void dispatchSetInitialValue() { + // By now, we know if we are persistent. + final boolean shouldPersist = shouldPersist(); + if (!shouldPersist || !getSharedPreferences().contains(mKey)) { + if (mDefaultValue != null) { + onSetInitialValue(false, mDefaultValue); + } + } else { + onSetInitialValue(true, null); + } + } + + /** + * Implement this to set the initial value of the Preference. If the + * restoreValue flag is true, you should restore the value from the shared + * preferences. If false, you should set (and possibly store to shared + * preferences if {@link #shouldPersist()}) to defaultValue. + * <p> + * This may not always be called. One example is if it should not persist + * but there is no default value given. + * + * @param restorePersistedValue Whether to restore the persisted value + * (true), or use the given default value (false). + * @param defaultValue The default value. Only use if restoreValue is false. + */ + protected void onSetInitialValue(boolean restorePersistedValue, Object defaultValue) { + } + + private void tryCommit(SharedPreferences.Editor editor) { + if (mPreferenceManager.shouldCommit()) { + editor.commit(); + } + } + + /** + * Attempts to persist a String to the SharedPreferences. + * <p> + * This will check if the Preference is persistent, get an editor from + * the preference manager, put the string, check if we should commit (and + * commit if so). + * + * @param value The value to persist. + * @return Whether the Preference is persistent. (This is not whether the + * value was persisted, since we may not necessarily commit if there + * will be a batch commit later.) + * @see #getPersistedString(String) + */ + protected boolean persistString(String value) { + if (shouldPersist()) { + // Shouldn't store null + if (value == getPersistedString(null)) { + // It's already there, so the same as persisting + return true; + } + + SharedPreferences.Editor editor = mPreferenceManager.getEditor(); + editor.putString(mKey, value); + tryCommit(editor); + return true; + } + return false; + } + + /** + * Attempts to get a persisted String from the SharedPreferences. + * <p> + * This will check if the Preference is persistent, get the shared + * preferences from the preference manager, get the value. + * + * @param defaultReturnValue The default value to return if either the + * Preference is not persistent or the Preference is not in the + * shared preferences. + * @return The value from the shared preferences or the default return + * value. + * @see #persistString(String) + */ + protected String getPersistedString(String defaultReturnValue) { + if (!shouldPersist()) { + return defaultReturnValue; + } + + return mPreferenceManager.getSharedPreferences().getString(mKey, defaultReturnValue); + } + + /** + * Attempts to persist an int to the SharedPreferences. + * + * @param value The value to persist. + * @return Whether the Preference is persistent. (This is not whether the + * value was persisted, since we may not necessarily commit if there + * will be a batch commit later.) + * @see #persistString(String) + * @see #getPersistedInt(int) + */ + protected boolean persistInt(int value) { + if (shouldPersist()) { + if (value == getPersistedInt(~value)) { + // It's already there, so the same as persisting + return true; + } + + SharedPreferences.Editor editor = mPreferenceManager.getEditor(); + editor.putInt(mKey, value); + tryCommit(editor); + return true; + } + return false; + } + + /** + * Attempts to get a persisted int from the SharedPreferences. + * + * @param defaultReturnValue The default value to return if either the + * Preference is not persistent or the Preference is not in the + * shared preferences. + * @return The value from the shared preferences or the default return + * value. + * @see #getPersistedString(String) + * @see #persistInt(int) + */ + protected int getPersistedInt(int defaultReturnValue) { + if (!shouldPersist()) { + return defaultReturnValue; + } + + return mPreferenceManager.getSharedPreferences().getInt(mKey, defaultReturnValue); + } + + /** + * Attempts to persist a float to the SharedPreferences. + * + * @param value The value to persist. + * @return Whether the Preference is persistent. (This is not whether the + * value was persisted, since we may not necessarily commit if there + * will be a batch commit later.) + * @see #persistString(String) + * @see #getPersistedFloat(float) + */ + protected boolean persistFloat(float value) { + if (shouldPersist()) { + if (value == getPersistedFloat(Float.NaN)) { + // It's already there, so the same as persisting + return true; + } + + SharedPreferences.Editor editor = mPreferenceManager.getEditor(); + editor.putFloat(mKey, value); + tryCommit(editor); + return true; + } + return false; + } + + /** + * Attempts to get a persisted float from the SharedPreferences. + * + * @param defaultReturnValue The default value to return if either the + * Preference is not persistent or the Preference is not in the + * shared preferences. + * @return The value from the shared preferences or the default return + * value. + * @see #getPersistedString(String) + * @see #persistFloat(float) + */ + protected float getPersistedFloat(float defaultReturnValue) { + if (!shouldPersist()) { + return defaultReturnValue; + } + + return mPreferenceManager.getSharedPreferences().getFloat(mKey, defaultReturnValue); + } + + /** + * Attempts to persist a long to the SharedPreferences. + * + * @param value The value to persist. + * @return Whether the Preference is persistent. (This is not whether the + * value was persisted, since we may not necessarily commit if there + * will be a batch commit later.) + * @see #persistString(String) + * @see #getPersistedLong(long) + */ + protected boolean persistLong(long value) { + if (shouldPersist()) { + if (value == getPersistedLong(~value)) { + // It's already there, so the same as persisting + return true; + } + + SharedPreferences.Editor editor = mPreferenceManager.getEditor(); + editor.putLong(mKey, value); + tryCommit(editor); + return true; + } + return false; + } + + /** + * Attempts to get a persisted long from the SharedPreferences. + * + * @param defaultReturnValue The default value to return if either the + * Preference is not persistent or the Preference is not in the + * shared preferences. + * @return The value from the shared preferences or the default return + * value. + * @see #getPersistedString(String) + * @see #persistLong(long) + */ + protected long getPersistedLong(long defaultReturnValue) { + if (!shouldPersist()) { + return defaultReturnValue; + } + + return mPreferenceManager.getSharedPreferences().getLong(mKey, defaultReturnValue); + } + + /** + * Attempts to persist a boolean to the SharedPreferences. + * + * @param value The value to persist. + * @return Whether the Preference is persistent. (This is not whether the + * value was persisted, since we may not necessarily commit if there + * will be a batch commit later.) + * @see #persistString(String) + * @see #getPersistedBoolean(boolean) + */ + protected boolean persistBoolean(boolean value) { + if (shouldPersist()) { + if (value == getPersistedBoolean(!value)) { + // It's already there, so the same as persisting + return true; + } + + SharedPreferences.Editor editor = mPreferenceManager.getEditor(); + editor.putBoolean(mKey, value); + tryCommit(editor); + return true; + } + return false; + } + + /** + * Attempts to get a persisted boolean from the SharedPreferences. + * + * @param defaultReturnValue The default value to return if either the + * Preference is not persistent or the Preference is not in the + * shared preferences. + * @return The value from the shared preferences or the default return + * value. + * @see #getPersistedString(String) + * @see #persistBoolean(boolean) + */ + protected boolean getPersistedBoolean(boolean defaultReturnValue) { + if (!shouldPersist()) { + return defaultReturnValue; + } + + return mPreferenceManager.getSharedPreferences().getBoolean(mKey, defaultReturnValue); + } + + boolean hasSpecifiedLayout() { + return mHasSpecifiedLayout; + } + + @Override + public String toString() { + return getFilterableStringBuilder().toString(); + } + + /** + * Returns the text that will be used to filter this preference depending on + * user input. + * <p> + * If overridding and calling through to the superclass, make sure to prepend + * your additions with a space. + * + * @return Text as a {@link StringBuilder} that will be used to filter this + * preference. By default, this is the title and summary + * (concatenated with a space). + */ + StringBuilder getFilterableStringBuilder() { + StringBuilder sb = new StringBuilder(); + CharSequence title = getTitle(); + if (!TextUtils.isEmpty(title)) { + sb.append(title).append(' '); + } + CharSequence summary = getSummary(); + if (!TextUtils.isEmpty(summary)) { + sb.append(summary).append(' '); + } + // Drop the last space + sb.setLength(sb.length() - 1); + return sb; + } + + /** + * Store this preference hierarchy's frozen state into the given container. + * + * @param container The Bundle in which to save the preference's icicles. + * + * @see #restoreHierarchyState + * @see #dispatchSaveInstanceState + * @see #onSaveInstanceState + */ + public void saveHierarchyState(Bundle container) { + dispatchSaveInstanceState(container); + } + + /** + * Called by {@link #saveHierarchyState} to store the icicles for this preference and its children. + * May be overridden to modify how freezing happens to a preference's children; for example, some + * preferences may want to not store icicles for their children. + * + * @param container The Bundle in which to save the preference's icicles. + * + * @see #dispatchRestoreInstanceState + * @see #saveHierarchyState + * @see #onSaveInstanceState + */ + void dispatchSaveInstanceState(Bundle container) { + if (hasKey()) { + mBaseMethodCalled = false; + Parcelable state = onSaveInstanceState(); + if (!mBaseMethodCalled) { + throw new IllegalStateException( + "Derived class did not call super.onSaveInstanceState()"); + } + if (state != null) { + container.putParcelable(mKey, state); + } + } + } + + /** + * Hook allowing a preference to generate a representation of its internal + * state that can later be used to create a new instance with that same + * state. This state should only contain information that is not persistent + * or can be reconstructed later. + * + * @return Returns a Parcelable object containing the preference's current + * dynamic state, or null if there is nothing interesting to save. + * The default implementation returns null. + * @see #onRestoreInstanceState + * @see #saveHierarchyState + * @see #dispatchSaveInstanceState + */ + protected Parcelable onSaveInstanceState() { + mBaseMethodCalled = true; + return BaseSavedState.EMPTY_STATE; + } + + /** + * Restore this preference hierarchy's frozen state from the given container. + * + * @param container The Bundle which holds previously frozen icicles. + * + * @see #saveHierarchyState + * @see #dispatchRestoreInstanceState + * @see #onRestoreInstanceState + */ + public void restoreHierarchyState(Bundle container) { + dispatchRestoreInstanceState(container); + } + + /** + * Called by {@link #restoreHierarchyState} to retrieve the icicles for this + * preference and its children. May be overridden to modify how restoreing + * happens to a preference's children; for example, some preferences may + * want to not store icicles for their children. + * + * @param container The Bundle which holds previously frozen icicles. + * @see #dispatchSaveInstanceState + * @see #restoreHierarchyState + * @see #onRestoreInstanceState + */ + void dispatchRestoreInstanceState(Bundle container) { + if (hasKey()) { + Parcelable state = container.getParcelable(mKey); + if (state != null) { + mBaseMethodCalled = false; + onRestoreInstanceState(state); + if (!mBaseMethodCalled) { + throw new IllegalStateException( + "Derived class did not call super.onRestoreInstanceState()"); + } + } + } + } + + /** + * Hook allowing a preference to re-apply a representation of its internal + * state that had previously been generated by {@link #onSaveInstanceState}. + * This function will never be called with a null icicle. + * + * @param state The frozen state that had previously been returned by + * {@link #onSaveInstanceState}. + * @see #onSaveInstanceState + * @see #restoreHierarchyState + * @see #dispatchRestoreInstanceState + */ + protected void onRestoreInstanceState(Parcelable state) { + mBaseMethodCalled = true; + if (state != BaseSavedState.EMPTY_STATE && state != null) { + throw new IllegalArgumentException("Wrong state class -- expecting Preference State"); + } + } + + public static class BaseSavedState extends AbsSavedState { + public BaseSavedState(Parcel source) { + super(source); + } + + public BaseSavedState(Parcelable superState) { + super(superState); + } + + public static final Parcelable.Creator<BaseSavedState> CREATOR = + new Parcelable.Creator<BaseSavedState>() { + public BaseSavedState createFromParcel(Parcel in) { + return new BaseSavedState(in); + } + + public BaseSavedState[] newArray(int size) { + return new BaseSavedState[size]; + } + }; + } + +} diff --git a/core/java/android/preference/PreferenceActivity.java b/core/java/android/preference/PreferenceActivity.java new file mode 100644 index 0000000..98144ca --- /dev/null +++ b/core/java/android/preference/PreferenceActivity.java @@ -0,0 +1,284 @@ +/* + * 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 android.app.Activity; +import android.app.ListActivity; +import android.content.Intent; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.view.View; +import android.view.Window; + +/** + * The {@link PreferenceActivity} activity shows a hierarchy of preferences as + * lists, possibly spanning multiple screens. These preferences will + * automatically save to {@link SharedPreferences} as the user interacts with + * them. To retrieve an instance of {@link SharedPreferences} that the + * preference hierarchy in this activity will use, call + * {@link PreferenceManager#getDefaultSharedPreferences(android.content.Context)} + * with a context in the same package as this activity. + * <p> + * Furthermore, the preferences shown will follow the visual style of system + * preferences. It is easy to create a hierarchy of preferences (that can be + * shown on multiple screens) via XML. For these reasons, it is recommended to + * use this activity (as a superclass) to deal with preferences in applications. + * <p> + * A {@link PreferenceScreen} object should be at the top of the preference + * hierarchy. Furthermore, subsequent {@link PreferenceScreen} in the hierarchy + * denote a screen break--that is the preferences contained within subsequent + * {@link PreferenceScreen} should be shown on another screen. The preference + * framework handles showing these other screens from the preference hierarchy. + * <p> + * The preference hierarchy can be formed in multiple ways: + * <li> From an XML file specifying the hierarchy + * <li> From different {@link Activity Activities} that each specify its own + * preferences in an XML file via {@link Activity} meta-data + * <li> From an object hierarchy rooted with {@link PreferenceScreen} + * <p> + * To inflate from XML, use the {@link #addPreferencesFromResource(int)}. The + * root element should be a {@link PreferenceScreen}. Subsequent elements can point + * to actual {@link Preference} subclasses. As mentioned above, subsequent + * {@link PreferenceScreen} in the hierarchy will result in the screen break. + * <p> + * To specify an {@link Intent} to query {@link Activity Activities} that each + * have preferences, use {@link #addPreferencesFromIntent}. Each + * {@link Activity} can specify meta-data in the manifest (via the key + * {@link PreferenceManager#METADATA_KEY_PREFERENCES}) that points to an XML + * resource. These XML resources will be inflated into a single preference + * hierarchy and shown by this activity. + * <p> + * To specify an object hierarchy rooted with {@link PreferenceScreen}, use + * {@link #setPreferenceScreen(PreferenceScreen)}. + * <p> + * As a convenience, this activity implements a click listener for any + * preference in the current hierarchy, see + * {@link #onPreferenceTreeClick(PreferenceScreen, Preference)}. + * + * @see Preference + * @see PreferenceScreen + */ +public abstract class PreferenceActivity extends ListActivity implements + PreferenceManager.OnPreferenceTreeClickListener { + + private static final String PREFERENCES_TAG = "android:preferences"; + + private PreferenceManager mPreferenceManager; + + /** + * 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 Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + + case MSG_BIND_PREFERENCES: + bindPreferences(); + break; + } + } + }; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + requestWindowFeature(Window.FEATURE_NO_TITLE); + + setContentView(com.android.internal.R.layout.preference_list_content); + + mPreferenceManager = onCreatePreferenceManager(); + getListView().setScrollBarStyle(View.SCROLLBARS_INSIDE_INSET); + } + + @Override + protected void onStop() { + super.onStop(); + + mPreferenceManager.dispatchActivityStop(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + + mPreferenceManager.dispatchActivityDestroy(); + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + + 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) { + super.onRestoreInstanceState(state); + + Bundle container = state.getBundle(PREFERENCES_TAG); + if (container != null) { + final PreferenceScreen preferenceScreen = getPreferenceScreen(); + if (preferenceScreen != null) { + preferenceScreen.restoreHierarchyState(container); + } + } + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + + mPreferenceManager.dispatchActivityResult(requestCode, resultCode, data); + } + + @Override + public void onContentChanged() { + super.onContentChanged(); + postBindPreferences(); + } + + /** + * 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()); + } + } + + /** + * Creates the {@link PreferenceManager}. + * + * @return The {@link PreferenceManager} used by this activity. + */ + private PreferenceManager onCreatePreferenceManager() { + PreferenceManager preferenceManager = new PreferenceManager(this, FIRST_REQUEST_CODE); + preferenceManager.setOnPreferenceTreeClickListener(this); + return preferenceManager; + } + + /** + * Returns the {@link PreferenceManager} used by this activity. + * @return The {@link PreferenceManager}. + */ + public PreferenceManager getPreferenceManager() { + return mPreferenceManager; + } + + private void requirePreferenceManager() { + if (mPreferenceManager == null) { + throw new RuntimeException("This should be called after super.onCreate."); + } + } + + /** + * Sets the root of the preference hierarchy that this activity is showing. + * + * @param preferenceScreen The root {@link PreferenceScreen} of the preference hierarchy. + */ + public void setPreferenceScreen(PreferenceScreen preferenceScreen) { + if (mPreferenceManager.setPreferences(preferenceScreen) && preferenceScreen != null) { + postBindPreferences(); + } + } + + /** + * Gets the root of the preference hierarchy that this activity is showing. + * + * @return The {@link PreferenceScreen} that is the root of the preference + * hierarchy. + */ + public PreferenceScreen getPreferenceScreen() { + return mPreferenceManager.getPreferenceScreen(); + } + + /** + * Adds preferences from activities that match the given {@link Intent}. + * + * @param intent The {@link Intent} to query activities. + */ + 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. + */ + public void addPreferencesFromResource(int preferencesResId) { + requirePreferenceManager(); + + setPreferenceScreen(mPreferenceManager.inflateFromResource(this, preferencesResId, + getPreferenceScreen())); + } + + /** + * {@inheritDoc} + */ + 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) + */ + 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); + } + } + +} diff --git a/core/java/android/preference/PreferenceCategory.java b/core/java/android/preference/PreferenceCategory.java new file mode 100644 index 0000000..a1b6f09 --- /dev/null +++ b/core/java/android/preference/PreferenceCategory.java @@ -0,0 +1,58 @@ +/* + * 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 java.util.Map; + +import android.content.Context; +import android.util.AttributeSet; + +/** + * The {@link PreferenceCategory} class is used to group {@link Preference}s + * and provide a disabled title above the group. + */ +public class PreferenceCategory extends PreferenceGroup { + private static final String TAG = "PreferenceCategory"; + + public PreferenceCategory(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + public PreferenceCategory(Context context, AttributeSet attrs) { + this(context, attrs, com.android.internal.R.attr.preferenceCategoryStyle); + } + + public PreferenceCategory(Context context) { + this(context, null); + } + + @Override + protected boolean onPrepareAddPreference(Preference preference) { + if (preference instanceof PreferenceCategory) { + throw new IllegalArgumentException( + "Cannot add a " + TAG + " directly to a " + TAG); + } + + return super.onPrepareAddPreference(preference); + } + + @Override + public boolean isEnabled() { + return false; + } + +} diff --git a/core/java/android/preference/PreferenceGroup.java b/core/java/android/preference/PreferenceGroup.java new file mode 100644 index 0000000..55b3753 --- /dev/null +++ b/core/java/android/preference/PreferenceGroup.java @@ -0,0 +1,320 @@ +/* + * 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 java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import android.content.Context; +import android.content.res.TypedArray; +import android.os.Bundle; +import android.os.Parcelable; +import android.util.AttributeSet; + +/** + * The {@link PreferenceGroup} class is a container for multiple + * {@link Preference}s. It is a base class for {@link Preference} that are + * parents, such as {@link PreferenceCategory} and {@link PreferenceScreen}. + * + * @attr ref android.R.styleable#PreferenceGroup_orderingFromXml + */ +public abstract class PreferenceGroup extends Preference implements GenericInflater.Parent<Preference> { + /** + * The container for child {@link Preference}s. This is sorted based on the + * ordering, please use {@link #addPreference(Preference)} instead of adding + * to this directly. + */ + private List<Preference> mPreferenceList; + + private boolean mOrderingAsAdded = true; + + private int mCurrentPreferenceOrder = 0; + + private boolean mAttachedToActivity = false; + + public PreferenceGroup(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + mPreferenceList = new ArrayList<Preference>(); + + TypedArray a = context.obtainStyledAttributes(attrs, + com.android.internal.R.styleable.PreferenceGroup, defStyle, 0); + mOrderingAsAdded = a.getBoolean(com.android.internal.R.styleable.PreferenceGroup_orderingFromXml, + mOrderingAsAdded); + a.recycle(); + } + + public PreferenceGroup(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + /** + * Whether to order the {@link Preference} children of this group as they + * are added. If this is false, the ordering will follow each Preference + * order and default to alphabetic for those without an order. + * <p> + * If this is called after preferences are added, they will not be + * re-ordered in the order they were added, hence call this method early on. + * + * @param orderingAsAdded Whether to order according to the order added. + * @see Preference#setOrder(int) + */ + public void setOrderingAsAdded(boolean orderingAsAdded) { + mOrderingAsAdded = orderingAsAdded; + } + + /** + * Whether this group is ordering preferences in the order they are added. + * + * @return Whether this group orders based on the order the children are added. + * @see #setOrderingAsAdded(boolean) + */ + public boolean isOrderingAsAdded() { + return mOrderingAsAdded; + } + + /** + * Called by the inflater to add an item to this group. + */ + public void addItemFromInflater(Preference preference) { + addPreference(preference); + } + + /** + * Returns the number of children {@link Preference}s. + * @return The number of preference children in this group. + */ + public int getPreferenceCount() { + return mPreferenceList.size(); + } + + /** + * Returns the {@link Preference} at a particular index. + * + * @param index The index of the {@link Preference} to retrieve. + * @return The {@link Preference}. + */ + public Preference getPreference(int index) { + return mPreferenceList.get(index); + } + + /** + * Adds a {@link Preference} at the correct position based on the + * preference's order. + * + * @param preference The preference to add. + * @return Whether the preference is now in this group. + */ + public boolean addPreference(Preference preference) { + if (mPreferenceList.contains(preference)) { + // Exists + return true; + } + + if (preference.getOrder() == Preference.DEFAULT_ORDER) { + if (mOrderingAsAdded) { + preference.setOrder(mCurrentPreferenceOrder++); + } + + if (preference instanceof PreferenceGroup) { + // TODO: fix (method is called tail recursively when inflating, + // so we won't end up properly passing this flag down to children + ((PreferenceGroup)preference).setOrderingAsAdded(mOrderingAsAdded); + } + } + + int insertionIndex = Collections.binarySearch(mPreferenceList, preference); + if (insertionIndex < 0) { + insertionIndex = insertionIndex * -1 - 1; + } + + if (!onPrepareAddPreference(preference)) { + return false; + } + + synchronized(this) { + mPreferenceList.add(insertionIndex, preference); + } + + preference.onAttachedToHierarchy(getPreferenceManager()); + + if (mAttachedToActivity) { + preference.onAttachedToActivity(); + } + + notifyHierarchyChanged(); + + return true; + } + + /** + * Removes a {@link Preference} from this group. + * + * @param preference The preference to remove. + * @return Whether the preference was found and removed. + */ + public boolean removePreference(Preference preference) { + final boolean returnValue = removePreferenceInt(preference); + notifyHierarchyChanged(); + return returnValue; + } + + private boolean removePreferenceInt(Preference preference) { + synchronized(this) { + preference.onPrepareForRemoval(); + return mPreferenceList.remove(preference); + } + } + + /** + * Removes all {@link Preference Preferences} from this group. + */ + public void removeAll() { + synchronized(this) { + List<Preference> preferenceList = mPreferenceList; + for (int i = preferenceList.size() - 1; i >= 0; i--) { + removePreferenceInt(preferenceList.get(0)); + } + } + notifyHierarchyChanged(); + } + + /** + * Prepares a {@link Preference} to be added to the group. + * + * @param preference The preference to add. + * @return Whether to allow adding the preference (true), or not (false). + */ + protected boolean onPrepareAddPreference(Preference preference) { + if (!super.isEnabled()) { + preference.setEnabled(false); + } + + return true; + } + + /** + * Finds a {@link Preference} based on its key. If two {@link Preference} + * share the same key (not recommended), the first to appear will be + * returned (to retrieve the other preference with the same key, call this + * method on the first preference). If this preference has the key, it will + * not be returned. + * <p> + * This will recursively search for the preference into children that are + * also {@link PreferenceGroup PreferenceGroups}. + * + * @param key The key of the preference to retrieve. + * @return The {@link Preference} with the key, or null. + */ + public Preference findPreference(CharSequence key) { + final int preferenceCount = getPreferenceCount(); + for (int i = 0; i < preferenceCount; i++) { + final Preference preference = getPreference(i); + final String curKey = preference.getKey(); + + if (curKey != null && curKey.equals(key)) { + return preference; + } + + if (preference instanceof PreferenceGroup) { + final Preference returnedPreference = ((PreferenceGroup)preference) + .findPreference(key); + if (returnedPreference != null) { + return returnedPreference; + } + } + } + + return null; + } + + /** + * Whether this preference group should be shown on the same screen as its + * contained preferences. + * + * @return True if the contained preferences should be shown on the same + * screen as this preference. + */ + protected boolean isOnSameScreenAsChildren() { + return true; + } + + @Override + protected void onAttachedToActivity() { + super.onAttachedToActivity(); + + // Mark as attached so if a preference is later added to this group, we + // can tell it we are already attached + mAttachedToActivity = true; + + // Dispatch to all contained preferences + final int preferenceCount = getPreferenceCount(); + for (int i = 0; i < preferenceCount; i++) { + getPreference(i).onAttachedToActivity(); + } + } + + @Override + protected void onPrepareForRemoval() { + super.onPrepareForRemoval(); + + // We won't be attached to the activity anymore + mAttachedToActivity = false; + } + + @Override + public void setEnabled(boolean enabled) { + super.setEnabled(enabled); + + // Dispatch to all contained preferences + final int preferenceCount = getPreferenceCount(); + for (int i = 0; i < preferenceCount; i++) { + getPreference(i).setEnabled(enabled); + } + } + + void sortPreferences() { + synchronized (this) { + Collections.sort(mPreferenceList); + } + } + + @Override + protected void dispatchSaveInstanceState(Bundle container) { + super.dispatchSaveInstanceState(container); + + // Dispatch to all contained preferences + final int preferenceCount = getPreferenceCount(); + for (int i = 0; i < preferenceCount; i++) { + getPreference(i).dispatchSaveInstanceState(container); + } + } + + @Override + protected void dispatchRestoreInstanceState(Bundle container) { + super.dispatchRestoreInstanceState(container); + + // Dispatch to all contained preferences + final int preferenceCount = getPreferenceCount(); + for (int i = 0; i < preferenceCount; i++) { + getPreference(i).dispatchRestoreInstanceState(container); + } + } + +} diff --git a/core/java/android/preference/PreferenceGroupAdapter.java b/core/java/android/preference/PreferenceGroupAdapter.java new file mode 100644 index 0000000..e2a3157 --- /dev/null +++ b/core/java/android/preference/PreferenceGroupAdapter.java @@ -0,0 +1,246 @@ +/* + * 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 java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import android.os.Handler; +import android.preference.Preference.OnPreferenceChangeInternalListener; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Adapter; +import android.widget.BaseAdapter; +import android.widget.ListView; + +/** + * An adapter that returns the {@link Preference} contained in this group. + * In most cases, this adapter should be the base class for any custom + * adapters from {@link Preference#getAdapter()}. + * <p> + * This adapter obeys the + * {@link Preference}'s adapter rule (the + * {@link Adapter#getView(int, View, ViewGroup)} should be used instead of + * {@link Preference#getView(ViewGroup)} if a {@link Preference} has an + * adapter via {@link Preference#getAdapter()}). + * <p> + * This adapter also propagates data change/invalidated notifications upward. + * <p> + * This adapter does not include this {@link PreferenceGroup} in the returned + * adapter, use {@link PreferenceCategoryAdapter} instead. + * + * @see PreferenceCategoryAdapter + */ +class PreferenceGroupAdapter extends BaseAdapter implements OnPreferenceChangeInternalListener { + + private static final String TAG = "PreferenceGroupAdapter"; + + /** + * The group that we are providing data from. + */ + private PreferenceGroup mPreferenceGroup; + + /** + * Maps a position into this adapter -> {@link Preference}. These + * {@link Preference}s don't have to be direct children of this + * {@link PreferenceGroup}, they can be grand children or younger) + */ + private List<Preference> mPreferenceList; + + /** + * List of unique Preference and its subclasses' names. This is used to find + * out how many types of views this adapter can return. Once the count is + * returned, this cannot be modified (since the ListView only checks the + * count once--when the adapter is being set). We will not recycle views for + * Preference subclasses seen after the count has been returned. + */ + private List<String> mPreferenceClassNames; + + /** + * Blocks the mPreferenceClassNames from being changed anymore. + */ + private boolean mHasReturnedViewTypeCount = false; + + private volatile boolean mIsSyncing = false; + + private Handler mHandler = new Handler(); + + private Runnable mSyncRunnable = new Runnable() { + public void run() { + syncMyPreferences(); + } + }; + + public PreferenceGroupAdapter(PreferenceGroup preferenceGroup) { + mPreferenceGroup = preferenceGroup; + mPreferenceList = new ArrayList<Preference>(); + mPreferenceClassNames = new ArrayList<String>(); + + syncMyPreferences(); + } + + private void syncMyPreferences() { + synchronized(this) { + if (mIsSyncing) { + return; + } + + mIsSyncing = true; + } + + List<Preference> newPreferenceList = new ArrayList<Preference>(mPreferenceList.size()); + flattenPreferenceGroup(newPreferenceList, mPreferenceGroup); + mPreferenceList = newPreferenceList; + + notifyDataSetChanged(); + + synchronized(this) { + mIsSyncing = false; + notifyAll(); + } + } + + private void flattenPreferenceGroup(List<Preference> preferences, PreferenceGroup group) { + // TODO: shouldn't always? + group.sortPreferences(); + + final int groupSize = group.getPreferenceCount(); + for (int i = 0; i < groupSize; i++) { + final Preference preference = group.getPreference(i); + + preferences.add(preference); + + if (!mHasReturnedViewTypeCount) { + addPreferenceClassName(preference); + } + + if (preference instanceof PreferenceGroup) { + final PreferenceGroup preferenceAsGroup = (PreferenceGroup) preference; + if (preferenceAsGroup.isOnSameScreenAsChildren()) { + flattenPreferenceGroup(preferences, preferenceAsGroup); + preference.setOnPreferenceChangeInternalListener(this); + } + } else { + preference.setOnPreferenceChangeInternalListener(this); + } + } + } + + private void addPreferenceClassName(Preference preference) { + final String name = preference.getClass().getName(); + int insertPos = Collections.binarySearch(mPreferenceClassNames, name); + + // Only insert if it doesn't exist (when it is negative). + if (insertPos < 0) { + // Convert to insert index + insertPos = insertPos * -1 - 1; + mPreferenceClassNames.add(insertPos, name); + } + } + + public int getCount() { + return mPreferenceList.size(); + } + + public Preference getItem(int position) { + if (position < 0 || position >= getCount()) return null; + return mPreferenceList.get(position); + } + + public long getItemId(int position) { + if (position < 0 || position >= getCount()) return ListView.INVALID_ROW_ID; + return this.getItem(position).getId(); + } + + public View getView(int position, View convertView, ViewGroup parent) { + final Preference preference = this.getItem(position); + + if (preference.hasSpecifiedLayout()) { + // If the preference had specified a layout (as opposed to the + // default), don't use convert views. + convertView = null; + } else { + // TODO: better way of doing this + final String name = preference.getClass().getName(); + if (Collections.binarySearch(mPreferenceClassNames, name) < 0) { + convertView = null; + } + } + + return preference.getView(convertView, parent); + } + + @Override + public boolean isEnabled(int position) { + if (position < 0 || position >= getCount()) return true; + return this.getItem(position).isSelectable(); + } + + @Override + public boolean areAllItemsEnabled() { + // There should always be a preference group, and these groups are always + // disabled + return false; + } + + public void onPreferenceChange(Preference preference) { + notifyDataSetChanged(); + } + + public void onPreferenceHierarchyChange(Preference preference) { + mHandler.removeCallbacks(mSyncRunnable); + mHandler.post(mSyncRunnable); + } + + @Override + public boolean hasStableIds() { + return true; + } + + @Override + public int getItemViewType(int position) { + if (!mHasReturnedViewTypeCount) { + mHasReturnedViewTypeCount = true; + } + + final Preference preference = this.getItem(position); + if (preference.hasSpecifiedLayout()) { + return IGNORE_ITEM_VIEW_TYPE; + } + + final String name = preference.getClass().getName(); + int viewType = Collections.binarySearch(mPreferenceClassNames, name); + if (viewType < 0) { + // This is a class that was seen after we returned the count, so + // don't recycle it. + return IGNORE_ITEM_VIEW_TYPE; + } else { + return viewType; + } + } + + @Override + public int getViewTypeCount() { + if (!mHasReturnedViewTypeCount) { + mHasReturnedViewTypeCount = true; + } + + return mPreferenceClassNames.size(); + } + +} diff --git a/core/java/android/preference/PreferenceInflater.java b/core/java/android/preference/PreferenceInflater.java new file mode 100644 index 0000000..779e746 --- /dev/null +++ b/core/java/android/preference/PreferenceInflater.java @@ -0,0 +1,103 @@ +/* + * 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 java.io.IOException; +import java.util.Map; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import android.app.AliasActivity; +import android.content.Context; +import android.content.Intent; +import android.util.AttributeSet; +import android.util.Log; + +/** + * The {@link PreferenceInflater} is used to inflate preference hierarchies from + * XML files. + * <p> + * Do not construct this directly, instead use + * {@link Context#getSystemService(String)} with + * {@link Context#PREFERENCE_INFLATER_SERVICE}. + */ +class PreferenceInflater extends GenericInflater<Preference, PreferenceGroup> { + private static final String TAG = "PreferenceInflater"; + private static final String INTENT_TAG_NAME = "intent"; + + private PreferenceManager mPreferenceManager; + + public PreferenceInflater(Context context, PreferenceManager preferenceManager) { + super(context); + init(preferenceManager); + } + + PreferenceInflater(GenericInflater<Preference, PreferenceGroup> original, PreferenceManager preferenceManager, Context newContext) { + super(original, newContext); + init(preferenceManager); + } + + @Override + public GenericInflater<Preference, PreferenceGroup> cloneInContext(Context newContext) { + return new PreferenceInflater(this, mPreferenceManager, newContext); + } + + private void init(PreferenceManager preferenceManager) { + mPreferenceManager = preferenceManager; + setDefaultPackage("android.preference."); + } + + @Override + protected boolean onCreateCustomFromTag(XmlPullParser parser, Preference parentPreference, + AttributeSet attrs) throws XmlPullParserException { + final String tag = parser.getName(); + + if (tag.equals(INTENT_TAG_NAME)) { + Intent intent = null; + + try { + intent = Intent.parseIntent(getContext().getResources(), parser, attrs); + } catch (IOException e) { + Log.w(TAG, "Could not parse Intent."); + Log.w(TAG, e); + } + + if (intent != null) { + parentPreference.setIntent(intent); + } + + return true; + } + + return false; + } + + @Override + protected PreferenceGroup onMergeRoots(PreferenceGroup givenRoot, boolean attachToGivenRoot, + PreferenceGroup xmlRoot) { + // If we were given a Preferences, use it as the root (ignoring the root + // Preferences from the XML file). + if (givenRoot == null) { + xmlRoot.onAttachedToHierarchy(mPreferenceManager); + return xmlRoot; + } else { + return givenRoot; + } + } + +} diff --git a/core/java/android/preference/PreferenceManager.java b/core/java/android/preference/PreferenceManager.java new file mode 100644 index 0000000..9963544 --- /dev/null +++ b/core/java/android/preference/PreferenceManager.java @@ -0,0 +1,790 @@ +/* + * 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 java.util.ArrayList; +import java.util.HashSet; +import java.util.List; + +import android.app.Activity; +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.res.XmlResourceParser; +import android.os.Bundle; +import android.util.Log; + +/** + * The {@link PreferenceManager} is used to help create preference hierarchies + * from activities or XML. + * <p> + * In most cases, clients should use + * {@link PreferenceActivity#addPreferencesFromIntent} or + * {@link PreferenceActivity#addPreferencesFromResource(int)}. + * + * @see PreferenceActivity + */ +public class PreferenceManager { + + private static final String TAG = "PreferenceManager"; + + /** + * The Activity meta-data key for its XML preference hierarchy. + */ + public static final String METADATA_KEY_PREFERENCES = "android.preference"; + + public static final String KEY_HAS_SET_DEFAULT_VALUES = "_has_set_default_values"; + + /** + * @see #getActivity() + */ + private Activity mActivity; + + /** + * The context to use. This should always be set. + * + * @see #mActivity + */ + private Context mContext; + + /** + * The counter for unique IDs. + */ + private long mNextId = 0; + + /** + * The counter for unique request codes. + */ + private int mNextRequestCode; + + /** + * Cached shared preferences. + */ + private SharedPreferences mSharedPreferences; + + /** + * If in no-commit mode, the shared editor to give out (which will be + * committed when exiting no-commit mode). + */ + private SharedPreferences.Editor mEditor; + + /** + * Blocks commits from happening on the shared editor. This is used when + * inflating the hierarchy. Do not set this directly, use {@link #setNoCommit(boolean)} + */ + private boolean mNoCommit; + + /** + * The SharedPreferences name that will be used for all {@link Preference}s + * managed by this instance. + */ + private String mSharedPreferencesName; + + /** + * The SharedPreferences mode that will be used for all {@link Preference}s + * managed by this instance. + */ + private int mSharedPreferencesMode; + + /** + * The {@link PreferenceScreen} at the root of the preference hierarchy. + */ + private PreferenceScreen mPreferenceScreen; + + /** + * List of activity result listeners. + */ + private List<OnActivityResultListener> mActivityResultListeners; + + /** + * List of activity stop listeners. + */ + private List<OnActivityStopListener> mActivityStopListeners; + + /** + * List of activity destroy listeners. + */ + private List<OnActivityDestroyListener> mActivityDestroyListeners; + + /** + * List of dialogs that should be dismissed when we receive onNewIntent in + * our PreferenceActivity. + */ + private List<DialogInterface> mPreferencesScreens; + + private OnPreferenceTreeClickListener mOnPreferenceTreeClickListener; + + PreferenceManager(Activity activity, int firstRequestCode) { + mActivity = activity; + mNextRequestCode = firstRequestCode; + + init(activity); + } + + /** + * This constructor should ONLY be used when getting default values from + * an XML preference hierarchy. + * <p> + * The {@link PreferenceManager#PreferenceManager(Activity)} + * should be used ANY time a preference will be displayed, since some preference + * types need an Activity for managed queries. + */ + private PreferenceManager(Context context) { + init(context); + } + + private void init(Context context) { + mContext = context; + + setSharedPreferencesName(getDefaultSharedPreferencesName(context)); + } + + /** + * Returns a list of {@link Activity} (indirectly) that match a given + * {@link Intent}. + * + * @param queryIntent The Intent to match. + * @return The list of {@link ResolveInfo} that point to the matched + * activities. + */ + private List<ResolveInfo> queryIntentActivities(Intent queryIntent) { + return mContext.getPackageManager().queryIntentActivities(queryIntent, + PackageManager.GET_META_DATA); + } + + /** + * Inflates a preference hierarchy from the preference hierarchies of + * {@link Activity Activities} that match the given {@link Intent}. An + * {@link Activity} defines its preference hierarchy with meta-data using + * the {@link #METADATA_KEY_PREFERENCES} key. + * <p> + * If a preference hierarchy is given, the new preference hierarchies will + * be merged in. + * + * @param queryIntent The intent to match activities. + * @param rootPreferences Optional existing hierarchy to merge the new + * hierarchies into. + * @return The root hierarchy (if one was not provided, the new hierarchy's + * root). + */ + PreferenceScreen inflateFromIntent(Intent queryIntent, PreferenceScreen rootPreferences) { + final List<ResolveInfo> activities = queryIntentActivities(queryIntent); + final HashSet<String> inflatedRes = new HashSet<String>(); + + for (int i = activities.size() - 1; i >= 0; i--) { + final ActivityInfo activityInfo = activities.get(i).activityInfo; + final Bundle metaData = activityInfo.metaData; + + if ((metaData == null) || !metaData.containsKey(METADATA_KEY_PREFERENCES)) { + continue; + } + + // Need to concat the package with res ID since the same res ID + // can be re-used across contexts + final String uniqueResId = activityInfo.packageName + ":" + + activityInfo.metaData.getInt(METADATA_KEY_PREFERENCES); + + if (!inflatedRes.contains(uniqueResId)) { + inflatedRes.add(uniqueResId); + + final Context context; + try { + context = mContext.createPackageContext(activityInfo.packageName, 0); + } catch (NameNotFoundException e) { + Log.w(TAG, "Could not create context for " + activityInfo.packageName + ": " + + Log.getStackTraceString(e)); + continue; + } + + final PreferenceInflater inflater = new PreferenceInflater(context, this); + final XmlResourceParser parser = activityInfo.loadXmlMetaData(context + .getPackageManager(), METADATA_KEY_PREFERENCES); + rootPreferences = (PreferenceScreen) inflater + .inflate(parser, rootPreferences, true); + parser.close(); + } + } + + rootPreferences.onAttachedToHierarchy(this); + + return rootPreferences; + } + + /** + * Inflates a preference hierarchy from XML. If a preference hierarchy is + * given, the new preference hierarchies will be merged in. + * + * @param context The context of the resource. + * @param resId The resource ID of the XML to inflate. + * @param rootPreferences Optional existing hierarchy to merge the new + * hierarchies into. + * @return The root hierarchy (if one was not provided, the new hierarchy's + * root). + */ + PreferenceScreen inflateFromResource(Context context, int resId, + PreferenceScreen rootPreferences) { + // Block commits + setNoCommit(true); + + final PreferenceInflater inflater = new PreferenceInflater(context, this); + rootPreferences = (PreferenceScreen) inflater.inflate(resId, rootPreferences, true); + rootPreferences.onAttachedToHierarchy(this); + + // Unblock commits + setNoCommit(false); + + return rootPreferences; + } + + public PreferenceScreen createPreferenceScreen(Context context) { + final PreferenceScreen preferenceScreen = new PreferenceScreen(context, null); + preferenceScreen.onAttachedToHierarchy(this); + return preferenceScreen; + } + + /** + * Called by a preference to get a unique ID in its hierarchy. + * + * @return A unique ID. + */ + long getNextId() { + synchronized (this) { + return mNextId++; + } + } + + /** + * Returns the current name of the SharedPreferences file that preferences managed by + * this will use. + * + * @return The name that can be passed to {@link Context#getSharedPreferences(String, int)}. + * @see Context#getSharedPreferences(String, int) + */ + public String getSharedPreferencesName() { + return mSharedPreferencesName; + } + + /** + * Sets the name of the SharedPreferences file that preferences managed by this + * will use. + * + * @param sharedPreferencesName The name of the SharedPreferences file. + * @see Context#getSharedPreferences(String, int) + */ + public void setSharedPreferencesName(String sharedPreferencesName) { + mSharedPreferencesName = sharedPreferencesName; + mSharedPreferences = null; + } + + /** + * Returns the current mode of the SharedPreferences file that preferences managed by + * this will use. + * + * @return The mode that can be passed to {@link Context#getSharedPreferences(String, int)}. + * @see Context#getSharedPreferences(String, int) + */ + public int getSharedPreferencesMode() { + return mSharedPreferencesMode; + } + + /** + * Sets the mode of the SharedPreferences file that preferences managed by this + * will use. + * + * @param sharedPreferencesMode The mode of the SharedPreferences file. + * @see Context#getSharedPreferences(String, int) + */ + public void setSharedPreferencesMode(int sharedPreferencesMode) { + mSharedPreferencesMode = sharedPreferencesMode; + mSharedPreferences = null; + } + + /** + * Gets a SharedPreferences instance that preferences managed by this will + * use. + * + * @return A SharedPreferences instance pointing to the file that contains + * the values of preferences that are managed by this. + */ + public SharedPreferences getSharedPreferences() { + if (mSharedPreferences == null) { + mSharedPreferences = mContext.getSharedPreferences(mSharedPreferencesName, + mSharedPreferencesMode); + } + + return mSharedPreferences; + } + + /** + * Gets a SharedPreferences instance that points to the default file that is + * used by the preference framework in the given context. + * + * @param context The context of the preferences whose values are wanted. + * @return A SharedPreferences instance that can be used to retrieve and + * listen to values of the preferences. + */ + public static SharedPreferences getDefaultSharedPreferences(Context context) { + return context.getSharedPreferences(getDefaultSharedPreferencesName(context), + getDefaultSharedPreferencesMode()); + } + + private static String getDefaultSharedPreferencesName(Context context) { + return context.getPackageName() + "_preferences"; + } + + private static int getDefaultSharedPreferencesMode() { + return Context.MODE_PRIVATE; + } + + /** + * Returns the root of the preference hierarchy managed by this class. + * + * @return The {@link PreferenceScreen} object that is at the root of the hierarchy. + */ + PreferenceScreen getPreferenceScreen() { + return mPreferenceScreen; + } + + /** + * Sets the root of the preference hierarchy. + * + * @param preferenceScreen The root {@link PreferenceScreen} of the preference hierarchy. + * @return Whether the {@link PreferenceScreen} given is different than the previous. + */ + boolean setPreferences(PreferenceScreen preferenceScreen) { + if (preferenceScreen != mPreferenceScreen) { + mPreferenceScreen = preferenceScreen; + return true; + } + + 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) + */ + public Preference findPreference(CharSequence key) { + if (mPreferenceScreen == null) { + return null; + } + + return mPreferenceScreen.findPreference(key); + } + + /** + * Sets the default values from a preference hierarchy in XML. This should + * be called by the application's main activity. + * <p> + * If {@code readAgain} is false, this will only set the default values if this + * method has never been called in the past (or the + * {@link #KEY_HAS_SET_DEFAULT_VALUES} in the default value shared + * preferences file is false). To attempt to set the default values again + * bypassing this check, set {@code readAgain} to true. + * + * @param context The context of the shared preferences. + * @param resId The resource ID of the preference hierarchy XML file. + * @param readAgain Whether to re-read the default values. + * <p> + * Note: this will NOT reset preferences back to their default + * values. For that functionality, use + * {@link PreferenceManager#getDefaultSharedPreferences(Context)} + * and clear it followed by a call to this method with this + * parameter set to true. + */ + public static void setDefaultValues(Context context, int resId, boolean readAgain) { + + // Use the default shared preferences name and mode + setDefaultValues(context, getDefaultSharedPreferencesName(context), + getDefaultSharedPreferencesMode(), resId, readAgain); + } + + /** + * Similar to {@link #setDefaultValues(Context, int, boolean)} but allows + * the client to provide the filename and mode of the shared preferences + * file. + * + * @see #setDefaultValues(Context, int, boolean) + * @see #setSharedPreferencesName(String) + * @see #setSharedPreferencesMode(int) + */ + public static void setDefaultValues(Context context, String sharedPreferencesName, + int sharedPreferencesMode, int resId, boolean readAgain) { + final SharedPreferences defaultValueSp = context.getSharedPreferences( + KEY_HAS_SET_DEFAULT_VALUES, Context.MODE_PRIVATE); + + if (readAgain || !defaultValueSp.getBoolean(KEY_HAS_SET_DEFAULT_VALUES, false)) { + final PreferenceManager pm = new PreferenceManager(context); + pm.setSharedPreferencesName(sharedPreferencesName); + pm.setSharedPreferencesMode(sharedPreferencesMode); + pm.inflateFromResource(context, resId, null); + + defaultValueSp.edit().putBoolean(KEY_HAS_SET_DEFAULT_VALUES, true).commit(); + } + } + + /** + * Returns an editor to use when modifying the shared preferences. + * <p> + * Do NOT commit unless {@link #shouldCommit()} returns true. + * + * @return An editor to use to write to shared preferences. + * @see #shouldCommit() + */ + SharedPreferences.Editor getEditor() { + + if (mNoCommit) { + if (mEditor == null) { + mEditor = getSharedPreferences().edit(); + } + + return mEditor; + } else { + return getSharedPreferences().edit(); + } + } + + /** + * Whether it is the client's responsibility to commit on the + * {@link #getEditor()}. This will return false in cases where the writes + * should be batched, for example when inflating preferences from XML. + * + * @return Whether the client should commit. + */ + boolean shouldCommit() { + return !mNoCommit; + } + + private void setNoCommit(boolean noCommit) { + if (!noCommit && mEditor != null) { + mEditor.commit(); + } + + mNoCommit = noCommit; + } + + /** + * Returns the activity that shows the preferences. This is useful for doing + * managed queries, but in most cases the use of {@link #getContext()} is + * preferred. + * <p> + * This will return null if this class was instantiated with a Context + * instead of Activity. For example, when setting the default values. + * + * @return The activity that shows the preferences. + * @see #mContext + */ + Activity getActivity() { + return mActivity; + } + + /** + * Returns the context. This is preferred over {@link #getActivity()} when + * possible. + * + * @return The context. + */ + Context getContext() { + return mContext; + } + + /** + * Registers a listener. + * + * @see OnActivityResultListener + */ + void registerOnActivityResultListener(OnActivityResultListener listener) { + synchronized (this) { + if (mActivityResultListeners == null) { + mActivityResultListeners = new ArrayList<OnActivityResultListener>(); + } + + if (!mActivityResultListeners.contains(listener)) { + mActivityResultListeners.add(listener); + } + } + } + + /** + * Unregisters a listener. + * + * @see OnActivityResultListener + */ + void unregisterOnActivityResultListener(OnActivityResultListener listener) { + synchronized (this) { + if (mActivityResultListeners != null) { + mActivityResultListeners.remove(listener); + } + } + } + + /** + * Called by the {@link PreferenceManager} to dispatch a subactivity result. + */ + void dispatchActivityResult(int requestCode, int resultCode, Intent data) { + List<OnActivityResultListener> list; + + synchronized (this) { + if (mActivityResultListeners == null) return; + list = new ArrayList<OnActivityResultListener>(mActivityResultListeners); + } + + final int N = list.size(); + for (int i = 0; i < N; i++) { + if (list.get(i).onActivityResult(requestCode, resultCode, data)) { + break; + } + } + } + + /** + * Registers a listener. + * + * @see OnActivityStopListener + */ + void registerOnActivityStopListener(OnActivityStopListener listener) { + synchronized (this) { + if (mActivityStopListeners == null) { + mActivityStopListeners = new ArrayList<OnActivityStopListener>(); + } + + if (!mActivityStopListeners.contains(listener)) { + mActivityStopListeners.add(listener); + } + } + } + + /** + * Unregisters a listener. + * + * @see OnActivityStopListener + */ + void unregisterOnActivityStopListener(OnActivityStopListener listener) { + synchronized (this) { + if (mActivityStopListeners != null) { + mActivityStopListeners.remove(listener); + } + } + } + + /** + * Called by the {@link PreferenceManager} to dispatch the activity stop + * event. + */ + void dispatchActivityStop() { + List<OnActivityStopListener> list; + + synchronized (this) { + if (mActivityStopListeners == null) return; + list = new ArrayList<OnActivityStopListener>(mActivityStopListeners); + } + + final int N = list.size(); + for (int i = 0; i < N; i++) { + list.get(i).onActivityStop(); + } + } + + /** + * Registers a listener. + * + * @see OnActivityDestroyListener + */ + void registerOnActivityDestroyListener(OnActivityDestroyListener listener) { + synchronized (this) { + if (mActivityDestroyListeners == null) { + mActivityDestroyListeners = new ArrayList<OnActivityDestroyListener>(); + } + + if (!mActivityDestroyListeners.contains(listener)) { + mActivityDestroyListeners.add(listener); + } + } + } + + /** + * Unregisters a listener. + * + * @see OnActivityDestroyListener + */ + void unregisterOnActivityDestroyListener(OnActivityDestroyListener listener) { + synchronized (this) { + if (mActivityDestroyListeners != null) { + mActivityDestroyListeners.remove(listener); + } + } + } + + /** + * Called by the {@link PreferenceManager} to dispatch the activity destroy + * event. + */ + void dispatchActivityDestroy() { + List<OnActivityDestroyListener> list; + + synchronized (this) { + if (mActivityDestroyListeners == null) return; + list = new ArrayList<OnActivityDestroyListener>(mActivityDestroyListeners); + } + + final int N = list.size(); + for (int i = 0; i < N; i++) { + list.get(i).onActivityDestroy(); + } + } + + /** + * Returns a request code that is unique for the activity. Each subsequent + * call to this method should return another unique request code. + * + * @return A unique request code that will never be used by anyone other + * than the caller of this method. + */ + int getNextRequestCode() { + synchronized (this) { + return mNextRequestCode++; + } + } + + void addPreferencesScreen(DialogInterface screen) { + synchronized (this) { + + if (mPreferencesScreens == null) { + mPreferencesScreens = new ArrayList<DialogInterface>(); + } + + mPreferencesScreens.add(screen); + } + } + + void removePreferencesScreen(DialogInterface screen) { + synchronized (this) { + + if (mPreferencesScreens == null) { + return; + } + + mPreferencesScreens.remove(screen); + } + } + + /** + * Called by {@link PreferenceActivity} to dispatch the new Intent event. + * + * @param intent The new Intent. + */ + void dispatchNewIntent(Intent intent) { + + // Remove any of the previously shown preferences screens + ArrayList<DialogInterface> screensToDismiss; + + synchronized (this) { + + if (mPreferencesScreens == null) { + return; + } + + screensToDismiss = new ArrayList<DialogInterface>(mPreferencesScreens); + mPreferencesScreens.clear(); + } + + for (int i = screensToDismiss.size() - 1; i >= 0; i--) { + screensToDismiss.get(i).dismiss(); + } + } + + /** + * Sets the callback to be invoked when a {@link Preference} in the + * hierarchy rooted at this {@link PreferenceManager} is clicked. + * + * @param listener The callback to be invoked. + */ + void setOnPreferenceTreeClickListener(OnPreferenceTreeClickListener listener) { + mOnPreferenceTreeClickListener = listener; + } + + OnPreferenceTreeClickListener getOnPreferenceTreeClickListener() { + return mOnPreferenceTreeClickListener; + } + + /** + * Interface definition for a callback to be invoked when a + * {@link Preference} in the hierarchy rooted at this {@link PreferenceScreen} is + * clicked. + */ + interface OnPreferenceTreeClickListener { + /** + * Called when a preference in the tree rooted at this + * {@link PreferenceScreen} has been clicked. + * + * @param preferenceScreen The {@link PreferenceScreen} that the + * preference is located in. + * @param preference The preference that was clicked. + * @return Whether the click was handled. + */ + boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference); + } + + /** + * Interface definition for a class that will be called when the container's activity + * receives an activity result. + */ + public interface OnActivityResultListener { + + /** + * See Activity's onActivityResult. + * + * @return Whether the request code was handled (in which case + * subsequent listeners will not be called. + */ + boolean onActivityResult(int requestCode, int resultCode, Intent data); + } + + /** + * Interface definition for a class that will be called when the container's activity + * is stopped. + */ + public interface OnActivityStopListener { + + /** + * See Activity's onStop. + */ + void onActivityStop(); + } + + /** + * Interface definition for a class that will be called when the container's activity + * is destroyed. + */ + public interface OnActivityDestroyListener { + + /** + * See Activity's onDestroy. + */ + void onActivityDestroy(); + } + +} diff --git a/core/java/android/preference/PreferenceScreen.java b/core/java/android/preference/PreferenceScreen.java new file mode 100644 index 0000000..e4ecb88 --- /dev/null +++ b/core/java/android/preference/PreferenceScreen.java @@ -0,0 +1,252 @@ +/* + * 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 android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.AttributeSet; +import android.view.View; +import android.widget.Adapter; +import android.widget.AdapterView; +import android.widget.ListAdapter; +import android.widget.ListView; + +/** + * The {@link PreferenceScreen} class represents a top-level {@link Preference} that + * is the root of a {@link Preference} hierarchy. A {@link PreferenceActivity} + * points to an instance of this class to show the preferences. To instantiate + * this class, use {@link PreferenceManager#createPreferenceScreen(Context)}. + * <p> + * This class can appear in two places: + * <li> When a {@link PreferenceActivity} points to this, it is used as the root + * and is not shown (only the contained preferences are shown). + * <li> When it appears inside another preference hierarchy, it is shown and + * serves as the gateway to another screen of preferences (either by showing + * another screen of preferences as a {@link Dialog} or via a + * {@link Context#startActivity(android.content.Intent)} from the + * {@link Preference#getIntent()}). The children of this {@link PreferenceScreen} + * are NOT shown in the screen that this {@link PreferenceScreen} is shown in. + * Instead, a separate screen will be shown when this preference is clicked. + * <p> + * <code> + <PreferenceScreen + xmlns:android="http://schemas.android.com/apk/res/android" + android:key="first_preferencescreen"> + <CheckBoxPreference + android:key="wifi enabled" + android:title="WiFi" /> + <PreferenceScreen + android:key="second_preferencescreen" + android:title="WiFi settings"> + <CheckBoxPreference + android:key="prefer wifi" + android:title="Prefer WiFi" /> + ... other preferences here ... + </PreferenceScreen> + </PreferenceScreen> + * </code> + * In this example, the "first_preferencescreen" will be used as the root of the + * hierarchy and given to a {@link PreferenceActivity}. The first screen will + * show preferences "WiFi" (which can be used to quickly enable/disable WiFi) + * and "WiFi settings". The "WiFi settings" is the "second_preferencescreen" and when + * clicked will show another screen of preferences such as "Prefer WiFi" (and + * the other preferences that are children of the "second_preferencescreen" tag). + * + * @see PreferenceCategory + */ +public final class PreferenceScreen extends PreferenceGroup implements AdapterView.OnItemClickListener, + DialogInterface.OnDismissListener { + + private ListAdapter mRootAdapter; + + private Dialog mDialog; + + /** + * Do NOT use this constructor, use {@link PreferenceManager#createPreferenceScreen(Context)}. + * @hide- + */ + public PreferenceScreen(Context context, AttributeSet attrs) { + super(context, attrs, com.android.internal.R.attr.preferenceScreenStyle); + } + + /** + * Returns an adapter that can be attached to a {@link PreferenceActivity} + * to show the preferences contained in this {@link PreferenceScreen}. + * <p> + * This {@link PreferenceScreen} will NOT appear in the returned adapter, instead + * it appears in the hierarchy above this {@link PreferenceScreen}. + * <p> + * This adapter's {@link Adapter#getItem(int)} should always return a + * subclass of {@link Preference}. + * + * @return An adapter that provides the {@link Preference} contained in this + * {@link PreferenceScreen}. + * @see PreferenceGroupAdapter + */ + public ListAdapter getRootAdapter() { + if (mRootAdapter == null) { + mRootAdapter = onCreateRootAdapter(); + } + + return mRootAdapter; + } + + /** + * Creates the root adapter. + * + * @return An adapter that contains the preferences contained in this {@link PreferenceScreen}. + * @see #getRootAdapter() + */ + protected ListAdapter onCreateRootAdapter() { + return new PreferenceGroupAdapter(this); + } + + /** + * Binds a {@link ListView} to the preferences contained in this {@link PreferenceScreen} via + * {@link #getRootAdapter()}. It also handles passing list item clicks to the corresponding + * {@link Preference} contained by this {@link PreferenceScreen}. + * + * @param listView The list view to attach to. + */ + public void bind(ListView listView) { + listView.setOnItemClickListener(this); + listView.setAdapter(getRootAdapter()); + + onAttachedToActivity(); + } + + @Override + protected void onClick() { + if (getIntent() != null || getPreferenceCount() == 0) { + return; + } + + showDialog(null); + } + + private void showDialog(Bundle state) { + Context context = getContext(); + ListView listView = new ListView(context); + bind(listView); + + Dialog dialog = mDialog = new Dialog(context, com.android.internal.R.style.Theme_NoTitleBar); + dialog.setContentView(listView); + dialog.setOnDismissListener(this); + if (state != null) { + dialog.onRestoreInstanceState(state); + } + + // Add the screen to the list of preferences screens opened as dialogs + getPreferenceManager().addPreferencesScreen(dialog); + + dialog.show(); + } + + public void onDismiss(DialogInterface dialog) { + mDialog = null; + getPreferenceManager().removePreferencesScreen(dialog); + } + + /** + * Used to get a handle to the dialog. + * This is useful for cases where we want to manipulate the dialog + * as we would with any other activity or view. + */ + public Dialog getDialog() { + return mDialog; + } + + public void onItemClick(AdapterView parent, View view, int position, long id) { + Object item = getRootAdapter().getItem(position); + if (!(item instanceof Preference)) return; + + final Preference preference = (Preference) item; + preference.performClick(this); + } + + @Override + protected boolean isOnSameScreenAsChildren() { + return false; + } + + @Override + protected Parcelable onSaveInstanceState() { + final Parcelable superState = super.onSaveInstanceState(); + final Dialog dialog = mDialog; + if (dialog == null || !dialog.isShowing()) { + return superState; + } + + final SavedState myState = new SavedState(superState); + myState.isDialogShowing = true; + myState.dialogBundle = dialog.onSaveInstanceState(); + return myState; + } + + @Override + protected void onRestoreInstanceState(Parcelable state) { + if (state == null || !state.getClass().equals(SavedState.class)) { + // Didn't save state for us in onSaveInstanceState + super.onRestoreInstanceState(state); + return; + } + + SavedState myState = (SavedState) state; + super.onRestoreInstanceState(myState.getSuperState()); + if (myState.isDialogShowing) { + showDialog(myState.dialogBundle); + } + } + + private static class SavedState extends BaseSavedState { + boolean isDialogShowing; + Bundle dialogBundle; + + public SavedState(Parcel source) { + super(source); + isDialogShowing = source.readInt() == 1; + dialogBundle = source.readBundle(); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeInt(isDialogShowing ? 1 : 0); + dest.writeBundle(dialogBundle); + } + + public SavedState(Parcelable superState) { + super(superState); + } + + public static final Parcelable.Creator<SavedState> CREATOR = + new Parcelable.Creator<SavedState>() { + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } + +} diff --git a/core/java/android/preference/RingtonePreference.java b/core/java/android/preference/RingtonePreference.java new file mode 100644 index 0000000..97674ce --- /dev/null +++ b/core/java/android/preference/RingtonePreference.java @@ -0,0 +1,242 @@ +/* + * 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 android.content.Context; +import android.content.Intent; +import android.content.res.TypedArray; +import android.media.RingtoneManager; +import android.net.Uri; +import android.provider.Settings.System; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.util.Log; + +/** + * The {@link RingtonePreference} allows the user to choose one from all of the + * available ringtones. The chosen ringtone's URI will be persisted as a string. + * <p> + * If the user chooses the "Default" item, the saved string will be one of + * {@link System#DEFAULT_RINGTONE_URI} or + * {@link System#DEFAULT_NOTIFICATION_URI}. If the user chooses the "Silent" + * item, the saved string will be an empty string. + * + * @attr ref android.R.styleable#RingtonePreference_ringtoneType + * @attr ref android.R.styleable#RingtonePreference_showDefault + * @attr ref android.R.styleable#RingtonePreference_showSilent + */ +public class RingtonePreference extends Preference implements + PreferenceManager.OnActivityResultListener { + + private static final String TAG = "RingtonePreference"; + + private int mRingtoneType; + private boolean mShowDefault; + private boolean mShowSilent; + + private int mRequestCode; + + public RingtonePreference(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + TypedArray a = context.obtainStyledAttributes(attrs, + com.android.internal.R.styleable.RingtonePreference, defStyle, 0); + mRingtoneType = a.getInt(com.android.internal.R.styleable.RingtonePreference_ringtoneType, + RingtoneManager.TYPE_RINGTONE); + mShowDefault = a.getBoolean(com.android.internal.R.styleable.RingtonePreference_showDefault, + true); + mShowSilent = a.getBoolean(com.android.internal.R.styleable.RingtonePreference_showSilent, + true); + a.recycle(); + } + + public RingtonePreference(Context context, AttributeSet attrs) { + this(context, attrs, com.android.internal.R.attr.ringtonePreferenceStyle); + } + + public RingtonePreference(Context context) { + this(context, null); + } + + /** + * Returns the sound type(s) that are shown in the picker. + * + * @return The sound type(s) that are shown in the picker. + * @see #setRingtoneType(int) + */ + public int getRingtoneType() { + return mRingtoneType; + } + + /** + * Sets the sound type(s) that are shown in the picker. + * + * @param type The sound type(s) that are shown in the picker. + * @see RingtoneManager#EXTRA_RINGTONE_TYPE + */ + public void setRingtoneType(int type) { + mRingtoneType = type; + } + + /** + * Returns whether to a show an item for the default sound/ringtone. + * + * @return Whether to show an item for the default sound/ringtone. + */ + public boolean getShowDefault() { + return mShowDefault; + } + + /** + * Sets whether to show an item for the default sound/ringtone. The default + * to use will be deduced from the sound type(s) being shown. + * + * @param showDefault Whether to show the default or not. + * @see RingtoneManager#EXTRA_RINGTONE_SHOW_DEFAULT + */ + public void setShowDefault(boolean showDefault) { + mShowDefault = showDefault; + } + + /** + * Returns whether to a show an item for 'Silent'. + * + * @return Whether to show an item for 'Silent'. + */ + public boolean getShowSilent() { + return mShowSilent; + } + + /** + * Sets whether to show an item for 'Silent'. + * + * @param showSilent Whether to show 'Silent'. + * @see RingtoneManager#EXTRA_RINGTONE_SHOW_SILENT + */ + public void setShowSilent(boolean showSilent) { + mShowSilent = showSilent; + } + + @Override + protected void onClick() { + // Launch the ringtone picker + Intent intent = new Intent(RingtoneManager.ACTION_RINGTONE_PICKER); + onPrepareRingtonePickerIntent(intent); + getPreferenceManager().getActivity().startActivityForResult(intent, mRequestCode); + } + + /** + * Prepares the intent to launch the ringtone picker. This can be modified + * to adjust the parameters of the ringtone picker. + * + * @param ringtonePickerIntent The ringtone picker intent that can be + * modified by putting extras. + */ + protected void onPrepareRingtonePickerIntent(Intent ringtonePickerIntent) { + + ringtonePickerIntent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, + onRestoreRingtone()); + + ringtonePickerIntent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, mShowDefault); + if (mShowDefault) { + ringtonePickerIntent.putExtra(RingtoneManager.EXTRA_RINGTONE_DEFAULT_URI, + RingtoneManager.getDefaultUri(getRingtoneType())); + } + + ringtonePickerIntent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, mShowSilent); + ringtonePickerIntent.putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, mRingtoneType); + } + + /** + * Called when a ringtone is chosen. + * <p> + * By default, this saves the ringtone URI to the persistent storage as a + * string. + * + * @param ringtoneUri The chosen ringtone's {@link Uri}. Can be null. + */ + protected void onSaveRingtone(Uri ringtoneUri) { + persistString(ringtoneUri != null ? ringtoneUri.toString() : ""); + } + + /** + * Called when the chooser is about to be shown and the current ringtone + * should be marked. Can return null to not mark any ringtone. + * <p> + * By default, this restores the previous ringtone URI from the persistent + * storage. + * + * @return The ringtone to be marked as the current ringtone. + */ + protected Uri onRestoreRingtone() { + final String uriString = getPersistedString(null); + return !TextUtils.isEmpty(uriString) ? Uri.parse(uriString) : null; + } + + @Override + protected Object onGetDefaultValue(TypedArray a, int index) { + return a.getString(index); + } + + @Override + protected void onSetInitialValue(boolean restorePersistedValue, Object defaultValueObj) { + String defaultValue = (String) defaultValueObj; + + /* + * This method is normally to make sure the internal state and UI + * matches either the persisted value or the default value. Since we + * don't show the current value in the UI (until the dialog is opened) + * and we don't keep local state, if we are restoring the persisted + * value we don't need to do anything. + */ + if (restorePersistedValue) { + return; + } + + // If we are setting to the default value, we should persist it. + if (!TextUtils.isEmpty(defaultValue)) { + onSaveRingtone(Uri.parse(defaultValue)); + } + } + + @Override + protected void onAttachedToHierarchy(PreferenceManager preferenceManager) { + super.onAttachedToHierarchy(preferenceManager); + + preferenceManager.registerOnActivityResultListener(this); + mRequestCode = preferenceManager.getNextRequestCode(); + } + + public boolean onActivityResult(int requestCode, int resultCode, Intent data) { + + if (requestCode == mRequestCode) { + + if (data != null) { + Uri uri = data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI); + + if (callChangeListener(uri != null ? uri.toString() : "")) { + onSaveRingtone(uri); + } + } + + return true; + } + + return false; + } + +} diff --git a/core/java/android/preference/SeekBarPreference.java b/core/java/android/preference/SeekBarPreference.java new file mode 100644 index 0000000..658c2a7 --- /dev/null +++ b/core/java/android/preference/SeekBarPreference.java @@ -0,0 +1,62 @@ +/* + * 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 android.content.Context; +import android.graphics.drawable.Drawable; +import android.preference.DialogPreference; +import android.util.AttributeSet; +import android.view.View; +import android.widget.ImageView; +import android.widget.SeekBar; + +/** + * @hide + */ +public class SeekBarPreference extends DialogPreference { + private static final String TAG = "SeekBarPreference"; + + private Drawable mMyIcon; + + public SeekBarPreference(Context context, AttributeSet attrs) { + super(context, attrs); + + setDialogLayoutResource(com.android.internal.R.layout.seekbar_dialog); + setPositiveButtonText(android.R.string.ok); + setNegativeButtonText(android.R.string.cancel); + + // Steal the XML dialogIcon attribute's value + mMyIcon = getDialogIcon(); + setDialogIcon(null); + } + + @Override + protected void onBindDialogView(View view) { + super.onBindDialogView(view); + + final ImageView iconView = (ImageView) view.findViewById(android.R.id.icon); + if (mMyIcon != null) { + iconView.setImageDrawable(mMyIcon); + } else { + iconView.setVisibility(View.GONE); + } + } + + protected static SeekBar getSeekBar(View dialogView) { + return (SeekBar) dialogView.findViewById(com.android.internal.R.id.seekbar); + } +} diff --git a/core/java/android/preference/VolumePreference.java b/core/java/android/preference/VolumePreference.java new file mode 100644 index 0000000..5a0a089 --- /dev/null +++ b/core/java/android/preference/VolumePreference.java @@ -0,0 +1,161 @@ +/* + * 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 android.content.ContentResolver; +import android.content.Context; +import android.content.res.TypedArray; +import android.database.ContentObserver; +import android.media.Ringtone; +import android.media.RingtoneManager; +import android.media.AudioManager; +import android.os.Handler; +import android.preference.PreferenceManager; +import android.provider.Settings; +import android.provider.Settings.System; +import android.util.AttributeSet; +import android.view.View; +import android.widget.SeekBar; +import android.widget.SeekBar.OnSeekBarChangeListener; + +/** + * @hide + */ +public class VolumePreference extends SeekBarPreference implements OnSeekBarChangeListener, + Runnable, PreferenceManager.OnActivityStopListener { + + private static final String TAG = "VolumePreference"; + + private ContentResolver mContentResolver; + private Handler mHandler = new Handler(); + + private AudioManager mVolume; + private int mStreamType; + private int mOriginalStreamVolume; + private Ringtone mRingtone; + + private int mLastProgress; + private SeekBar mSeekBar; + + private ContentObserver mVolumeObserver = new ContentObserver(mHandler) { + @Override + public void onChange(boolean selfChange) { + super.onChange(selfChange); + + if (mSeekBar != null) { + mSeekBar.setProgress(System.getInt(mContentResolver, + System.VOLUME_SETTINGS[mStreamType], 0)); + } + } + }; + + public VolumePreference(Context context, AttributeSet attrs) { + super(context, attrs); + + TypedArray a = context.obtainStyledAttributes(attrs, + com.android.internal.R.styleable.VolumePreference, 0, 0); + mStreamType = a.getInt(android.R.styleable.VolumePreference_streamType, 0); + a.recycle(); + + mContentResolver = context.getContentResolver(); + + mVolume = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + } + + @Override + protected void onBindDialogView(View view) { + super.onBindDialogView(view); + + final SeekBar seekBar = mSeekBar = (SeekBar) view.findViewById(com.android.internal.R.id.seekbar); + seekBar.setMax(mVolume.getStreamMaxVolume(mStreamType)); + mOriginalStreamVolume = mVolume.getStreamVolume(mStreamType); + seekBar.setProgress(mOriginalStreamVolume); + seekBar.setOnSeekBarChangeListener(this); + + mContentResolver.registerContentObserver(System.getUriFor(System.VOLUME_SETTINGS[mStreamType]), false, mVolumeObserver); + + getPreferenceManager().registerOnActivityStopListener(this); + mRingtone = RingtoneManager.getRingtone(getContext(), Settings.System.DEFAULT_RINGTONE_URI); + } + + @Override + protected void onDialogClosed(boolean positiveResult) { + super.onDialogClosed(positiveResult); + + if (!positiveResult) { + mVolume.setStreamVolume(mStreamType, mOriginalStreamVolume, 0); + } + + cleanup(); + } + + + public void onActivityStop() { + cleanup(); + } + + /** + * Do clean up. This can be called multiple times! + */ + private void cleanup() { + stopSample(); + if (mVolumeObserver != null) { + mContentResolver.unregisterContentObserver(mVolumeObserver); + } + getPreferenceManager().unregisterOnActivityStopListener(this); + mSeekBar = null; + } + + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromTouch) { + if (!fromTouch) { + return; + } + + postSetVolume(progress); + } + + private void postSetVolume(int progress) { + // Do the volume changing separately to give responsive UI + mLastProgress = progress; + mHandler.removeCallbacks(this); + mHandler.post(this); + } + + public void onStartTrackingTouch(SeekBar seekBar) { + } + + public void onStopTrackingTouch(SeekBar seekBar) { + if (mRingtone != null && !mRingtone.isPlaying()) { + sample(); + } + } + + public void run() { + mVolume.setStreamVolume(mStreamType, mLastProgress, 0); + } + + private void sample() { + mRingtone.play(); + } + + private void stopSample() { + if (mRingtone != null) { + mRingtone.stop(); + } + } + +} diff --git a/core/java/android/preference/package.html b/core/java/android/preference/package.html new file mode 100644 index 0000000..d24d5bb --- /dev/null +++ b/core/java/android/preference/package.html @@ -0,0 +1,23 @@ +<HTML> +<BODY> +Provides classes that manage application preferences and implement the preferences UI. +Using these ensures that all the preferences within each application are maintained +in the same manner and the user experience is consistent with that of the system and +other applications. +<p> +The preferences portion of an application +should be ran as a separate {@link android.app.Activity} that extends +the {@link android.preference.PreferenceActivity} class. In the PreferenceActivity, a +{@link android.preference.PreferenceScreen} object should be the root element of the layout. +The PreferenceScreen contains {@link android.preference.Preference} elements such as a +{@link android.preference.CheckBoxPreference}, {@link android.preference.EditTextPreference}, +{@link android.preference.ListPreference}, {@link android.preference.PreferenceCategory}, +or {@link android.preference.RingtonePreference}. </p> +<p> +All settings made for a given {@link android.preference.Preference} will be automatically saved +to the application's instance of {@link android.content.SharedPreferences}. Access to the +SharedPreferences is simple with {@link android.preference.Preference#getSharedPreferences()}.</p> +<p> +Note that saved preferences are accessible only to the application that created them.</p> +</BODY> +</HTML> |