1/*
2 * Copyright (C) 2008 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 com.android.internal.app;
18
19import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
20
21import com.android.internal.R;
22
23import android.annotation.Nullable;
24import android.app.AlertDialog;
25import android.content.Context;
26import android.content.DialogInterface;
27import android.content.res.Configuration;
28import android.content.res.TypedArray;
29import android.database.Cursor;
30import android.graphics.drawable.Drawable;
31import android.os.Handler;
32import android.os.Message;
33import android.text.Layout;
34import android.text.TextUtils;
35import android.text.method.LinkMovementMethod;
36import android.text.method.MovementMethod;
37import android.util.AttributeSet;
38import android.util.TypedValue;
39import android.view.Gravity;
40import android.view.KeyEvent;
41import android.view.LayoutInflater;
42import android.view.View;
43import android.view.ViewGroup;
44import android.view.ViewGroup.LayoutParams;
45import android.view.ViewParent;
46import android.view.ViewStub;
47import android.view.Window;
48import android.view.WindowInsets;
49import android.view.WindowManager;
50import android.widget.AdapterView;
51import android.widget.AdapterView.OnItemClickListener;
52import android.widget.ArrayAdapter;
53import android.widget.Button;
54import android.widget.CheckedTextView;
55import android.widget.CursorAdapter;
56import android.widget.FrameLayout;
57import android.widget.ImageView;
58import android.widget.LinearLayout;
59import android.widget.ListAdapter;
60import android.widget.ListView;
61import android.widget.ScrollView;
62import android.widget.SimpleCursorAdapter;
63import android.widget.TextView;
64
65import java.lang.ref.WeakReference;
66
67public class AlertController {
68    public static final int MICRO = 1;
69
70    private final Context mContext;
71    private final DialogInterface mDialogInterface;
72    protected final Window mWindow;
73
74    private CharSequence mTitle;
75    protected CharSequence mMessage;
76    protected ListView mListView;
77    private View mView;
78
79    private int mViewLayoutResId;
80
81    private int mViewSpacingLeft;
82    private int mViewSpacingTop;
83    private int mViewSpacingRight;
84    private int mViewSpacingBottom;
85    private boolean mViewSpacingSpecified = false;
86
87    private Button mButtonPositive;
88    private CharSequence mButtonPositiveText;
89    private Message mButtonPositiveMessage;
90
91    private Button mButtonNegative;
92    private CharSequence mButtonNegativeText;
93    private Message mButtonNegativeMessage;
94
95    private Button mButtonNeutral;
96    private CharSequence mButtonNeutralText;
97    private Message mButtonNeutralMessage;
98
99    protected ScrollView mScrollView;
100
101    private int mIconId = 0;
102    private Drawable mIcon;
103
104    private ImageView mIconView;
105    private TextView mTitleView;
106    protected TextView mMessageView;
107    private MovementMethod mMessageMovementMethod;
108    @Layout.HyphenationFrequency
109    private Integer mMessageHyphenationFrequency;
110    private View mCustomTitleView;
111
112    private boolean mForceInverseBackground;
113
114    private ListAdapter mAdapter;
115
116    private int mCheckedItem = -1;
117
118    private int mAlertDialogLayout;
119    private int mButtonPanelSideLayout;
120    private int mListLayout;
121    private int mMultiChoiceItemLayout;
122    private int mSingleChoiceItemLayout;
123    private int mListItemLayout;
124
125    private boolean mShowTitle;
126
127    private int mButtonPanelLayoutHint = AlertDialog.LAYOUT_HINT_NONE;
128
129    private Handler mHandler;
130
131    private final View.OnClickListener mButtonHandler = new View.OnClickListener() {
132        @Override
133        public void onClick(View v) {
134            final Message m;
135            if (v == mButtonPositive && mButtonPositiveMessage != null) {
136                m = Message.obtain(mButtonPositiveMessage);
137            } else if (v == mButtonNegative && mButtonNegativeMessage != null) {
138                m = Message.obtain(mButtonNegativeMessage);
139            } else if (v == mButtonNeutral && mButtonNeutralMessage != null) {
140                m = Message.obtain(mButtonNeutralMessage);
141            } else {
142                m = null;
143            }
144
145            if (m != null) {
146                m.sendToTarget();
147            }
148
149            // Post a message so we dismiss after the above handlers are executed
150            mHandler.obtainMessage(ButtonHandler.MSG_DISMISS_DIALOG, mDialogInterface)
151                    .sendToTarget();
152        }
153    };
154
155    private static final class ButtonHandler extends Handler {
156        // Button clicks have Message.what as the BUTTON{1,2,3} constant
157        private static final int MSG_DISMISS_DIALOG = 1;
158
159        private WeakReference<DialogInterface> mDialog;
160
161        public ButtonHandler(DialogInterface dialog) {
162            mDialog = new WeakReference<>(dialog);
163        }
164
165        @Override
166        public void handleMessage(Message msg) {
167            switch (msg.what) {
168
169                case DialogInterface.BUTTON_POSITIVE:
170                case DialogInterface.BUTTON_NEGATIVE:
171                case DialogInterface.BUTTON_NEUTRAL:
172                    ((DialogInterface.OnClickListener) msg.obj).onClick(mDialog.get(), msg.what);
173                    break;
174
175                case MSG_DISMISS_DIALOG:
176                    ((DialogInterface) msg.obj).dismiss();
177            }
178        }
179    }
180
181    private static boolean shouldCenterSingleButton(Context context) {
182        final TypedValue outValue = new TypedValue();
183        context.getTheme().resolveAttribute(R.attr.alertDialogCenterButtons, outValue, true);
184        return outValue.data != 0;
185    }
186
187    public static final AlertController create(Context context, DialogInterface di, Window window) {
188        final TypedArray a = context.obtainStyledAttributes(
189                null, R.styleable.AlertDialog, R.attr.alertDialogStyle, 0);
190        int controllerType = a.getInt(R.styleable.AlertDialog_controllerType, 0);
191        a.recycle();
192
193        switch (controllerType) {
194            case MICRO:
195                return new MicroAlertController(context, di, window);
196            default:
197                return new AlertController(context, di, window);
198        }
199    }
200
201    protected AlertController(Context context, DialogInterface di, Window window) {
202        mContext = context;
203        mDialogInterface = di;
204        mWindow = window;
205        mHandler = new ButtonHandler(di);
206
207        final TypedArray a = context.obtainStyledAttributes(null,
208                R.styleable.AlertDialog, R.attr.alertDialogStyle, 0);
209
210        mAlertDialogLayout = a.getResourceId(
211                R.styleable.AlertDialog_layout, R.layout.alert_dialog);
212        mButtonPanelSideLayout = a.getResourceId(
213                R.styleable.AlertDialog_buttonPanelSideLayout, 0);
214        mListLayout = a.getResourceId(
215                R.styleable.AlertDialog_listLayout, R.layout.select_dialog);
216
217        mMultiChoiceItemLayout = a.getResourceId(
218                R.styleable.AlertDialog_multiChoiceItemLayout,
219                R.layout.select_dialog_multichoice);
220        mSingleChoiceItemLayout = a.getResourceId(
221                R.styleable.AlertDialog_singleChoiceItemLayout,
222                R.layout.select_dialog_singlechoice);
223        mListItemLayout = a.getResourceId(
224                R.styleable.AlertDialog_listItemLayout,
225                R.layout.select_dialog_item);
226        mShowTitle = a.getBoolean(R.styleable.AlertDialog_showTitle, true);
227
228        a.recycle();
229
230        /* We use a custom title so never request a window title */
231        window.requestFeature(Window.FEATURE_NO_TITLE);
232    }
233
234    static boolean canTextInput(View v) {
235        if (v.onCheckIsTextEditor()) {
236            return true;
237        }
238
239        if (!(v instanceof ViewGroup)) {
240            return false;
241        }
242
243        ViewGroup vg = (ViewGroup)v;
244        int i = vg.getChildCount();
245        while (i > 0) {
246            i--;
247            v = vg.getChildAt(i);
248            if (canTextInput(v)) {
249                return true;
250            }
251        }
252
253        return false;
254    }
255
256    public void installContent(AlertParams params) {
257        params.apply(this);
258        installContent();
259    }
260
261    public void installContent() {
262        int contentView = selectContentView();
263        mWindow.setContentView(contentView);
264        setupView();
265    }
266
267    private int selectContentView() {
268        if (mButtonPanelSideLayout == 0) {
269            return mAlertDialogLayout;
270        }
271        if (mButtonPanelLayoutHint == AlertDialog.LAYOUT_HINT_SIDE) {
272            return mButtonPanelSideLayout;
273        }
274        // TODO: use layout hint side for long messages/lists
275        return mAlertDialogLayout;
276    }
277
278    public void setTitle(CharSequence title) {
279        mTitle = title;
280        if (mTitleView != null) {
281            mTitleView.setText(title);
282        }
283    }
284
285    /**
286     * @see AlertDialog.Builder#setCustomTitle(View)
287     */
288    public void setCustomTitle(View customTitleView) {
289        mCustomTitleView = customTitleView;
290    }
291
292    public void setMessage(CharSequence message) {
293        mMessage = message;
294        if (mMessageView != null) {
295            mMessageView.setText(message);
296        }
297    }
298
299    public void setMessageMovementMethod(MovementMethod movementMethod) {
300        mMessageMovementMethod = movementMethod;
301        if (mMessageView != null) {
302            mMessageView.setMovementMethod(movementMethod);
303        }
304    }
305
306    public void setMessageHyphenationFrequency(
307            @Layout.HyphenationFrequency int hyphenationFrequency) {
308        mMessageHyphenationFrequency = hyphenationFrequency;
309        if (mMessageView != null) {
310            mMessageView.setHyphenationFrequency(hyphenationFrequency);
311        }
312    }
313
314    /**
315     * Set the view resource to display in the dialog.
316     */
317    public void setView(int layoutResId) {
318        mView = null;
319        mViewLayoutResId = layoutResId;
320        mViewSpacingSpecified = false;
321    }
322
323    /**
324     * Set the view to display in the dialog.
325     */
326    public void setView(View view) {
327        mView = view;
328        mViewLayoutResId = 0;
329        mViewSpacingSpecified = false;
330    }
331
332    /**
333     * Set the view to display in the dialog along with the spacing around that view
334     */
335    public void setView(View view, int viewSpacingLeft, int viewSpacingTop, int viewSpacingRight,
336            int viewSpacingBottom) {
337        mView = view;
338        mViewLayoutResId = 0;
339        mViewSpacingSpecified = true;
340        mViewSpacingLeft = viewSpacingLeft;
341        mViewSpacingTop = viewSpacingTop;
342        mViewSpacingRight = viewSpacingRight;
343        mViewSpacingBottom = viewSpacingBottom;
344    }
345
346    /**
347     * Sets a hint for the best button panel layout.
348     */
349    public void setButtonPanelLayoutHint(int layoutHint) {
350        mButtonPanelLayoutHint = layoutHint;
351    }
352
353    /**
354     * Sets a click listener or a message to be sent when the button is clicked.
355     * You only need to pass one of {@code listener} or {@code msg}.
356     *
357     * @param whichButton Which button, can be one of
358     *            {@link DialogInterface#BUTTON_POSITIVE},
359     *            {@link DialogInterface#BUTTON_NEGATIVE}, or
360     *            {@link DialogInterface#BUTTON_NEUTRAL}
361     * @param text The text to display in positive button.
362     * @param listener The {@link DialogInterface.OnClickListener} to use.
363     * @param msg The {@link Message} to be sent when clicked.
364     */
365    public void setButton(int whichButton, CharSequence text,
366            DialogInterface.OnClickListener listener, Message msg) {
367
368        if (msg == null && listener != null) {
369            msg = mHandler.obtainMessage(whichButton, listener);
370        }
371
372        switch (whichButton) {
373
374            case DialogInterface.BUTTON_POSITIVE:
375                mButtonPositiveText = text;
376                mButtonPositiveMessage = msg;
377                break;
378
379            case DialogInterface.BUTTON_NEGATIVE:
380                mButtonNegativeText = text;
381                mButtonNegativeMessage = msg;
382                break;
383
384            case DialogInterface.BUTTON_NEUTRAL:
385                mButtonNeutralText = text;
386                mButtonNeutralMessage = msg;
387                break;
388
389            default:
390                throw new IllegalArgumentException("Button does not exist");
391        }
392    }
393
394    /**
395     * Specifies the icon to display next to the alert title.
396     *
397     * @param resId the resource identifier of the drawable to use as the icon,
398     *            or 0 for no icon
399     */
400    public void setIcon(int resId) {
401        mIcon = null;
402        mIconId = resId;
403
404        if (mIconView != null) {
405            if (resId != 0) {
406                mIconView.setVisibility(View.VISIBLE);
407                mIconView.setImageResource(mIconId);
408            } else {
409                mIconView.setVisibility(View.GONE);
410            }
411        }
412    }
413
414    /**
415     * Specifies the icon to display next to the alert title.
416     *
417     * @param icon the drawable to use as the icon or null for no icon
418     */
419    public void setIcon(Drawable icon) {
420        mIcon = icon;
421        mIconId = 0;
422
423        if (mIconView != null) {
424            if (icon != null) {
425                mIconView.setVisibility(View.VISIBLE);
426                mIconView.setImageDrawable(icon);
427            } else {
428                mIconView.setVisibility(View.GONE);
429            }
430        }
431    }
432
433    /**
434     * @param attrId the attributeId of the theme-specific drawable
435     * to resolve the resourceId for.
436     *
437     * @return resId the resourceId of the theme-specific drawable
438     */
439    public int getIconAttributeResId(int attrId) {
440        TypedValue out = new TypedValue();
441        mContext.getTheme().resolveAttribute(attrId, out, true);
442        return out.resourceId;
443    }
444
445    public void setInverseBackgroundForced(boolean forceInverseBackground) {
446        mForceInverseBackground = forceInverseBackground;
447    }
448
449    public ListView getListView() {
450        return mListView;
451    }
452
453    public Button getButton(int whichButton) {
454        switch (whichButton) {
455            case DialogInterface.BUTTON_POSITIVE:
456                return mButtonPositive;
457            case DialogInterface.BUTTON_NEGATIVE:
458                return mButtonNegative;
459            case DialogInterface.BUTTON_NEUTRAL:
460                return mButtonNeutral;
461            default:
462                return null;
463        }
464    }
465
466    @SuppressWarnings({"UnusedDeclaration"})
467    public boolean onKeyDown(int keyCode, KeyEvent event) {
468        return mScrollView != null && mScrollView.executeKeyEvent(event);
469    }
470
471    @SuppressWarnings({"UnusedDeclaration"})
472    public boolean onKeyUp(int keyCode, KeyEvent event) {
473        return mScrollView != null && mScrollView.executeKeyEvent(event);
474    }
475
476    /**
477     * Resolves whether a custom or default panel should be used. Removes the
478     * default panel if a custom panel should be used. If the resolved panel is
479     * a view stub, inflates before returning.
480     *
481     * @param customPanel the custom panel
482     * @param defaultPanel the default panel
483     * @return the panel to use
484     */
485    @Nullable
486    private ViewGroup resolvePanel(@Nullable View customPanel, @Nullable View defaultPanel) {
487        if (customPanel == null) {
488            // Inflate the default panel, if needed.
489            if (defaultPanel instanceof ViewStub) {
490                defaultPanel = ((ViewStub) defaultPanel).inflate();
491            }
492
493            return (ViewGroup) defaultPanel;
494        }
495
496        // Remove the default panel entirely.
497        if (defaultPanel != null) {
498            final ViewParent parent = defaultPanel.getParent();
499            if (parent instanceof ViewGroup) {
500                ((ViewGroup) parent).removeView(defaultPanel);
501            }
502        }
503
504        // Inflate the custom panel, if needed.
505        if (customPanel instanceof ViewStub) {
506            customPanel = ((ViewStub) customPanel).inflate();
507        }
508
509        return (ViewGroup) customPanel;
510    }
511
512    private void setupView() {
513        final View parentPanel = mWindow.findViewById(R.id.parentPanel);
514        final View defaultTopPanel = parentPanel.findViewById(R.id.topPanel);
515        final View defaultContentPanel = parentPanel.findViewById(R.id.contentPanel);
516        final View defaultButtonPanel = parentPanel.findViewById(R.id.buttonPanel);
517
518        // Install custom content before setting up the title or buttons so
519        // that we can handle panel overrides.
520        final ViewGroup customPanel = (ViewGroup) parentPanel.findViewById(R.id.customPanel);
521        setupCustomContent(customPanel);
522
523        final View customTopPanel = customPanel.findViewById(R.id.topPanel);
524        final View customContentPanel = customPanel.findViewById(R.id.contentPanel);
525        final View customButtonPanel = customPanel.findViewById(R.id.buttonPanel);
526
527        // Resolve the correct panels and remove the defaults, if needed.
528        final ViewGroup topPanel = resolvePanel(customTopPanel, defaultTopPanel);
529        final ViewGroup contentPanel = resolvePanel(customContentPanel, defaultContentPanel);
530        final ViewGroup buttonPanel = resolvePanel(customButtonPanel, defaultButtonPanel);
531
532        setupContent(contentPanel);
533        setupButtons(buttonPanel);
534        setupTitle(topPanel);
535
536        final boolean hasCustomPanel = customPanel != null
537                && customPanel.getVisibility() != View.GONE;
538        final boolean hasTopPanel = topPanel != null
539                && topPanel.getVisibility() != View.GONE;
540        final boolean hasButtonPanel = buttonPanel != null
541                && buttonPanel.getVisibility() != View.GONE;
542
543        // Only display the text spacer if we don't have buttons.
544        if (!hasButtonPanel) {
545            if (contentPanel != null) {
546                final View spacer = contentPanel.findViewById(R.id.textSpacerNoButtons);
547                if (spacer != null) {
548                    spacer.setVisibility(View.VISIBLE);
549                }
550            }
551            mWindow.setCloseOnTouchOutsideIfNotSet(true);
552        }
553
554        if (hasTopPanel) {
555            // Only clip scrolling content to padding if we have a title.
556            if (mScrollView != null) {
557                mScrollView.setClipToPadding(true);
558            }
559
560            // Only show the divider if we have a title.
561            View divider = null;
562            if (mMessage != null || mListView != null || hasCustomPanel) {
563                if (!hasCustomPanel) {
564                    divider = topPanel.findViewById(R.id.titleDividerNoCustom);
565                }
566                if (divider == null) {
567                    divider = topPanel.findViewById(R.id.titleDivider);
568                }
569
570            } else {
571                divider = topPanel.findViewById(R.id.titleDividerTop);
572            }
573
574            if (divider != null) {
575                divider.setVisibility(View.VISIBLE);
576            }
577        } else {
578            if (contentPanel != null) {
579                final View spacer = contentPanel.findViewById(R.id.textSpacerNoTitle);
580                if (spacer != null) {
581                    spacer.setVisibility(View.VISIBLE);
582                }
583            }
584        }
585
586        if (mListView instanceof RecycleListView) {
587            ((RecycleListView) mListView).setHasDecor(hasTopPanel, hasButtonPanel);
588        }
589
590        // Update scroll indicators as needed.
591        if (!hasCustomPanel) {
592            final View content = mListView != null ? mListView : mScrollView;
593            if (content != null) {
594                final int indicators = (hasTopPanel ? View.SCROLL_INDICATOR_TOP : 0)
595                        | (hasButtonPanel ? View.SCROLL_INDICATOR_BOTTOM : 0);
596                content.setScrollIndicators(indicators,
597                        View.SCROLL_INDICATOR_TOP | View.SCROLL_INDICATOR_BOTTOM);
598            }
599        }
600
601        final TypedArray a = mContext.obtainStyledAttributes(
602                null, R.styleable.AlertDialog, R.attr.alertDialogStyle, 0);
603        setBackground(a, topPanel, contentPanel, customPanel, buttonPanel,
604                hasTopPanel, hasCustomPanel, hasButtonPanel);
605        a.recycle();
606    }
607
608    private void setupCustomContent(ViewGroup customPanel) {
609        final View customView;
610        if (mView != null) {
611            customView = mView;
612        } else if (mViewLayoutResId != 0) {
613            final LayoutInflater inflater = LayoutInflater.from(mContext);
614            customView = inflater.inflate(mViewLayoutResId, customPanel, false);
615        } else {
616            customView = null;
617        }
618
619        final boolean hasCustomView = customView != null;
620        if (!hasCustomView || !canTextInput(customView)) {
621            mWindow.setFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM,
622                    WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);
623        }
624
625        if (hasCustomView) {
626            final FrameLayout custom = (FrameLayout) mWindow.findViewById(R.id.custom);
627            custom.addView(customView, new LayoutParams(MATCH_PARENT, MATCH_PARENT));
628
629            if (mViewSpacingSpecified) {
630                custom.setPadding(
631                        mViewSpacingLeft, mViewSpacingTop, mViewSpacingRight, mViewSpacingBottom);
632            }
633
634            if (mListView != null) {
635                ((LinearLayout.LayoutParams) customPanel.getLayoutParams()).weight = 0;
636            }
637        } else {
638            customPanel.setVisibility(View.GONE);
639        }
640    }
641
642    protected void setupTitle(ViewGroup topPanel) {
643        if (mCustomTitleView != null && mShowTitle) {
644            // Add the custom title view directly to the topPanel layout
645            final LayoutParams lp = new LayoutParams(
646                    LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
647
648            topPanel.addView(mCustomTitleView, 0, lp);
649
650            // Hide the title template
651            final View titleTemplate = mWindow.findViewById(R.id.title_template);
652            titleTemplate.setVisibility(View.GONE);
653        } else {
654            mIconView = (ImageView) mWindow.findViewById(R.id.icon);
655
656            final boolean hasTextTitle = !TextUtils.isEmpty(mTitle);
657            if (hasTextTitle && mShowTitle) {
658                // Display the title if a title is supplied, else hide it.
659                mTitleView = (TextView) mWindow.findViewById(R.id.alertTitle);
660                mTitleView.setText(mTitle);
661
662                // Do this last so that if the user has supplied any icons we
663                // use them instead of the default ones. If the user has
664                // specified 0 then make it disappear.
665                if (mIconId != 0) {
666                    mIconView.setImageResource(mIconId);
667                } else if (mIcon != null) {
668                    mIconView.setImageDrawable(mIcon);
669                } else {
670                    // Apply the padding from the icon to ensure the title is
671                    // aligned correctly.
672                    mTitleView.setPadding(mIconView.getPaddingLeft(),
673                            mIconView.getPaddingTop(),
674                            mIconView.getPaddingRight(),
675                            mIconView.getPaddingBottom());
676                    mIconView.setVisibility(View.GONE);
677                }
678            } else {
679                // Hide the title template
680                final View titleTemplate = mWindow.findViewById(R.id.title_template);
681                titleTemplate.setVisibility(View.GONE);
682                mIconView.setVisibility(View.GONE);
683                topPanel.setVisibility(View.GONE);
684            }
685        }
686    }
687
688    protected void setupContent(ViewGroup contentPanel) {
689        mScrollView = (ScrollView) contentPanel.findViewById(R.id.scrollView);
690        mScrollView.setFocusable(false);
691
692        // Special case for users that only want to display a String
693        mMessageView = (TextView) contentPanel.findViewById(R.id.message);
694        if (mMessageView == null) {
695            return;
696        }
697
698        if (mMessage != null) {
699            mMessageView.setText(mMessage);
700            if (mMessageMovementMethod != null) {
701                mMessageView.setMovementMethod(mMessageMovementMethod);
702            }
703            if (mMessageHyphenationFrequency != null) {
704                mMessageView.setHyphenationFrequency(mMessageHyphenationFrequency);
705            }
706        } else {
707            mMessageView.setVisibility(View.GONE);
708            mScrollView.removeView(mMessageView);
709
710            if (mListView != null) {
711                final ViewGroup scrollParent = (ViewGroup) mScrollView.getParent();
712                final int childIndex = scrollParent.indexOfChild(mScrollView);
713                scrollParent.removeViewAt(childIndex);
714                scrollParent.addView(mListView, childIndex,
715                        new LayoutParams(MATCH_PARENT, MATCH_PARENT));
716            } else {
717                contentPanel.setVisibility(View.GONE);
718            }
719        }
720    }
721
722    private static void manageScrollIndicators(View v, View upIndicator, View downIndicator) {
723        if (upIndicator != null) {
724            upIndicator.setVisibility(v.canScrollVertically(-1) ? View.VISIBLE : View.INVISIBLE);
725        }
726        if (downIndicator != null) {
727            downIndicator.setVisibility(v.canScrollVertically(1) ? View.VISIBLE : View.INVISIBLE);
728        }
729    }
730
731    protected void setupButtons(ViewGroup buttonPanel) {
732        int BIT_BUTTON_POSITIVE = 1;
733        int BIT_BUTTON_NEGATIVE = 2;
734        int BIT_BUTTON_NEUTRAL = 4;
735        int whichButtons = 0;
736        mButtonPositive = (Button) buttonPanel.findViewById(R.id.button1);
737        mButtonPositive.setOnClickListener(mButtonHandler);
738
739        if (TextUtils.isEmpty(mButtonPositiveText)) {
740            mButtonPositive.setVisibility(View.GONE);
741        } else {
742            mButtonPositive.setText(mButtonPositiveText);
743            mButtonPositive.setVisibility(View.VISIBLE);
744            whichButtons = whichButtons | BIT_BUTTON_POSITIVE;
745        }
746
747        mButtonNegative = (Button) buttonPanel.findViewById(R.id.button2);
748        mButtonNegative.setOnClickListener(mButtonHandler);
749
750        if (TextUtils.isEmpty(mButtonNegativeText)) {
751            mButtonNegative.setVisibility(View.GONE);
752        } else {
753            mButtonNegative.setText(mButtonNegativeText);
754            mButtonNegative.setVisibility(View.VISIBLE);
755
756            whichButtons = whichButtons | BIT_BUTTON_NEGATIVE;
757        }
758
759        mButtonNeutral = (Button) buttonPanel.findViewById(R.id.button3);
760        mButtonNeutral.setOnClickListener(mButtonHandler);
761
762        if (TextUtils.isEmpty(mButtonNeutralText)) {
763            mButtonNeutral.setVisibility(View.GONE);
764        } else {
765            mButtonNeutral.setText(mButtonNeutralText);
766            mButtonNeutral.setVisibility(View.VISIBLE);
767
768            whichButtons = whichButtons | BIT_BUTTON_NEUTRAL;
769        }
770
771        if (shouldCenterSingleButton(mContext)) {
772            /*
773             * If we only have 1 button it should be centered on the layout and
774             * expand to fill 50% of the available space.
775             */
776            if (whichButtons == BIT_BUTTON_POSITIVE) {
777                centerButton(mButtonPositive);
778            } else if (whichButtons == BIT_BUTTON_NEGATIVE) {
779                centerButton(mButtonNegative);
780            } else if (whichButtons == BIT_BUTTON_NEUTRAL) {
781                centerButton(mButtonNeutral);
782            }
783        }
784
785        final boolean hasButtons = whichButtons != 0;
786        if (!hasButtons) {
787            buttonPanel.setVisibility(View.GONE);
788        }
789    }
790
791    private void centerButton(Button button) {
792        LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) button.getLayoutParams();
793        params.gravity = Gravity.CENTER_HORIZONTAL;
794        params.weight = 0.5f;
795        button.setLayoutParams(params);
796        View leftSpacer = mWindow.findViewById(R.id.leftSpacer);
797        if (leftSpacer != null) {
798            leftSpacer.setVisibility(View.VISIBLE);
799        }
800        View rightSpacer = mWindow.findViewById(R.id.rightSpacer);
801        if (rightSpacer != null) {
802            rightSpacer.setVisibility(View.VISIBLE);
803        }
804    }
805
806    private void setBackground(TypedArray a, View topPanel, View contentPanel, View customPanel,
807            View buttonPanel, boolean hasTitle, boolean hasCustomView, boolean hasButtons) {
808        int fullDark = 0;
809        int topDark = 0;
810        int centerDark = 0;
811        int bottomDark = 0;
812        int fullBright = 0;
813        int topBright = 0;
814        int centerBright = 0;
815        int bottomBright = 0;
816        int bottomMedium = 0;
817
818        // If the needsDefaultBackgrounds attribute is set, we know we're
819        // inheriting from a framework style.
820        final boolean needsDefaultBackgrounds = a.getBoolean(
821                R.styleable.AlertDialog_needsDefaultBackgrounds, true);
822        if (needsDefaultBackgrounds) {
823            fullDark = R.drawable.popup_full_dark;
824            topDark = R.drawable.popup_top_dark;
825            centerDark = R.drawable.popup_center_dark;
826            bottomDark = R.drawable.popup_bottom_dark;
827            fullBright = R.drawable.popup_full_bright;
828            topBright = R.drawable.popup_top_bright;
829            centerBright = R.drawable.popup_center_bright;
830            bottomBright = R.drawable.popup_bottom_bright;
831            bottomMedium = R.drawable.popup_bottom_medium;
832        }
833
834        topBright = a.getResourceId(R.styleable.AlertDialog_topBright, topBright);
835        topDark = a.getResourceId(R.styleable.AlertDialog_topDark, topDark);
836        centerBright = a.getResourceId(R.styleable.AlertDialog_centerBright, centerBright);
837        centerDark = a.getResourceId(R.styleable.AlertDialog_centerDark, centerDark);
838
839        /* We now set the background of all of the sections of the alert.
840         * First collect together each section that is being displayed along
841         * with whether it is on a light or dark background, then run through
842         * them setting their backgrounds.  This is complicated because we need
843         * to correctly use the full, top, middle, and bottom graphics depending
844         * on how many views they are and where they appear.
845         */
846
847        final View[] views = new View[4];
848        final boolean[] light = new boolean[4];
849        View lastView = null;
850        boolean lastLight = false;
851
852        int pos = 0;
853        if (hasTitle) {
854            views[pos] = topPanel;
855            light[pos] = false;
856            pos++;
857        }
858
859        /* The contentPanel displays either a custom text message or
860         * a ListView. If it's text we should use the dark background
861         * for ListView we should use the light background. If neither
862         * are there the contentPanel will be hidden so set it as null.
863         */
864        views[pos] = contentPanel.getVisibility() == View.GONE ? null : contentPanel;
865        light[pos] = mListView != null;
866        pos++;
867
868        if (hasCustomView) {
869            views[pos] = customPanel;
870            light[pos] = mForceInverseBackground;
871            pos++;
872        }
873
874        if (hasButtons) {
875            views[pos] = buttonPanel;
876            light[pos] = true;
877        }
878
879        boolean setView = false;
880        for (pos = 0; pos < views.length; pos++) {
881            final View v = views[pos];
882            if (v == null) {
883                continue;
884            }
885
886            if (lastView != null) {
887                if (!setView) {
888                    lastView.setBackgroundResource(lastLight ? topBright : topDark);
889                } else {
890                    lastView.setBackgroundResource(lastLight ? centerBright : centerDark);
891                }
892                setView = true;
893            }
894
895            lastView = v;
896            lastLight = light[pos];
897        }
898
899        if (lastView != null) {
900            if (setView) {
901                bottomBright = a.getResourceId(R.styleable.AlertDialog_bottomBright, bottomBright);
902                bottomMedium = a.getResourceId(R.styleable.AlertDialog_bottomMedium, bottomMedium);
903                bottomDark = a.getResourceId(R.styleable.AlertDialog_bottomDark, bottomDark);
904
905                // ListViews will use the Bright background, but buttons use the
906                // Medium background.
907                lastView.setBackgroundResource(
908                        lastLight ? (hasButtons ? bottomMedium : bottomBright) : bottomDark);
909            } else {
910                fullBright = a.getResourceId(R.styleable.AlertDialog_fullBright, fullBright);
911                fullDark = a.getResourceId(R.styleable.AlertDialog_fullDark, fullDark);
912
913                lastView.setBackgroundResource(lastLight ? fullBright : fullDark);
914            }
915        }
916
917        final ListView listView = mListView;
918        if (listView != null && mAdapter != null) {
919            listView.setAdapter(mAdapter);
920            final int checkedItem = mCheckedItem;
921            if (checkedItem > -1) {
922                listView.setItemChecked(checkedItem, true);
923                listView.setSelectionFromTop(checkedItem,
924                        a.getDimensionPixelSize(R.styleable.AlertDialog_selectionScrollOffset, 0));
925            }
926        }
927    }
928
929    public static class RecycleListView extends ListView {
930        private final int mPaddingTopNoTitle;
931        private final int mPaddingBottomNoButtons;
932
933        boolean mRecycleOnMeasure = true;
934
935        public RecycleListView(Context context) {
936            this(context, null);
937        }
938
939        public RecycleListView(Context context, AttributeSet attrs) {
940            super(context, attrs);
941
942            final TypedArray ta = context.obtainStyledAttributes(
943                    attrs, R.styleable.RecycleListView);
944            mPaddingBottomNoButtons = ta.getDimensionPixelOffset(
945                    R.styleable.RecycleListView_paddingBottomNoButtons, -1);
946            mPaddingTopNoTitle = ta.getDimensionPixelOffset(
947                    R.styleable.RecycleListView_paddingTopNoTitle, -1);
948        }
949
950        public void setHasDecor(boolean hasTitle, boolean hasButtons) {
951            if (!hasButtons || !hasTitle) {
952                final int paddingLeft = getPaddingLeft();
953                final int paddingTop = hasTitle ? getPaddingTop() : mPaddingTopNoTitle;
954                final int paddingRight = getPaddingRight();
955                final int paddingBottom = hasButtons ? getPaddingBottom() : mPaddingBottomNoButtons;
956                setPadding(paddingLeft, paddingTop, paddingRight, paddingBottom);
957            }
958        }
959
960        @Override
961        protected boolean recycleOnMeasure() {
962            return mRecycleOnMeasure;
963        }
964    }
965
966    public static class AlertParams {
967        public final Context mContext;
968        public final LayoutInflater mInflater;
969
970        public int mIconId = 0;
971        public Drawable mIcon;
972        public int mIconAttrId = 0;
973        public CharSequence mTitle;
974        public View mCustomTitleView;
975        public CharSequence mMessage;
976        public CharSequence mPositiveButtonText;
977        public DialogInterface.OnClickListener mPositiveButtonListener;
978        public CharSequence mNegativeButtonText;
979        public DialogInterface.OnClickListener mNegativeButtonListener;
980        public CharSequence mNeutralButtonText;
981        public DialogInterface.OnClickListener mNeutralButtonListener;
982        public boolean mCancelable;
983        public DialogInterface.OnCancelListener mOnCancelListener;
984        public DialogInterface.OnDismissListener mOnDismissListener;
985        public DialogInterface.OnKeyListener mOnKeyListener;
986        public CharSequence[] mItems;
987        public ListAdapter mAdapter;
988        public DialogInterface.OnClickListener mOnClickListener;
989        public int mViewLayoutResId;
990        public View mView;
991        public int mViewSpacingLeft;
992        public int mViewSpacingTop;
993        public int mViewSpacingRight;
994        public int mViewSpacingBottom;
995        public boolean mViewSpacingSpecified = false;
996        public boolean[] mCheckedItems;
997        public boolean mIsMultiChoice;
998        public boolean mIsSingleChoice;
999        public int mCheckedItem = -1;
1000        public DialogInterface.OnMultiChoiceClickListener mOnCheckboxClickListener;
1001        public Cursor mCursor;
1002        public String mLabelColumn;
1003        public String mIsCheckedColumn;
1004        public boolean mForceInverseBackground;
1005        public AdapterView.OnItemSelectedListener mOnItemSelectedListener;
1006        public OnPrepareListViewListener mOnPrepareListViewListener;
1007        public boolean mRecycleOnMeasure = true;
1008
1009        /**
1010         * Interface definition for a callback to be invoked before the ListView
1011         * will be bound to an adapter.
1012         */
1013        public interface OnPrepareListViewListener {
1014
1015            /**
1016             * Called before the ListView is bound to an adapter.
1017             * @param listView The ListView that will be shown in the dialog.
1018             */
1019            void onPrepareListView(ListView listView);
1020        }
1021
1022        public AlertParams(Context context) {
1023            mContext = context;
1024            mCancelable = true;
1025            mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
1026        }
1027
1028        public void apply(AlertController dialog) {
1029            if (mCustomTitleView != null) {
1030                dialog.setCustomTitle(mCustomTitleView);
1031            } else {
1032                if (mTitle != null) {
1033                    dialog.setTitle(mTitle);
1034                }
1035                if (mIcon != null) {
1036                    dialog.setIcon(mIcon);
1037                }
1038                if (mIconId != 0) {
1039                    dialog.setIcon(mIconId);
1040                }
1041                if (mIconAttrId != 0) {
1042                    dialog.setIcon(dialog.getIconAttributeResId(mIconAttrId));
1043                }
1044            }
1045            if (mMessage != null) {
1046                dialog.setMessage(mMessage);
1047            }
1048            if (mPositiveButtonText != null) {
1049                dialog.setButton(DialogInterface.BUTTON_POSITIVE, mPositiveButtonText,
1050                        mPositiveButtonListener, null);
1051            }
1052            if (mNegativeButtonText != null) {
1053                dialog.setButton(DialogInterface.BUTTON_NEGATIVE, mNegativeButtonText,
1054                        mNegativeButtonListener, null);
1055            }
1056            if (mNeutralButtonText != null) {
1057                dialog.setButton(DialogInterface.BUTTON_NEUTRAL, mNeutralButtonText,
1058                        mNeutralButtonListener, null);
1059            }
1060            if (mForceInverseBackground) {
1061                dialog.setInverseBackgroundForced(true);
1062            }
1063            // For a list, the client can either supply an array of items or an
1064            // adapter or a cursor
1065            if ((mItems != null) || (mCursor != null) || (mAdapter != null)) {
1066                createListView(dialog);
1067            }
1068            if (mView != null) {
1069                if (mViewSpacingSpecified) {
1070                    dialog.setView(mView, mViewSpacingLeft, mViewSpacingTop, mViewSpacingRight,
1071                            mViewSpacingBottom);
1072                } else {
1073                    dialog.setView(mView);
1074                }
1075            } else if (mViewLayoutResId != 0) {
1076                dialog.setView(mViewLayoutResId);
1077            }
1078
1079            /*
1080            dialog.setCancelable(mCancelable);
1081            dialog.setOnCancelListener(mOnCancelListener);
1082            if (mOnKeyListener != null) {
1083                dialog.setOnKeyListener(mOnKeyListener);
1084            }
1085            */
1086        }
1087
1088        private void createListView(final AlertController dialog) {
1089            final RecycleListView listView =
1090                    (RecycleListView) mInflater.inflate(dialog.mListLayout, null);
1091            final ListAdapter adapter;
1092
1093            if (mIsMultiChoice) {
1094                if (mCursor == null) {
1095                    adapter = new ArrayAdapter<CharSequence>(
1096                            mContext, dialog.mMultiChoiceItemLayout, R.id.text1, mItems) {
1097                        @Override
1098                        public View getView(int position, View convertView, ViewGroup parent) {
1099                            View view = super.getView(position, convertView, parent);
1100                            if (mCheckedItems != null) {
1101                                boolean isItemChecked = mCheckedItems[position];
1102                                if (isItemChecked) {
1103                                    listView.setItemChecked(position, true);
1104                                }
1105                            }
1106                            return view;
1107                        }
1108                    };
1109                } else {
1110                    adapter = new CursorAdapter(mContext, mCursor, false) {
1111                        private final int mLabelIndex;
1112                        private final int mIsCheckedIndex;
1113
1114                        {
1115                            final Cursor cursor = getCursor();
1116                            mLabelIndex = cursor.getColumnIndexOrThrow(mLabelColumn);
1117                            mIsCheckedIndex = cursor.getColumnIndexOrThrow(mIsCheckedColumn);
1118                        }
1119
1120                        @Override
1121                        public void bindView(View view, Context context, Cursor cursor) {
1122                            CheckedTextView text = (CheckedTextView) view.findViewById(R.id.text1);
1123                            text.setText(cursor.getString(mLabelIndex));
1124                            listView.setItemChecked(
1125                                    cursor.getPosition(),
1126                                    cursor.getInt(mIsCheckedIndex) == 1);
1127                        }
1128
1129                        @Override
1130                        public View newView(Context context, Cursor cursor, ViewGroup parent) {
1131                            return mInflater.inflate(dialog.mMultiChoiceItemLayout,
1132                                    parent, false);
1133                        }
1134
1135                    };
1136                }
1137            } else {
1138                final int layout;
1139                if (mIsSingleChoice) {
1140                    layout = dialog.mSingleChoiceItemLayout;
1141                } else {
1142                    layout = dialog.mListItemLayout;
1143                }
1144
1145                if (mCursor != null) {
1146                    adapter = new SimpleCursorAdapter(mContext, layout, mCursor,
1147                            new String[] { mLabelColumn }, new int[] { R.id.text1 });
1148                } else if (mAdapter != null) {
1149                    adapter = mAdapter;
1150                } else {
1151                    adapter = new CheckedItemAdapter(mContext, layout, R.id.text1, mItems);
1152                }
1153            }
1154
1155            if (mOnPrepareListViewListener != null) {
1156                mOnPrepareListViewListener.onPrepareListView(listView);
1157            }
1158
1159            /* Don't directly set the adapter on the ListView as we might
1160             * want to add a footer to the ListView later.
1161             */
1162            dialog.mAdapter = adapter;
1163            dialog.mCheckedItem = mCheckedItem;
1164
1165            if (mOnClickListener != null) {
1166                listView.setOnItemClickListener(new OnItemClickListener() {
1167                    @Override
1168                    public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
1169                        mOnClickListener.onClick(dialog.mDialogInterface, position);
1170                        if (!mIsSingleChoice) {
1171                            dialog.mDialogInterface.dismiss();
1172                        }
1173                    }
1174                });
1175            } else if (mOnCheckboxClickListener != null) {
1176                listView.setOnItemClickListener(new OnItemClickListener() {
1177                    @Override
1178                    public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
1179                        if (mCheckedItems != null) {
1180                            mCheckedItems[position] = listView.isItemChecked(position);
1181                        }
1182                        mOnCheckboxClickListener.onClick(
1183                                dialog.mDialogInterface, position, listView.isItemChecked(position));
1184                    }
1185                });
1186            }
1187
1188            // Attach a given OnItemSelectedListener to the ListView
1189            if (mOnItemSelectedListener != null) {
1190                listView.setOnItemSelectedListener(mOnItemSelectedListener);
1191            }
1192
1193            if (mIsSingleChoice) {
1194                listView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
1195            } else if (mIsMultiChoice) {
1196                listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
1197            }
1198            listView.mRecycleOnMeasure = mRecycleOnMeasure;
1199            dialog.mListView = listView;
1200        }
1201    }
1202
1203    private static class CheckedItemAdapter extends ArrayAdapter<CharSequence> {
1204        public CheckedItemAdapter(Context context, int resource, int textViewResourceId,
1205                CharSequence[] objects) {
1206            super(context, resource, textViewResourceId, objects);
1207        }
1208
1209        @Override
1210        public boolean hasStableIds() {
1211            return true;
1212        }
1213
1214        @Override
1215        public long getItemId(int position) {
1216            return position;
1217        }
1218    }
1219}
1220