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