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