1/*
2 * Copyright (C) 2007 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.preference;
18
19
20import android.app.AlertDialog;
21import android.app.Dialog;
22import android.content.Context;
23import android.content.DialogInterface;
24import android.content.SharedPreferences;
25import android.content.res.TypedArray;
26import android.graphics.drawable.Drawable;
27import android.os.Bundle;
28import android.os.Parcel;
29import android.os.Parcelable;
30import android.text.TextUtils;
31import android.util.AttributeSet;
32import android.view.LayoutInflater;
33import android.view.View;
34import android.view.Window;
35import android.view.WindowManager;
36import android.widget.TextView;
37
38/**
39 * A base class for {@link Preference} objects that are
40 * dialog-based. These preferences will, when clicked, open a dialog showing the
41 * actual preference controls.
42 *
43 * @attr ref android.R.styleable#DialogPreference_dialogTitle
44 * @attr ref android.R.styleable#DialogPreference_dialogMessage
45 * @attr ref android.R.styleable#DialogPreference_dialogIcon
46 * @attr ref android.R.styleable#DialogPreference_dialogLayout
47 * @attr ref android.R.styleable#DialogPreference_positiveButtonText
48 * @attr ref android.R.styleable#DialogPreference_negativeButtonText
49 */
50public abstract class DialogPreference extends Preference implements
51        DialogInterface.OnClickListener, DialogInterface.OnDismissListener,
52        PreferenceManager.OnActivityDestroyListener {
53    private AlertDialog.Builder mBuilder;
54
55    private CharSequence mDialogTitle;
56    private CharSequence mDialogMessage;
57    private Drawable mDialogIcon;
58    private CharSequence mPositiveButtonText;
59    private CharSequence mNegativeButtonText;
60    private int mDialogLayoutResId;
61
62    /** The dialog, if it is showing. */
63    private Dialog mDialog;
64
65    /** Which button was clicked. */
66    private int mWhichButtonClicked;
67
68    public DialogPreference(
69            Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
70        super(context, attrs, defStyleAttr, defStyleRes);
71
72        final TypedArray a = context.obtainStyledAttributes(attrs,
73                com.android.internal.R.styleable.DialogPreference, defStyleAttr, defStyleRes);
74        mDialogTitle = a.getString(com.android.internal.R.styleable.DialogPreference_dialogTitle);
75        if (mDialogTitle == null) {
76            // Fallback on the regular title of the preference
77            // (the one that is seen in the list)
78            mDialogTitle = getTitle();
79        }
80        mDialogMessage = a.getString(com.android.internal.R.styleable.DialogPreference_dialogMessage);
81        mDialogIcon = a.getDrawable(com.android.internal.R.styleable.DialogPreference_dialogIcon);
82        mPositiveButtonText = a.getString(com.android.internal.R.styleable.DialogPreference_positiveButtonText);
83        mNegativeButtonText = a.getString(com.android.internal.R.styleable.DialogPreference_negativeButtonText);
84        mDialogLayoutResId = a.getResourceId(com.android.internal.R.styleable.DialogPreference_dialogLayout,
85                mDialogLayoutResId);
86        a.recycle();
87    }
88
89    public DialogPreference(Context context, AttributeSet attrs, int defStyleAttr) {
90        this(context, attrs, defStyleAttr, 0);
91    }
92
93    public DialogPreference(Context context, AttributeSet attrs) {
94        this(context, attrs, com.android.internal.R.attr.dialogPreferenceStyle);
95    }
96
97    public DialogPreference(Context context) {
98        this(context, null);
99    }
100
101    /**
102     * Sets the title of the dialog. This will be shown on subsequent dialogs.
103     *
104     * @param dialogTitle The title.
105     */
106    public void setDialogTitle(CharSequence dialogTitle) {
107        mDialogTitle = dialogTitle;
108    }
109
110    /**
111     * @see #setDialogTitle(CharSequence)
112     * @param dialogTitleResId The dialog title as a resource.
113     */
114    public void setDialogTitle(int dialogTitleResId) {
115        setDialogTitle(getContext().getString(dialogTitleResId));
116    }
117
118    /**
119     * Returns the title to be shown on subsequent dialogs.
120     * @return The title.
121     */
122    public CharSequence getDialogTitle() {
123        return mDialogTitle;
124    }
125
126    /**
127     * Sets the message of the dialog. This will be shown on subsequent dialogs.
128     * <p>
129     * This message forms the content View of the dialog and conflicts with
130     * list-based dialogs, for example. If setting a custom View on a dialog via
131     * {@link #setDialogLayoutResource(int)}, include a text View with ID
132     * {@link android.R.id#message} and it will be populated with this message.
133     *
134     * @param dialogMessage The message.
135     */
136    public void setDialogMessage(CharSequence dialogMessage) {
137        mDialogMessage = dialogMessage;
138    }
139
140    /**
141     * @see #setDialogMessage(CharSequence)
142     * @param dialogMessageResId The dialog message as a resource.
143     */
144    public void setDialogMessage(int dialogMessageResId) {
145        setDialogMessage(getContext().getString(dialogMessageResId));
146    }
147
148    /**
149     * Returns the message to be shown on subsequent dialogs.
150     * @return The message.
151     */
152    public CharSequence getDialogMessage() {
153        return mDialogMessage;
154    }
155
156    /**
157     * Sets the icon of the dialog. This will be shown on subsequent dialogs.
158     *
159     * @param dialogIcon The icon, as a {@link Drawable}.
160     */
161    public void setDialogIcon(Drawable dialogIcon) {
162        mDialogIcon = dialogIcon;
163    }
164
165    /**
166     * Sets the icon (resource ID) of the dialog. This will be shown on
167     * subsequent dialogs.
168     *
169     * @param dialogIconRes The icon, as a resource ID.
170     */
171    public void setDialogIcon(int dialogIconRes) {
172        mDialogIcon = getContext().getDrawable(dialogIconRes);
173    }
174
175    /**
176     * Returns the icon to be shown on subsequent dialogs.
177     * @return The icon, as a {@link Drawable}.
178     */
179    public Drawable getDialogIcon() {
180        return mDialogIcon;
181    }
182
183    /**
184     * Sets the text of the positive button of the dialog. This will be shown on
185     * subsequent dialogs.
186     *
187     * @param positiveButtonText The text of the positive button.
188     */
189    public void setPositiveButtonText(CharSequence positiveButtonText) {
190        mPositiveButtonText = positiveButtonText;
191    }
192
193    /**
194     * @see #setPositiveButtonText(CharSequence)
195     * @param positiveButtonTextResId The positive button text as a resource.
196     */
197    public void setPositiveButtonText(int positiveButtonTextResId) {
198        setPositiveButtonText(getContext().getString(positiveButtonTextResId));
199    }
200
201    /**
202     * Returns the text of the positive button to be shown on subsequent
203     * dialogs.
204     *
205     * @return The text of the positive button.
206     */
207    public CharSequence getPositiveButtonText() {
208        return mPositiveButtonText;
209    }
210
211    /**
212     * Sets the text of the negative button of the dialog. This will be shown on
213     * subsequent dialogs.
214     *
215     * @param negativeButtonText The text of the negative button.
216     */
217    public void setNegativeButtonText(CharSequence negativeButtonText) {
218        mNegativeButtonText = negativeButtonText;
219    }
220
221    /**
222     * @see #setNegativeButtonText(CharSequence)
223     * @param negativeButtonTextResId The negative button text as a resource.
224     */
225    public void setNegativeButtonText(int negativeButtonTextResId) {
226        setNegativeButtonText(getContext().getString(negativeButtonTextResId));
227    }
228
229    /**
230     * Returns the text of the negative button to be shown on subsequent
231     * dialogs.
232     *
233     * @return The text of the negative button.
234     */
235    public CharSequence getNegativeButtonText() {
236        return mNegativeButtonText;
237    }
238
239    /**
240     * Sets the layout resource that is inflated as the {@link View} to be shown
241     * as the content View of subsequent dialogs.
242     *
243     * @param dialogLayoutResId The layout resource ID to be inflated.
244     * @see #setDialogMessage(CharSequence)
245     */
246    public void setDialogLayoutResource(int dialogLayoutResId) {
247        mDialogLayoutResId = dialogLayoutResId;
248    }
249
250    /**
251     * Returns the layout resource that is used as the content View for
252     * subsequent dialogs.
253     *
254     * @return The layout resource.
255     */
256    public int getDialogLayoutResource() {
257        return mDialogLayoutResId;
258    }
259
260    /**
261     * Prepares the dialog builder to be shown when the preference is clicked.
262     * Use this to set custom properties on the dialog.
263     * <p>
264     * Do not {@link AlertDialog.Builder#create()} or
265     * {@link AlertDialog.Builder#show()}.
266     */
267    protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
268    }
269
270    @Override
271    protected void onClick() {
272        if (mDialog != null && mDialog.isShowing()) return;
273
274        showDialog(null);
275    }
276
277    /**
278     * Shows the dialog associated with this Preference. This is normally initiated
279     * automatically on clicking on the preference. Call this method if you need to
280     * show the dialog on some other event.
281     *
282     * @param state Optional instance state to restore on the dialog
283     */
284    protected void showDialog(Bundle state) {
285        Context context = getContext();
286
287        mWhichButtonClicked = DialogInterface.BUTTON_NEGATIVE;
288
289        mBuilder = new AlertDialog.Builder(context)
290            .setTitle(mDialogTitle)
291            .setIcon(mDialogIcon)
292            .setPositiveButton(mPositiveButtonText, this)
293            .setNegativeButton(mNegativeButtonText, this);
294
295        View contentView = onCreateDialogView();
296        if (contentView != null) {
297            onBindDialogView(contentView);
298            mBuilder.setView(contentView);
299        } else {
300            mBuilder.setMessage(mDialogMessage);
301        }
302
303        onPrepareDialogBuilder(mBuilder);
304
305        getPreferenceManager().registerOnActivityDestroyListener(this);
306
307        // Create the dialog
308        final Dialog dialog = mDialog = mBuilder.create();
309        if (state != null) {
310            dialog.onRestoreInstanceState(state);
311        }
312        if (needInputMethod()) {
313            requestInputMethod(dialog);
314        }
315        dialog.setOnDismissListener(this);
316        dialog.show();
317    }
318
319    /**
320     * Returns whether the preference needs to display a soft input method when the dialog
321     * is displayed. Default is false. Subclasses should override this method if they need
322     * the soft input method brought up automatically.
323     * @hide
324     */
325    protected boolean needInputMethod() {
326        return false;
327    }
328
329    /**
330     * Sets the required flags on the dialog window to enable input method window to show up.
331     */
332    private void requestInputMethod(Dialog dialog) {
333        Window window = dialog.getWindow();
334        window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
335    }
336
337    /**
338     * Creates the content view for the dialog (if a custom content view is
339     * required). By default, it inflates the dialog layout resource if it is
340     * set.
341     *
342     * @return The content View for the dialog.
343     * @see #setLayoutResource(int)
344     */
345    protected View onCreateDialogView() {
346        if (mDialogLayoutResId == 0) {
347            return null;
348        }
349
350        LayoutInflater inflater = LayoutInflater.from(mBuilder.getContext());
351        return inflater.inflate(mDialogLayoutResId, null);
352    }
353
354    /**
355     * Binds views in the content View of the dialog to data.
356     * <p>
357     * Make sure to call through to the superclass implementation.
358     *
359     * @param view The content View of the dialog, if it is custom.
360     */
361    protected void onBindDialogView(View view) {
362        View dialogMessageView = view.findViewById(com.android.internal.R.id.message);
363
364        if (dialogMessageView != null) {
365            final CharSequence message = getDialogMessage();
366            int newVisibility = View.GONE;
367
368            if (!TextUtils.isEmpty(message)) {
369                if (dialogMessageView instanceof TextView) {
370                    ((TextView) dialogMessageView).setText(message);
371                }
372
373                newVisibility = View.VISIBLE;
374            }
375
376            if (dialogMessageView.getVisibility() != newVisibility) {
377                dialogMessageView.setVisibility(newVisibility);
378            }
379        }
380    }
381
382    public void onClick(DialogInterface dialog, int which) {
383        mWhichButtonClicked = which;
384    }
385
386    public void onDismiss(DialogInterface dialog) {
387
388        getPreferenceManager().unregisterOnActivityDestroyListener(this);
389
390        mDialog = null;
391        onDialogClosed(mWhichButtonClicked == DialogInterface.BUTTON_POSITIVE);
392    }
393
394    /**
395     * Called when the dialog is dismissed and should be used to save data to
396     * the {@link SharedPreferences}.
397     *
398     * @param positiveResult Whether the positive button was clicked (true), or
399     *            the negative button was clicked or the dialog was canceled (false).
400     */
401    protected void onDialogClosed(boolean positiveResult) {
402    }
403
404    /**
405     * Gets the dialog that is shown by this preference.
406     *
407     * @return The dialog, or null if a dialog is not being shown.
408     */
409    public Dialog getDialog() {
410        return mDialog;
411    }
412
413    /**
414     * {@inheritDoc}
415     */
416    public void onActivityDestroy() {
417
418        if (mDialog == null || !mDialog.isShowing()) {
419            return;
420        }
421
422        mDialog.dismiss();
423    }
424
425    @Override
426    protected Parcelable onSaveInstanceState() {
427        final Parcelable superState = super.onSaveInstanceState();
428        if (mDialog == null || !mDialog.isShowing()) {
429            return superState;
430        }
431
432        final SavedState myState = new SavedState(superState);
433        myState.isDialogShowing = true;
434        myState.dialogBundle = mDialog.onSaveInstanceState();
435        return myState;
436    }
437
438    @Override
439    protected void onRestoreInstanceState(Parcelable state) {
440        if (state == null || !state.getClass().equals(SavedState.class)) {
441            // Didn't save state for us in onSaveInstanceState
442            super.onRestoreInstanceState(state);
443            return;
444        }
445
446        SavedState myState = (SavedState) state;
447        super.onRestoreInstanceState(myState.getSuperState());
448        if (myState.isDialogShowing) {
449            showDialog(myState.dialogBundle);
450        }
451    }
452
453    private static class SavedState extends BaseSavedState {
454        boolean isDialogShowing;
455        Bundle dialogBundle;
456
457        public SavedState(Parcel source) {
458            super(source);
459            isDialogShowing = source.readInt() == 1;
460            dialogBundle = source.readBundle();
461        }
462
463        @Override
464        public void writeToParcel(Parcel dest, int flags) {
465            super.writeToParcel(dest, flags);
466            dest.writeInt(isDialogShowing ? 1 : 0);
467            dest.writeBundle(dialogBundle);
468        }
469
470        public SavedState(Parcelable superState) {
471            super(superState);
472        }
473
474        public static final Parcelable.Creator<SavedState> CREATOR =
475                new Parcelable.Creator<SavedState>() {
476            public SavedState createFromParcel(Parcel in) {
477                return new SavedState(in);
478            }
479
480            public SavedState[] newArray(int size) {
481                return new SavedState[size];
482            }
483        };
484    }
485
486}
487