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