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