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