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