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