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