1 /*
2  * Copyright (C) 2019 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 package com.android.car.ui;
17 
18 import static android.view.WindowInsets.Type.ime;
19 
20 import static com.android.car.ui.core.CarUi.MIN_TARGET_API;
21 import static com.android.car.ui.imewidescreen.CarUiImeWideScreenController.ADD_DESC_TITLE_TO_CONTENT_AREA;
22 import static com.android.car.ui.imewidescreen.CarUiImeWideScreenController.ADD_DESC_TO_CONTENT_AREA;
23 import static com.android.car.ui.imewidescreen.CarUiImeWideScreenController.WIDE_SCREEN_ACTION;
24 
25 import android.annotation.TargetApi;
26 import android.app.AlertDialog;
27 import android.app.Dialog;
28 import android.content.Context;
29 import android.content.DialogInterface;
30 import android.database.Cursor;
31 import android.graphics.drawable.Drawable;
32 import android.os.Build;
33 import android.os.Build.VERSION;
34 import android.os.Build.VERSION_CODES;
35 import android.os.Bundle;
36 import android.text.Editable;
37 import android.text.InputFilter;
38 import android.text.TextUtils;
39 import android.text.TextWatcher;
40 import android.text.method.LinkMovementMethod;
41 import android.view.LayoutInflater;
42 import android.view.View;
43 import android.view.ViewGroup;
44 import android.view.inputmethod.InputMethodManager;
45 import android.widget.AdapterView;
46 import android.widget.EditText;
47 import android.widget.ImageView;
48 import android.widget.ListAdapter;
49 import android.widget.TextView;
50 
51 import androidx.annotation.ArrayRes;
52 import androidx.annotation.AttrRes;
53 import androidx.annotation.DrawableRes;
54 import androidx.annotation.NonNull;
55 import androidx.annotation.StringRes;
56 import androidx.recyclerview.widget.LinearLayoutManager;
57 import androidx.recyclerview.widget.RecyclerView;
58 
59 import com.android.car.ui.recyclerview.CarUiListItemAdapter;
60 import com.android.car.ui.recyclerview.CarUiRadioButtonListItemAdapter;
61 import com.android.car.ui.utils.CarUiUtils;
62 
63 /**
64  * Wrapper for AlertDialog.Builder
65  * <p>
66  * Rendered views will comply with
67  * <a href="https://source.android.com/devices/automotive/hmi/car_ui/appendix_b">customization guardrails</a>
68  */
69 @TargetApi(MIN_TARGET_API)
70 public class AlertDialogBuilder {
71 
72     private AlertDialog.Builder mBuilder;
73     private Context mContext;
74     private boolean mPositiveButtonSet;
75     private boolean mNeutralButtonSet;
76     private boolean mNegativeButtonSet;
77     private CharSequence mTitle;
78     private CharSequence mSubtitle;
79     private Drawable mIcon;
80     private boolean mIconTinted;
81     private EditText mCarUiEditText;
82     private InputMethodManager mInputMethodManager;
83     private String mWideScreenTitle;
84     private String mWideScreenTitleDesc;
85     private ViewGroup mRoot;
86     private boolean mAllowDismissButton = true;
87     private boolean mHasSingleChoiceBodyButton = false;
88 
89     private final TextWatcher mTextWatcherWideScreen = new TextWatcher() {
90         @Override
91         public void beforeTextChanged(CharSequence s, int start, int count, int after) {
92 
93         }
94 
95         @Override
96         public void onTextChanged(CharSequence s, int start, int before, int count) {
97             if (VERSION.SDK_INT >= VERSION_CODES.R) {
98                 Bundle bundle = new Bundle();
99                 String titleString = mWideScreenTitle != null ? mWideScreenTitle
100                         : mTitle.toString();
101                 bundle.putString(ADD_DESC_TITLE_TO_CONTENT_AREA, titleString);
102                 bundle.putString(ADD_DESC_TO_CONTENT_AREA, s.toString());
103                 mInputMethodManager.sendAppPrivateCommand(mCarUiEditText, WIDE_SCREEN_ACTION,
104                     bundle);
105             }
106         }
107 
108         @Override
109         public void afterTextChanged(Editable s) {
110 
111         }
112     };
113 
114     // Whenever the IME is closed and opened again, the title and desc information needs to be
115     // passed to the IME to be rendered. If the information is not passed to the IME the content
116     // area of the IME will render nothing into the content area.
117     private final View.OnApplyWindowInsetsListener mOnApplyWindowInsetsListener = (v, insets) -> {
118         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
119             // WindowInsets.isVisible() is only available on R or above
120             return v.onApplyWindowInsets(insets);
121         }
122 
123         if (insets.isVisible(ime())) {
124             Bundle bundle = new Bundle();
125             String title = mWideScreenTitle != null ? mWideScreenTitle : mTitle.toString();
126             bundle.putString(ADD_DESC_TITLE_TO_CONTENT_AREA, title);
127             if (mWideScreenTitleDesc != null) {
128                 bundle.putString(ADD_DESC_TO_CONTENT_AREA, mWideScreenTitleDesc);
129             }
130             mInputMethodManager.sendAppPrivateCommand(mCarUiEditText, WIDE_SCREEN_ACTION,
131                     bundle);
132         }
133         return v.onApplyWindowInsets(insets);
134     };
135 
136     private final DialogInterface.OnDismissListener mOnDismissListener = dialog -> {
137         if (mRoot != null) {
138             mRoot.setOnApplyWindowInsetsListener(null);
139         }
140     };
141 
AlertDialogBuilder(Context context)142     public AlertDialogBuilder(Context context) {
143         // Resource id specified as 0 uses the parent contexts resolved value for alertDialogTheme.
144         this(context, /* themeResId= */0);
145     }
146 
AlertDialogBuilder(Context context, int themeResId)147     public AlertDialogBuilder(Context context, int themeResId) {
148         mBuilder = new AlertDialog.Builder(context, themeResId);
149         mInputMethodManager = (InputMethodManager)
150                 context.getSystemService(Context.INPUT_METHOD_SERVICE);
151         mContext = context;
152     }
153 
getContext()154     public Context getContext() {
155         return mBuilder.getContext();
156     }
157 
158     /**
159      * Set the title using the given resource id.
160      *
161      * @return This Builder object to allow for chaining of calls to set methods
162      */
setTitle(@tringRes int titleId)163     public AlertDialogBuilder setTitle(@StringRes int titleId) {
164         return setTitle(mContext.getText(titleId));
165     }
166 
167     /**
168      * Set the title displayed in the {@link Dialog}.
169      *
170      * @return This Builder object to allow for chaining of calls to set methods
171      */
setTitle(CharSequence title)172     public AlertDialogBuilder setTitle(CharSequence title) {
173         mTitle = title;
174         mBuilder.setTitle(title);
175         return this;
176     }
177 
178     /**
179      * Sets a subtitle to be displayed in the {@link Dialog}.
180      *
181      * @return This Builder object to allow for chaining of calls to set methods
182      */
setSubtitle(@tringRes int subtitle)183     public AlertDialogBuilder setSubtitle(@StringRes int subtitle) {
184         return setSubtitle(mContext.getString(subtitle));
185     }
186 
187     /**
188      * Sets a subtitle to be displayed in the {@link Dialog}.
189      *
190      * @return This Builder object to allow for chaining of calls to set methods
191      */
setSubtitle(CharSequence subtitle)192     public AlertDialogBuilder setSubtitle(CharSequence subtitle) {
193         mSubtitle = subtitle;
194         return this;
195     }
196 
197     /**
198      * Set the message to display using the given resource id.
199      *
200      * @return This Builder object to allow for chaining of calls to set methods
201      */
setMessage(@tringRes int messageId)202     public AlertDialogBuilder setMessage(@StringRes int messageId) {
203         mBuilder.setMessage(messageId);
204         return this;
205     }
206 
207     /**
208      * Set the message to display.
209      *
210      * @return This Builder object to allow for chaining of calls to set methods
211      */
setMessage(CharSequence message)212     public AlertDialogBuilder setMessage(CharSequence message) {
213         mBuilder.setMessage(message);
214         return this;
215     }
216 
217     /**
218      * Set the resource id of the {@link Drawable} to be used in the title.
219      * <p>
220      * Takes precedence over values set using {@link #setIcon(Drawable)}.
221      *
222      * @return This Builder object to allow for chaining of calls to set methods
223      */
setIcon(@rawableRes int iconId)224     public AlertDialogBuilder setIcon(@DrawableRes int iconId) {
225         return setIcon(mContext.getDrawable(iconId));
226     }
227 
228     /**
229      * Set the {@link Drawable} to be used in the title.
230      * <p>
231      * <strong>Note:</strong> To ensure consistent styling, the drawable
232      * should be inflated or constructed using the alert dialog's themed
233      * context obtained via {@link #getContext()}.
234      *
235      * @return this Builder object to allow for chaining of calls to set
236      * methods
237      */
setIcon(Drawable icon)238     public AlertDialogBuilder setIcon(Drawable icon) {
239         mIcon = icon;
240         return this;
241     }
242 
243     /**
244      * Whether the icon provided by {@link #setIcon(Drawable)} should be
245      * tinted with the default system color.
246      *
247      * @return this Builder object to allow for chaining of calls to set
248      * methods.
249      */
setIconTinted(boolean tinted)250     public AlertDialogBuilder setIconTinted(boolean tinted) {
251         mIconTinted = tinted;
252         return this;
253     }
254 
255     /**
256      * Set an icon as supplied by a theme attribute. e.g.
257      * {@link android.R.attr#alertDialogIcon}.
258      * <p>
259      * Takes precedence over values set using {@link #setIcon(Drawable)}.
260      *
261      * @param attrId ID of a theme attribute that points to a drawable resource.
262      */
setIconAttribute(@ttrRes int attrId)263     public AlertDialogBuilder setIconAttribute(@AttrRes int attrId) {
264         mBuilder.setIconAttribute(attrId);
265         return this;
266     }
267 
268     /**
269      * Set a listener to be invoked when the positive button of the dialog is pressed.
270      *
271      * @param textId   The resource id of the text to display in the positive button
272      * @param listener The {@link DialogInterface.OnClickListener} to use.
273      * @return This Builder object to allow for chaining of calls to set methods
274      */
setPositiveButton(@tringRes int textId, final DialogInterface.OnClickListener listener)275     public AlertDialogBuilder setPositiveButton(@StringRes int textId,
276             final DialogInterface.OnClickListener listener) {
277         mBuilder.setPositiveButton(textId, listener);
278         mPositiveButtonSet = true;
279         return this;
280     }
281 
282     /**
283      * Set a listener to be invoked when the positive button of the dialog is pressed.
284      *
285      * @param text     The text to display in the positive button
286      * @param listener The {@link DialogInterface.OnClickListener} to use.
287      * @return This Builder object to allow for chaining of calls to set methods
288      */
setPositiveButton(CharSequence text, final DialogInterface.OnClickListener listener)289     public AlertDialogBuilder setPositiveButton(CharSequence text,
290             final DialogInterface.OnClickListener listener) {
291         mBuilder.setPositiveButton(text, listener);
292         mPositiveButtonSet = true;
293         return this;
294     }
295 
296     /**
297      * Set a listener to be invoked when the negative button of the dialog is pressed.
298      *
299      * @param textId   The resource id of the text to display in the negative button
300      * @param listener The {@link DialogInterface.OnClickListener} to use.
301      * @return This Builder object to allow for chaining of calls to set methods
302      */
setNegativeButton(@tringRes int textId, final DialogInterface.OnClickListener listener)303     public AlertDialogBuilder setNegativeButton(@StringRes int textId,
304             final DialogInterface.OnClickListener listener) {
305         mBuilder.setNegativeButton(textId, listener);
306         mNegativeButtonSet = true;
307         return this;
308     }
309 
310     /**
311      * Set a listener to be invoked when the negative button of the dialog is pressed.
312      *
313      * @param text     The text to display in the negative button
314      * @param listener The {@link DialogInterface.OnClickListener} to use.
315      * @return This Builder object to allow for chaining of calls to set methods
316      */
setNegativeButton(CharSequence text, final DialogInterface.OnClickListener listener)317     public AlertDialogBuilder setNegativeButton(CharSequence text,
318             final DialogInterface.OnClickListener listener) {
319         mBuilder.setNegativeButton(text, listener);
320         mNegativeButtonSet = true;
321         return this;
322     }
323 
324     /**
325      * Set a listener to be invoked when the neutral button of the dialog is pressed.
326      *
327      * @param textId   The resource id of the text to display in the neutral button
328      * @param listener The {@link DialogInterface.OnClickListener} to use.
329      * @return This Builder object to allow for chaining of calls to set methods
330      */
setNeutralButton(@tringRes int textId, final DialogInterface.OnClickListener listener)331     public AlertDialogBuilder setNeutralButton(@StringRes int textId,
332             final DialogInterface.OnClickListener listener) {
333         mBuilder.setNeutralButton(textId, listener);
334         mNeutralButtonSet = true;
335         return this;
336     }
337 
338     /**
339      * Set a listener to be invoked when the neutral button of the dialog is pressed.
340      *
341      * @param text     The text to display in the neutral button
342      * @param listener The {@link DialogInterface.OnClickListener} to use.
343      * @return This Builder object to allow for chaining of calls to set methods
344      */
setNeutralButton(CharSequence text, final DialogInterface.OnClickListener listener)345     public AlertDialogBuilder setNeutralButton(CharSequence text,
346             final DialogInterface.OnClickListener listener) {
347         mBuilder.setNeutralButton(text, listener);
348         mNeutralButtonSet = true;
349         return this;
350     }
351 
352     /**
353      * Sets whether the dialog is cancelable or not.  Default is true.
354      *
355      * @return This Builder object to allow for chaining of calls to set methods
356      */
setCancelable(boolean cancelable)357     public AlertDialogBuilder setCancelable(boolean cancelable) {
358         mBuilder.setCancelable(cancelable);
359         return this;
360     }
361 
362     /**
363      * Sets the callback that will be called if the dialog is canceled.
364      *
365      * <p>Even in a cancelable dialog, the dialog may be dismissed for reasons other than
366      * being canceled or one of the supplied choices being selected.
367      * If you are interested in listening for all cases where the dialog is dismissed
368      * and not just when it is canceled, see
369      * {@link #setOnDismissListener(android.content.DialogInterface.OnDismissListener)
370      * setOnDismissListener}.</p>
371      *
372      * @return This Builder object to allow for chaining of calls to set methods
373      * @see #setCancelable(boolean)
374      * @see #setOnDismissListener(android.content.DialogInterface.OnDismissListener)
375      */
setOnCancelListener( DialogInterface.OnCancelListener onCancelListener)376     public AlertDialogBuilder setOnCancelListener(
377             DialogInterface.OnCancelListener onCancelListener) {
378         mBuilder.setOnCancelListener(onCancelListener);
379         return this;
380     }
381 
382     /**
383      * Sets the callback that will be called when the dialog is dismissed for any reason.
384      *
385      * @return This Builder object to allow for chaining of calls to set methods
386      */
setOnDismissListener( DialogInterface.OnDismissListener onDismissListener)387     public AlertDialogBuilder setOnDismissListener(
388             DialogInterface.OnDismissListener onDismissListener) {
389         mBuilder.setOnDismissListener(onDismissListener);
390         return this;
391     }
392 
393     /**
394      * Sets the callback that will be called if a key is dispatched to the dialog.
395      *
396      * @return This Builder object to allow for chaining of calls to set methods
397      */
setOnKeyListener(DialogInterface.OnKeyListener onKeyListener)398     public AlertDialogBuilder setOnKeyListener(DialogInterface.OnKeyListener onKeyListener) {
399         mBuilder.setOnKeyListener(onKeyListener);
400         return this;
401     }
402 
403     /**
404      * Set a list of items to be displayed in the dialog as the content, you will be notified of the
405      * selected item via the supplied listener. This should be an array type i.e. R.array.foo
406      *
407      * @return This Builder object to allow for chaining of calls to set methods
408      */
setItems(@rrayRes int itemsId, final DialogInterface.OnClickListener listener)409     public AlertDialogBuilder setItems(@ArrayRes int itemsId,
410             final DialogInterface.OnClickListener listener) {
411         mBuilder.setItems(itemsId, listener);
412         mHasSingleChoiceBodyButton = true;
413         return this;
414     }
415 
416     /**
417      * Set a list of items to be displayed in the dialog as the content, you will be notified of the
418      * selected item via the supplied listener.
419      *
420      * @return This Builder object to allow for chaining of calls to set methods
421      */
setItems(CharSequence[] items, final DialogInterface.OnClickListener listener)422     public AlertDialogBuilder setItems(CharSequence[] items,
423             final DialogInterface.OnClickListener listener) {
424         mBuilder.setItems(items, listener);
425         mHasSingleChoiceBodyButton = true;
426         return this;
427     }
428 
429     /**
430      * This was not supposed to be in the Chassis API because it allows custom views.
431      *
432      * @deprecated Use {@link #setAdapter(CarUiListItemAdapter)} instead.
433      */
434     @Deprecated
setAdapter(final ListAdapter adapter, final DialogInterface.OnClickListener listener)435     public AlertDialogBuilder setAdapter(final ListAdapter adapter,
436             final DialogInterface.OnClickListener listener) {
437         mBuilder.setAdapter(adapter, listener);
438         mHasSingleChoiceBodyButton = true;
439         return this;
440     }
441 
442     /**
443      * Display all the {@link com.android.car.ui.recyclerview.CarUiListItem CarUiListItems} in a
444      * {@link CarUiListItemAdapter}. You should set click listeners on the CarUiListItems as
445      * opposed to a callback in this function.
446      */
setAdapter(final CarUiListItemAdapter adapter)447     public AlertDialogBuilder setAdapter(final CarUiListItemAdapter adapter) {
448         setCustomList(adapter);
449         mHasSingleChoiceBodyButton = true;
450         return this;
451     }
452 
setCustomList(@onNull CarUiListItemAdapter adapter)453     private void setCustomList(@NonNull CarUiListItemAdapter adapter) {
454         View customList = LayoutInflater.from(mContext).inflate(
455                 R.layout.car_ui_alert_dialog_list, null);
456         RecyclerView list = CarUiUtils.requireViewByRefId(customList, R.id.list);
457         list.setLayoutManager(new LinearLayoutManager(mContext));
458         list.setAdapter(adapter);
459         list.setFocusable(false);
460         mBuilder.setView(customList);
461     }
462 
463     /**
464      * Set a list of items, which are supplied by the given {@link Cursor}, to be
465      * displayed in the dialog as the content, you will be notified of the
466      * selected item via the supplied listener.
467      *
468      * @param cursor      The {@link Cursor} to supply the list of items
469      * @param listener    The listener that will be called when an item is clicked.
470      * @param labelColumn The column name on the cursor containing the string to display
471      *                    in the label.
472      * @return This Builder object to allow for chaining of calls to set methods
473      */
setCursor(final Cursor cursor, final DialogInterface.OnClickListener listener, String labelColumn)474     public AlertDialogBuilder setCursor(final Cursor cursor,
475             final DialogInterface.OnClickListener listener,
476             String labelColumn) {
477         mBuilder.setCursor(cursor, listener, labelColumn);
478         mHasSingleChoiceBodyButton = true;
479         return this;
480     }
481 
482     /**
483      * Set a list of items to be displayed in the dialog as the content,
484      * you will be notified of the selected item via the supplied listener.
485      * This should be an array type, e.g. R.array.foo. The list will have
486      * a check mark displayed to the right of the text for each checked
487      * item. Clicking on an item in the list will not dismiss the dialog.
488      * Clicking on a button will dismiss the dialog.
489      *
490      * @param itemsId      the resource id of an array i.e. R.array.foo
491      * @param checkedItems specifies which items are checked. It should be null in which case no
492      *                     items are checked. If non null it must be exactly the same length as the
493      *                     array of
494      *                     items.
495      * @param listener     notified when an item on the list is clicked. The dialog will not be
496      *                     dismissed when an item is clicked. It will only be dismissed if clicked
497      *                     on a
498      *                     button, if no buttons are supplied it's up to the user to dismiss the
499      *                     dialog.
500      * @return This Builder object to allow for chaining of calls to set methods
501      */
setMultiChoiceItems(@rrayRes int itemsId, boolean[] checkedItems, final DialogInterface.OnMultiChoiceClickListener listener)502     public AlertDialogBuilder setMultiChoiceItems(@ArrayRes int itemsId, boolean[] checkedItems,
503             final DialogInterface.OnMultiChoiceClickListener listener) {
504         mBuilder.setMultiChoiceItems(itemsId, checkedItems, listener);
505         mHasSingleChoiceBodyButton = false;
506         return this;
507     }
508 
509     /**
510      * Set a list of items to be displayed in the dialog as the content,
511      * you will be notified of the selected item via the supplied listener.
512      * The list will have a check mark displayed to the right of the text
513      * for each checked item. Clicking on an item in the list will not
514      * dismiss the dialog. Clicking on a button will dismiss the dialog.
515      *
516      * @param items        the text of the items to be displayed in the list.
517      * @param checkedItems specifies which items are checked. It should be null in which case no
518      *                     items are checked. If non null it must be exactly the same length as the
519      *                     array of
520      *                     items.
521      * @param listener     notified when an item on the list is clicked. The dialog will not be
522      *                     dismissed when an item is clicked. It will only be dismissed if clicked
523      *                     on a
524      *                     button, if no buttons are supplied it's up to the user to dismiss the
525      *                     dialog.
526      * @return This Builder object to allow for chaining of calls to set methods
527      */
setMultiChoiceItems(CharSequence[] items, boolean[] checkedItems, final DialogInterface.OnMultiChoiceClickListener listener)528     public AlertDialogBuilder setMultiChoiceItems(CharSequence[] items, boolean[] checkedItems,
529             final DialogInterface.OnMultiChoiceClickListener listener) {
530         mBuilder.setMultiChoiceItems(items, checkedItems, listener);
531         mHasSingleChoiceBodyButton = false;
532         return this;
533     }
534 
535     /**
536      * Set a list of items to be displayed in the dialog as the content,
537      * you will be notified of the selected item via the supplied listener.
538      * The list will have a check mark displayed to the right of the text
539      * for each checked item. Clicking on an item in the list will not
540      * dismiss the dialog. Clicking on a button will dismiss the dialog.
541      *
542      * @param cursor          the cursor used to provide the items.
543      * @param isCheckedColumn specifies the column name on the cursor to use to determine
544      *                        whether a checkbox is checked or not. It must return an integer value
545      *                        where 1
546      *                        means checked and 0 means unchecked.
547      * @param labelColumn     The column name on the cursor containing the string to display in the
548      *                        label.
549      * @param listener        notified when an item on the list is clicked. The dialog will not be
550      *                        dismissed when an item is clicked. It will only be dismissed if
551      *                        clicked on a
552      *                        button, if no buttons are supplied it's up to the user to dismiss the
553      *                        dialog.
554      * @return This Builder object to allow for chaining of calls to set methods
555      */
setMultiChoiceItems(Cursor cursor, String isCheckedColumn, String labelColumn, final DialogInterface.OnMultiChoiceClickListener listener)556     public AlertDialogBuilder setMultiChoiceItems(Cursor cursor, String isCheckedColumn,
557             String labelColumn,
558             final DialogInterface.OnMultiChoiceClickListener listener) {
559         mBuilder.setMultiChoiceItems(cursor, isCheckedColumn, labelColumn, listener);
560         mHasSingleChoiceBodyButton = false;
561         return this;
562     }
563 
564     /**
565      * Set a list of items to be displayed in the dialog as the content, you will be notified of
566      * the selected item via the supplied listener. This should be an array type i.e.
567      * R.array.foo The list will have a check mark displayed to the right of the text for the
568      * checked item. Clicking on an item in the list will not dismiss the dialog. Clicking on a
569      * button will dismiss the dialog.
570      *
571      * @param itemsId     the resource id of an array i.e. R.array.foo
572      * @param checkedItem specifies which item is checked. If -1 no items are checked.
573      * @param listener    notified when an item on the list is clicked. The dialog will not be
574      *                    dismissed when an item is clicked. It will only be dismissed if clicked on
575      *                    a
576      *                    button, if no buttons are supplied it's up to the user to dismiss the
577      *                    dialog.
578      * @return This Builder object to allow for chaining of calls to set methods
579      */
setSingleChoiceItems(@rrayRes int itemsId, int checkedItem, final DialogInterface.OnClickListener listener)580     public AlertDialogBuilder setSingleChoiceItems(@ArrayRes int itemsId, int checkedItem,
581             final DialogInterface.OnClickListener listener) {
582         mBuilder.setSingleChoiceItems(itemsId, checkedItem, listener);
583         mHasSingleChoiceBodyButton = true;
584         return this;
585     }
586 
587     /**
588      * Set a list of items to be displayed in the dialog as the content, you will be notified of
589      * the selected item via the supplied listener. The list will have a check mark displayed to
590      * the right of the text for the checked item. Clicking on an item in the list will not
591      * dismiss the dialog. Clicking on a button will dismiss the dialog.
592      *
593      * @param cursor      the cursor to retrieve the items from.
594      * @param checkedItem specifies which item is checked. If -1 no items are checked.
595      * @param labelColumn The column name on the cursor containing the string to display in the
596      *                    label.
597      * @param listener    notified when an item on the list is clicked. The dialog will not be
598      *                    dismissed when an item is clicked. It will only be dismissed if clicked on
599      *                    a
600      *                    button, if no buttons are supplied it's up to the user to dismiss the
601      *                    dialog.
602      * @return This Builder object to allow for chaining of calls to set methods
603      */
setSingleChoiceItems(Cursor cursor, int checkedItem, String labelColumn, final DialogInterface.OnClickListener listener)604     public AlertDialogBuilder setSingleChoiceItems(Cursor cursor, int checkedItem,
605             String labelColumn,
606             final DialogInterface.OnClickListener listener) {
607         mBuilder.setSingleChoiceItems(cursor, checkedItem, labelColumn, listener);
608         mHasSingleChoiceBodyButton = true;
609         return this;
610     }
611 
612     /**
613      * Set a list of items to be displayed in the dialog as the content, you will be notified of
614      * the selected item via the supplied listener. The list will have a check mark displayed to
615      * the right of the text for the checked item. Clicking on an item in the list will not
616      * dismiss the dialog. Clicking on a button will dismiss the dialog.
617      *
618      * @param items       the items to be displayed.
619      * @param checkedItem specifies which item is checked. If -1 no items are checked.
620      * @param listener    notified when an item on the list is clicked. The dialog will not be
621      *                    dismissed when an item is clicked. It will only be dismissed if clicked on
622      *                    a
623      *                    button, if no buttons are supplied it's up to the user to dismiss the
624      *                    dialog.
625      * @return This Builder object to allow for chaining of calls to set methods
626      */
setSingleChoiceItems(CharSequence[] items, int checkedItem, final DialogInterface.OnClickListener listener)627     public AlertDialogBuilder setSingleChoiceItems(CharSequence[] items, int checkedItem,
628             final DialogInterface.OnClickListener listener) {
629         mBuilder.setSingleChoiceItems(items, checkedItem, listener);
630         mHasSingleChoiceBodyButton = true;
631         return this;
632     }
633 
634     /**
635      * This was not supposed to be in the Chassis API because it allows custom views.
636      *
637      * @deprecated Use {@link #setSingleChoiceItems(CarUiRadioButtonListItemAdapter,
638      * DialogInterface.OnClickListener)} instead.
639      */
640     @Deprecated
setSingleChoiceItems(ListAdapter adapter, int checkedItem, final DialogInterface.OnClickListener listener)641     public AlertDialogBuilder setSingleChoiceItems(ListAdapter adapter, int checkedItem,
642             final DialogInterface.OnClickListener listener) {
643         mBuilder.setSingleChoiceItems(adapter, checkedItem, listener);
644         mHasSingleChoiceBodyButton = true;
645         return this;
646     }
647 
648     /**
649      * Set a list of items to be displayed in the dialog as the content, you will be notified of
650      * the selected item via the supplied listener. The list will have a check mark displayed to
651      * the right of the text for the checked item. Clicking on an item in the list will not
652      * dismiss the dialog. Clicking on a button will dismiss the dialog.
653      *
654      * @param adapter  The {@link CarUiRadioButtonListItemAdapter} to supply the list of items
655      * @param listener notified when an item on the list is clicked. The dialog will not be
656      *                 dismissed when an item is clicked. It will only be dismissed if clicked on a
657      *                 button, if no buttons are supplied it's up to the user to dismiss the dialog.
658      * @return This Builder object to allow for chaining of calls to set methods
659      * @deprecated Use {@link #setSingleChoiceItems(CarUiRadioButtonListItemAdapter)} instead.
660      */
661     @Deprecated
setSingleChoiceItems(CarUiRadioButtonListItemAdapter adapter, final DialogInterface.OnClickListener listener)662     public AlertDialogBuilder setSingleChoiceItems(CarUiRadioButtonListItemAdapter adapter,
663             final DialogInterface.OnClickListener listener) {
664         setCustomList(adapter);
665         mHasSingleChoiceBodyButton = false;
666         return this;
667     }
668 
669     /**
670      * Set a list of items to be displayed in the dialog as the content,The list will have a check
671      * mark displayed to the right of the text for the checked item. Clicking on an item in the list
672      * will not dismiss the dialog. Clicking on a button will dismiss the dialog.
673      *
674      * @param adapter The {@link CarUiRadioButtonListItemAdapter} to supply the list of items
675      *                dismissed when an item is clicked. It will only be dismissed if clicked on a
676      *                button, if no buttons are supplied it's up to the user to dismiss the dialog.
677      * @return This Builder object to allow for chaining of calls to set methods
678      */
setSingleChoiceItems(CarUiRadioButtonListItemAdapter adapter)679     public AlertDialogBuilder setSingleChoiceItems(CarUiRadioButtonListItemAdapter adapter) {
680         setCustomList(adapter);
681         mHasSingleChoiceBodyButton = false;
682         return this;
683     }
684 
685     /**
686      * Sets a listener to be invoked when an item in the list is selected.
687      *
688      * @param listener the listener to be invoked
689      * @return this Builder object to allow for chaining of calls to set methods
690      * @see AdapterView#setOnItemSelectedListener(android.widget.AdapterView.OnItemSelectedListener)
691      */
setOnItemSelectedListener( final AdapterView.OnItemSelectedListener listener)692     public AlertDialogBuilder setOnItemSelectedListener(
693             final AdapterView.OnItemSelectedListener listener) {
694         mBuilder.setOnItemSelectedListener(listener);
695         mHasSingleChoiceBodyButton = true;
696         return this;
697     }
698 
699     /**
700      * Sets a custom edit text box within the alert dialog.
701      *
702      * @param prompt              the string that will be set on the edit text view
703      * @param textChangedListener textWatcher whose methods are called whenever this TextView's text
704      *                            changes {@code null} otherwise.
705      * @param inputFilters        list of input filters, {@code null} if no filter is needed
706      * @param inputType           See {@link EditText#setInputType(int)}, except
707      *                            {@link android.text.InputType#TYPE_NULL} will not be set.
708      * @return this Builder object to allow for chaining of calls to set methods
709      */
setEditBox(String prompt, TextWatcher textChangedListener, InputFilter[] inputFilters, int inputType)710     public AlertDialogBuilder setEditBox(String prompt, TextWatcher textChangedListener,
711             InputFilter[] inputFilters, int inputType) {
712         View contentView = LayoutInflater.from(mContext).inflate(
713                 R.layout.car_ui_alert_dialog_edit_text, null);
714 
715         mCarUiEditText = CarUiUtils.requireViewByRefId(contentView, R.id.textbox);
716         mCarUiEditText.setText(prompt);
717 
718         if (textChangedListener != null) {
719             mCarUiEditText.addTextChangedListener(textChangedListener);
720         }
721 
722         if (inputFilters != null) {
723             mCarUiEditText.setFilters(inputFilters);
724         }
725 
726         if (inputType != 0) {
727             mCarUiEditText.setInputType(inputType);
728         }
729 
730         mBuilder.setView(contentView);
731         return this;
732     }
733 
734     /**
735      * Sets a custom edit text box within the alert dialog.
736      *
737      * @param prompt              the string that will be set on the edit text view
738      * @param textChangedListener textWatcher whose methods are called whenever this TextView's text
739      *                            changes {@code null} otherwise.
740      * @param inputFilters        list of input filters, {@code null} if no filter is needed
741      * @return this Builder object to allow for chaining of calls to set methods
742      */
setEditBox(String prompt, TextWatcher textChangedListener, InputFilter[] inputFilters)743     public AlertDialogBuilder setEditBox(String prompt, TextWatcher textChangedListener,
744             InputFilter[] inputFilters) {
745         return setEditBox(prompt, textChangedListener, inputFilters, 0);
746     }
747 
748     /**
749      * Sets the title and desc related to the dialog within the IMS templates.
750      *
751      * @param title title to be set.
752      * @param desc  description related to the dialog.
753      * @return this Builder object to allow for chaining of calls to set methods
754      */
setEditTextTitleAndDescForWideScreen(String title, String desc)755     public AlertDialogBuilder setEditTextTitleAndDescForWideScreen(String title, String desc) {
756         mWideScreenTitle = title;
757         mWideScreenTitleDesc = desc;
758 
759         return this;
760     }
761 
762     /**
763      * By default, the AlertDialogBuilder will just display the static text in the content area of
764      * widescreen IME provided by {@link #setEditTextTitleAndDescForWideScreen(String, String)}. To
765      * display the text typed by the user in the description set this to true.
766      *
767      * @return this Builder object to allow for chaining of calls to set methods
768      */
setAutoDescUpdateForWidescreen(boolean autoUpdateDesc)769     public AlertDialogBuilder setAutoDescUpdateForWidescreen(boolean autoUpdateDesc) {
770         if (autoUpdateDesc) {
771             mCarUiEditText.addTextChangedListener(mTextWatcherWideScreen);
772         } else {
773             mCarUiEditText.removeTextChangedListener(mTextWatcherWideScreen);
774         }
775         return this;
776     }
777 
778     /**
779      * By default, the AlertDialogBuilder may add a "Dismiss" button if you don't provide
780      * a positive/negative/neutral button. This is so that the dialog is still dismissible
781      * using the rotary controller. If however, you add buttons that can close the dialog via
782      * {@link #setAdapter(CarUiListItemAdapter)} or a similar method, then you may wish to
783      * suppress the addition of the dismiss button, which this method allows for.
784      *
785      * @param allowDismissButton If true, a "Dismiss" button may be added to the dialog.
786      *                           If false, it will never be added.
787      * @return this Builder object to allow for chaining of calls to set methods
788      */
setAllowDismissButton(boolean allowDismissButton)789     public AlertDialogBuilder setAllowDismissButton(boolean allowDismissButton) {
790         mAllowDismissButton = allowDismissButton;
791         return this;
792     }
793 
794     /** Final steps common to both {@link #create()} and {@link #show()} */
prepareDialog()795     private void prepareDialog() {
796         View customTitle = LayoutInflater.from(mContext).inflate(
797                 R.layout.car_ui_alert_dialog_title_with_subtitle, null);
798 
799         TextView mTitleView = CarUiUtils.requireViewByRefId(customTitle, R.id.car_ui_alert_title);
800         TextView mSubtitleView =
801                 CarUiUtils.requireViewByRefId(customTitle, R.id.car_ui_alert_subtitle);
802         mSubtitleView.setMovementMethod(LinkMovementMethod.getInstance());
803         ImageView mIconView = CarUiUtils.requireViewByRefId(customTitle, R.id.car_ui_alert_icon);
804 
805         mTitleView.setText(mTitle);
806         mTitleView.setVisibility(TextUtils.isEmpty(mTitle) ? View.GONE : View.VISIBLE);
807         mSubtitleView.setText(mSubtitle);
808         mSubtitleView.setVisibility(TextUtils.isEmpty(mSubtitle) ? View.GONE : View.VISIBLE);
809         mIconView.setImageDrawable(mIcon);
810         mIconView.setVisibility(mIcon != null ? View.VISIBLE : View.GONE);
811         if (mIconTinted) {
812             mIconView.setImageTintList(
813                     mContext.getColorStateList(R.color.car_ui_dialog_icon_color));
814         }
815         // Do not set custom title if not required to maintain extra padding for messages with no
816         // title logic
817         if (!TextUtils.isEmpty(mTitle) || !TextUtils.isEmpty(mSubtitle) || mIcon != null) {
818             mBuilder.setCustomTitle(customTitle);
819         }
820 
821         if (!mAllowDismissButton && !mHasSingleChoiceBodyButton
822                 && !mNeutralButtonSet && !mNegativeButtonSet && !mPositiveButtonSet) {
823             throw new RuntimeException(
824                     "The dialog must have at least one button to disable the dismiss button");
825         }
826         if (mContext.getResources().getBoolean(R.bool.car_ui_alert_dialog_force_dismiss_button)
827                 && !mNeutralButtonSet && !mNegativeButtonSet && !mPositiveButtonSet
828                 && mAllowDismissButton) {
829             String mDefaultButtonText = mContext.getString(
830                     R.string.car_ui_alert_dialog_default_button);
831             mBuilder.setNegativeButton(mDefaultButtonText, (dialog, which) -> {
832             });
833         }
834     }
835 
836     /**
837      * Creates an {@link AlertDialog} with the arguments supplied to this
838      * builder.
839      * <p>
840      * Calling this method does not display the dialog. If no additional
841      * processing is needed, {@link #show()} may be called instead to both
842      * create and display the dialog.
843      */
create()844     public AlertDialog create() {
845         prepareDialog();
846         AlertDialog alertDialog = mBuilder.create();
847 
848         // Put a FocusParkingView at the end of dialog window to prevent rotary controller
849         // wrap-around. Android will focus on the first view automatically when the dialog is shown,
850         // and we want it to focus on the title instead of the FocusParkingView, so we put the
851         // FocusParkingView at the end of dialog window.
852         mRoot = (ViewGroup) alertDialog.getWindow().getDecorView().getRootView();
853         FocusParkingView fpv = new FocusParkingView(mContext);
854         mRoot.addView(fpv);
855 
856         // apply window insets listener to know when IME is visible so we can set title and desc.
857         mRoot.setOnApplyWindowInsetsListener(mOnApplyWindowInsetsListener);
858         setOnDismissListener(mOnDismissListener);
859 
860         return alertDialog;
861     }
862 
863     /**
864      * Creates an {@link AlertDialog} with the arguments supplied to this
865      * builder and immediately displays the dialog.
866      */
show()867     public AlertDialog show() {
868         AlertDialog alertDialog = create();
869         alertDialog.show();
870         return alertDialog;
871     }
872 }
873