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