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