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