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