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.TypedArray; 28 import android.database.Cursor; 29 import android.graphics.drawable.Drawable; 30 import android.os.Handler; 31 import android.os.Message; 32 import android.text.TextUtils; 33 import android.util.AttributeSet; 34 import android.util.TypedValue; 35 import android.view.Gravity; 36 import android.view.KeyEvent; 37 import android.view.LayoutInflater; 38 import android.view.View; 39 import android.view.ViewGroup; 40 import android.view.ViewGroup.LayoutParams; 41 import android.view.ViewParent; 42 import android.view.ViewStub; 43 import android.view.Window; 44 import android.view.WindowInsets; 45 import android.view.WindowManager; 46 import android.widget.AbsListView; 47 import android.widget.AdapterView; 48 import android.widget.AdapterView.OnItemClickListener; 49 import android.widget.ArrayAdapter; 50 import android.widget.Button; 51 import android.widget.CheckedTextView; 52 import android.widget.CursorAdapter; 53 import android.widget.FrameLayout; 54 import android.widget.ImageView; 55 import android.widget.LinearLayout; 56 import android.widget.ListAdapter; 57 import android.widget.ListView; 58 import android.widget.ScrollView; 59 import android.widget.SimpleCursorAdapter; 60 import android.widget.TextView; 61 62 import java.lang.ref.WeakReference; 63 64 public class AlertController { 65 66 private final Context mContext; 67 private final DialogInterface mDialogInterface; 68 private final Window mWindow; 69 70 private CharSequence mTitle; 71 private CharSequence mMessage; 72 private ListView mListView; 73 private View mView; 74 75 private int mViewLayoutResId; 76 77 private int mViewSpacingLeft; 78 private int mViewSpacingTop; 79 private int mViewSpacingRight; 80 private int mViewSpacingBottom; 81 private boolean mViewSpacingSpecified = false; 82 83 private Button mButtonPositive; 84 private CharSequence mButtonPositiveText; 85 private Message mButtonPositiveMessage; 86 87 private Button mButtonNegative; 88 private CharSequence mButtonNegativeText; 89 private Message mButtonNegativeMessage; 90 91 private Button mButtonNeutral; 92 private CharSequence mButtonNeutralText; 93 private Message mButtonNeutralMessage; 94 95 private ScrollView mScrollView; 96 97 private int mIconId = 0; 98 private Drawable mIcon; 99 100 private ImageView mIconView; 101 private TextView mTitleView; 102 private TextView mMessageView; 103 private View mCustomTitleView; 104 105 private boolean mForceInverseBackground; 106 107 private ListAdapter mAdapter; 108 109 private int mCheckedItem = -1; 110 111 private int mAlertDialogLayout; 112 private int mButtonPanelSideLayout; 113 private int mListLayout; 114 private int mMultiChoiceItemLayout; 115 private int mSingleChoiceItemLayout; 116 private int mListItemLayout; 117 118 private int mButtonPanelLayoutHint = AlertDialog.LAYOUT_HINT_NONE; 119 120 private Handler mHandler; 121 122 private final View.OnClickListener mButtonHandler = new View.OnClickListener() { 123 @Override 124 public void onClick(View v) { 125 final Message m; 126 if (v == mButtonPositive && mButtonPositiveMessage != null) { 127 m = Message.obtain(mButtonPositiveMessage); 128 } else if (v == mButtonNegative && mButtonNegativeMessage != null) { 129 m = Message.obtain(mButtonNegativeMessage); 130 } else if (v == mButtonNeutral && mButtonNeutralMessage != null) { 131 m = Message.obtain(mButtonNeutralMessage); 132 } else { 133 m = null; 134 } 135 136 if (m != null) { 137 m.sendToTarget(); 138 } 139 140 // Post a message so we dismiss after the above handlers are executed 141 mHandler.obtainMessage(ButtonHandler.MSG_DISMISS_DIALOG, mDialogInterface) 142 .sendToTarget(); 143 } 144 }; 145 146 private static final class ButtonHandler extends Handler { 147 // Button clicks have Message.what as the BUTTON{1,2,3} constant 148 private static final int MSG_DISMISS_DIALOG = 1; 149 150 private WeakReference<DialogInterface> mDialog; 151 152 public ButtonHandler(DialogInterface dialog) { 153 mDialog = new WeakReference<DialogInterface>(dialog); 154 } 155 156 @Override 157 public void handleMessage(Message msg) { 158 switch (msg.what) { 159 160 case DialogInterface.BUTTON_POSITIVE: 161 case DialogInterface.BUTTON_NEGATIVE: 162 case DialogInterface.BUTTON_NEUTRAL: 163 ((DialogInterface.OnClickListener) msg.obj).onClick(mDialog.get(), msg.what); 164 break; 165 166 case MSG_DISMISS_DIALOG: 167 ((DialogInterface) msg.obj).dismiss(); 168 } 169 } 170 } 171 172 private static boolean shouldCenterSingleButton(Context context) { 173 final TypedValue outValue = new TypedValue(); 174 context.getTheme().resolveAttribute(R.attr.alertDialogCenterButtons, outValue, true); 175 return outValue.data != 0; 176 } 177 178 public AlertController(Context context, DialogInterface di, Window window) { 179 mContext = context; 180 mDialogInterface = di; 181 mWindow = window; 182 mHandler = new ButtonHandler(di); 183 184 final TypedArray a = context.obtainStyledAttributes(null, 185 R.styleable.AlertDialog, R.attr.alertDialogStyle, 0); 186 187 mAlertDialogLayout = a.getResourceId( 188 R.styleable.AlertDialog_layout, R.layout.alert_dialog); 189 mButtonPanelSideLayout = a.getResourceId( 190 R.styleable.AlertDialog_buttonPanelSideLayout, 0); 191 mListLayout = a.getResourceId( 192 R.styleable.AlertDialog_listLayout, R.layout.select_dialog); 193 194 mMultiChoiceItemLayout = a.getResourceId( 195 R.styleable.AlertDialog_multiChoiceItemLayout, 196 R.layout.select_dialog_multichoice); 197 mSingleChoiceItemLayout = a.getResourceId( 198 R.styleable.AlertDialog_singleChoiceItemLayout, 199 R.layout.select_dialog_singlechoice); 200 mListItemLayout = a.getResourceId( 201 R.styleable.AlertDialog_listItemLayout, 202 R.layout.select_dialog_item); 203 204 a.recycle(); 205 } 206 207 static boolean canTextInput(View v) { 208 if (v.onCheckIsTextEditor()) { 209 return true; 210 } 211 212 if (!(v instanceof ViewGroup)) { 213 return false; 214 } 215 216 ViewGroup vg = (ViewGroup)v; 217 int i = vg.getChildCount(); 218 while (i > 0) { 219 i--; 220 v = vg.getChildAt(i); 221 if (canTextInput(v)) { 222 return true; 223 } 224 } 225 226 return false; 227 } 228 229 public void installContent() { 230 /* We use a custom title so never request a window title */ 231 mWindow.requestFeature(Window.FEATURE_NO_TITLE); 232 int contentView = selectContentView(); 233 mWindow.setContentView(contentView); 234 setupView(); 235 setupDecor(); 236 } 237 238 private int selectContentView() { 239 if (mButtonPanelSideLayout == 0) { 240 return mAlertDialogLayout; 241 } 242 if (mButtonPanelLayoutHint == AlertDialog.LAYOUT_HINT_SIDE) { 243 return mButtonPanelSideLayout; 244 } 245 // TODO: use layout hint side for long messages/lists 246 return mAlertDialogLayout; 247 } 248 249 public void setTitle(CharSequence title) { 250 mTitle = title; 251 if (mTitleView != null) { 252 mTitleView.setText(title); 253 } 254 } 255 256 /** 257 * @see AlertDialog.Builder#setCustomTitle(View) 258 */ 259 public void setCustomTitle(View customTitleView) { 260 mCustomTitleView = customTitleView; 261 } 262 263 public void setMessage(CharSequence message) { 264 mMessage = message; 265 if (mMessageView != null) { 266 mMessageView.setText(message); 267 } 268 } 269 270 /** 271 * Set the view resource to display in the dialog. 272 */ 273 public void setView(int layoutResId) { 274 mView = null; 275 mViewLayoutResId = layoutResId; 276 mViewSpacingSpecified = false; 277 } 278 279 /** 280 * Set the view to display in the dialog. 281 */ 282 public void setView(View view) { 283 mView = view; 284 mViewLayoutResId = 0; 285 mViewSpacingSpecified = false; 286 } 287 288 /** 289 * Set the view to display in the dialog along with the spacing around that view 290 */ 291 public void setView(View view, int viewSpacingLeft, int viewSpacingTop, int viewSpacingRight, 292 int viewSpacingBottom) { 293 mView = view; 294 mViewLayoutResId = 0; 295 mViewSpacingSpecified = true; 296 mViewSpacingLeft = viewSpacingLeft; 297 mViewSpacingTop = viewSpacingTop; 298 mViewSpacingRight = viewSpacingRight; 299 mViewSpacingBottom = viewSpacingBottom; 300 } 301 302 /** 303 * Sets a hint for the best button panel layout. 304 */ 305 public void setButtonPanelLayoutHint(int layoutHint) { 306 mButtonPanelLayoutHint = layoutHint; 307 } 308 309 /** 310 * Sets a click listener or a message to be sent when the button is clicked. 311 * You only need to pass one of {@code listener} or {@code msg}. 312 * 313 * @param whichButton Which button, can be one of 314 * {@link DialogInterface#BUTTON_POSITIVE}, 315 * {@link DialogInterface#BUTTON_NEGATIVE}, or 316 * {@link DialogInterface#BUTTON_NEUTRAL} 317 * @param text The text to display in positive button. 318 * @param listener The {@link DialogInterface.OnClickListener} to use. 319 * @param msg The {@link Message} to be sent when clicked. 320 */ 321 public void setButton(int whichButton, CharSequence text, 322 DialogInterface.OnClickListener listener, Message msg) { 323 324 if (msg == null && listener != null) { 325 msg = mHandler.obtainMessage(whichButton, listener); 326 } 327 328 switch (whichButton) { 329 330 case DialogInterface.BUTTON_POSITIVE: 331 mButtonPositiveText = text; 332 mButtonPositiveMessage = msg; 333 break; 334 335 case DialogInterface.BUTTON_NEGATIVE: 336 mButtonNegativeText = text; 337 mButtonNegativeMessage = msg; 338 break; 339 340 case DialogInterface.BUTTON_NEUTRAL: 341 mButtonNeutralText = text; 342 mButtonNeutralMessage = msg; 343 break; 344 345 default: 346 throw new IllegalArgumentException("Button does not exist"); 347 } 348 } 349 350 /** 351 * Specifies the icon to display next to the alert title. 352 * 353 * @param resId the resource identifier of the drawable to use as the icon, 354 * or 0 for no icon 355 */ 356 public void setIcon(int resId) { 357 mIcon = null; 358 mIconId = resId; 359 360 if (mIconView != null) { 361 if (resId != 0) { 362 mIconView.setImageResource(mIconId); 363 } else { 364 mIconView.setVisibility(View.GONE); 365 } 366 } 367 } 368 369 /** 370 * Specifies the icon to display next to the alert title. 371 * 372 * @param icon the drawable to use as the icon or null for no icon 373 */ 374 public void setIcon(Drawable icon) { 375 mIcon = icon; 376 mIconId = 0; 377 378 if (mIconView != null) { 379 if (icon != null) { 380 mIconView.setImageDrawable(icon); 381 } else { 382 mIconView.setVisibility(View.GONE); 383 } 384 } 385 } 386 387 /** 388 * @param attrId the attributeId of the theme-specific drawable 389 * to resolve the resourceId for. 390 * 391 * @return resId the resourceId of the theme-specific drawable 392 */ 393 public int getIconAttributeResId(int attrId) { 394 TypedValue out = new TypedValue(); 395 mContext.getTheme().resolveAttribute(attrId, out, true); 396 return out.resourceId; 397 } 398 399 public void setInverseBackgroundForced(boolean forceInverseBackground) { 400 mForceInverseBackground = forceInverseBackground; 401 } 402 403 public ListView getListView() { 404 return mListView; 405 } 406 407 public Button getButton(int whichButton) { 408 switch (whichButton) { 409 case DialogInterface.BUTTON_POSITIVE: 410 return mButtonPositive; 411 case DialogInterface.BUTTON_NEGATIVE: 412 return mButtonNegative; 413 case DialogInterface.BUTTON_NEUTRAL: 414 return mButtonNeutral; 415 default: 416 return null; 417 } 418 } 419 420 @SuppressWarnings({"UnusedDeclaration"}) 421 public boolean onKeyDown(int keyCode, KeyEvent event) { 422 return mScrollView != null && mScrollView.executeKeyEvent(event); 423 } 424 425 @SuppressWarnings({"UnusedDeclaration"}) 426 public boolean onKeyUp(int keyCode, KeyEvent event) { 427 return mScrollView != null && mScrollView.executeKeyEvent(event); 428 } 429 430 private void setupDecor() { 431 final View decor = mWindow.getDecorView(); 432 final View parent = mWindow.findViewById(R.id.parentPanel); 433 if (parent != null && decor != null) { 434 decor.setOnApplyWindowInsetsListener(new View.OnApplyWindowInsetsListener() { 435 @Override 436 public WindowInsets onApplyWindowInsets(View view, WindowInsets insets) { 437 if (insets.isRound()) { 438 // TODO: Get the padding as a function of the window size. 439 int roundOffset = mContext.getResources().getDimensionPixelOffset( 440 R.dimen.alert_dialog_round_padding); 441 parent.setPadding(roundOffset, roundOffset, roundOffset, roundOffset); 442 } 443 return insets.consumeSystemWindowInsets(); 444 } 445 }); 446 decor.setFitsSystemWindows(true); 447 decor.requestApplyInsets(); 448 } 449 } 450 451 /** 452 * Resolves whether a custom or default panel should be used. Removes the 453 * default panel if a custom panel should be used. If the resolved panel is 454 * a view stub, inflates before returning. 455 * 456 * @param customPanel the custom panel 457 * @param defaultPanel the default panel 458 * @return the panel to use 459 */ 460 @Nullable 461 private ViewGroup resolvePanel(@Nullable View customPanel, @Nullable View defaultPanel) { 462 if (customPanel == null) { 463 // Inflate the default panel, if needed. 464 if (defaultPanel instanceof ViewStub) { 465 defaultPanel = ((ViewStub) defaultPanel).inflate(); 466 } 467 468 return (ViewGroup) defaultPanel; 469 } 470 471 // Remove the default panel entirely. 472 if (defaultPanel != null) { 473 final ViewParent parent = defaultPanel.getParent(); 474 if (parent instanceof ViewGroup) { 475 ((ViewGroup) parent).removeView(defaultPanel); 476 } 477 } 478 479 // Inflate the custom panel, if needed. 480 if (customPanel instanceof ViewStub) { 481 customPanel = ((ViewStub) customPanel).inflate(); 482 } 483 484 return (ViewGroup) customPanel; 485 } 486 487 private void setupView() { 488 final View parentPanel = mWindow.findViewById(R.id.parentPanel); 489 final View defaultTopPanel = parentPanel.findViewById(R.id.topPanel); 490 final View defaultContentPanel = parentPanel.findViewById(R.id.contentPanel); 491 final View defaultButtonPanel = parentPanel.findViewById(R.id.buttonPanel); 492 493 // Install custom content before setting up the title or buttons so 494 // that we can handle panel overrides. 495 final ViewGroup customPanel = (ViewGroup) parentPanel.findViewById(R.id.customPanel); 496 setupCustomContent(customPanel); 497 498 final View customTopPanel = customPanel.findViewById(R.id.topPanel); 499 final View customContentPanel = customPanel.findViewById(R.id.contentPanel); 500 final View customButtonPanel = customPanel.findViewById(R.id.buttonPanel); 501 502 // Resolve the correct panels and remove the defaults, if needed. 503 final ViewGroup topPanel = resolvePanel(customTopPanel, defaultTopPanel); 504 final ViewGroup contentPanel = resolvePanel(customContentPanel, defaultContentPanel); 505 final ViewGroup buttonPanel = resolvePanel(customButtonPanel, defaultButtonPanel); 506 507 setupContent(contentPanel); 508 setupButtons(buttonPanel); 509 setupTitle(topPanel); 510 511 final boolean hasCustomPanel = customPanel != null 512 && customPanel.getVisibility() != View.GONE; 513 final boolean hasTopPanel = topPanel != null 514 && topPanel.getVisibility() != View.GONE; 515 final boolean hasButtonPanel = buttonPanel != null 516 && buttonPanel.getVisibility() != View.GONE; 517 518 // Only display the text spacer if we don't have buttons. 519 if (!hasButtonPanel) { 520 if (contentPanel != null) { 521 final View spacer = contentPanel.findViewById(R.id.textSpacerNoButtons); 522 if (spacer != null) { 523 spacer.setVisibility(View.VISIBLE); 524 } 525 } 526 mWindow.setCloseOnTouchOutsideIfNotSet(true); 527 } 528 529 if (hasTopPanel) { 530 // Only clip scrolling content to padding if we have a title. 531 if (mScrollView != null) { 532 mScrollView.setClipToPadding(true); 533 } 534 535 // Only show the divider if we have a title. 536 final View divider; 537 if (mMessage != null || mListView != null || hasCustomPanel) { 538 divider = topPanel.findViewById(R.id.titleDivider); 539 } else { 540 divider = topPanel.findViewById(R.id.titleDividerTop); 541 } 542 543 if (divider != null) { 544 divider.setVisibility(View.VISIBLE); 545 } 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) { 602 // Add the custom title view directly to the topPanel layout 603 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 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) { 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 boolean mRecycleOnMeasure = true; 882 883 public RecycleListView(Context context) { 884 super(context); 885 } 886 887 public RecycleListView(Context context, AttributeSet attrs) { 888 super(context, attrs); 889 } 890 891 public RecycleListView(Context context, AttributeSet attrs, int defStyleAttr) { 892 super(context, attrs, defStyleAttr); 893 } 894 895 public RecycleListView( 896 Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 897 super(context, attrs, defStyleAttr, defStyleRes); 898 } 899 900 @Override 901 protected boolean recycleOnMeasure() { 902 return mRecycleOnMeasure; 903 } 904 } 905 906 public static class AlertParams { 907 public final Context mContext; 908 public final LayoutInflater mInflater; 909 910 public int mIconId = 0; 911 public Drawable mIcon; 912 public int mIconAttrId = 0; 913 public CharSequence mTitle; 914 public View mCustomTitleView; 915 public CharSequence mMessage; 916 public CharSequence mPositiveButtonText; 917 public DialogInterface.OnClickListener mPositiveButtonListener; 918 public CharSequence mNegativeButtonText; 919 public DialogInterface.OnClickListener mNegativeButtonListener; 920 public CharSequence mNeutralButtonText; 921 public DialogInterface.OnClickListener mNeutralButtonListener; 922 public boolean mCancelable; 923 public DialogInterface.OnCancelListener mOnCancelListener; 924 public DialogInterface.OnDismissListener mOnDismissListener; 925 public DialogInterface.OnKeyListener mOnKeyListener; 926 public CharSequence[] mItems; 927 public ListAdapter mAdapter; 928 public DialogInterface.OnClickListener mOnClickListener; 929 public int mViewLayoutResId; 930 public View mView; 931 public int mViewSpacingLeft; 932 public int mViewSpacingTop; 933 public int mViewSpacingRight; 934 public int mViewSpacingBottom; 935 public boolean mViewSpacingSpecified = false; 936 public boolean[] mCheckedItems; 937 public boolean mIsMultiChoice; 938 public boolean mIsSingleChoice; 939 public int mCheckedItem = -1; 940 public DialogInterface.OnMultiChoiceClickListener mOnCheckboxClickListener; 941 public Cursor mCursor; 942 public String mLabelColumn; 943 public String mIsCheckedColumn; 944 public boolean mForceInverseBackground; 945 public AdapterView.OnItemSelectedListener mOnItemSelectedListener; 946 public OnPrepareListViewListener mOnPrepareListViewListener; 947 public boolean mRecycleOnMeasure = true; 948 949 /** 950 * Interface definition for a callback to be invoked before the ListView 951 * will be bound to an adapter. 952 */ 953 public interface OnPrepareListViewListener { 954 955 /** 956 * Called before the ListView is bound to an adapter. 957 * @param listView The ListView that will be shown in the dialog. 958 */ 959 void onPrepareListView(ListView listView); 960 } 961 962 public AlertParams(Context context) { 963 mContext = context; 964 mCancelable = true; 965 mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 966 } 967 968 public void apply(AlertController dialog) { 969 if (mCustomTitleView != null) { 970 dialog.setCustomTitle(mCustomTitleView); 971 } else { 972 if (mTitle != null) { 973 dialog.setTitle(mTitle); 974 } 975 if (mIcon != null) { 976 dialog.setIcon(mIcon); 977 } 978 if (mIconId != 0) { 979 dialog.setIcon(mIconId); 980 } 981 if (mIconAttrId != 0) { 982 dialog.setIcon(dialog.getIconAttributeResId(mIconAttrId)); 983 } 984 } 985 if (mMessage != null) { 986 dialog.setMessage(mMessage); 987 } 988 if (mPositiveButtonText != null) { 989 dialog.setButton(DialogInterface.BUTTON_POSITIVE, mPositiveButtonText, 990 mPositiveButtonListener, null); 991 } 992 if (mNegativeButtonText != null) { 993 dialog.setButton(DialogInterface.BUTTON_NEGATIVE, mNegativeButtonText, 994 mNegativeButtonListener, null); 995 } 996 if (mNeutralButtonText != null) { 997 dialog.setButton(DialogInterface.BUTTON_NEUTRAL, mNeutralButtonText, 998 mNeutralButtonListener, null); 999 } 1000 if (mForceInverseBackground) { 1001 dialog.setInverseBackgroundForced(true); 1002 } 1003 // For a list, the client can either supply an array of items or an 1004 // adapter or a cursor 1005 if ((mItems != null) || (mCursor != null) || (mAdapter != null)) { 1006 createListView(dialog); 1007 } 1008 if (mView != null) { 1009 if (mViewSpacingSpecified) { 1010 dialog.setView(mView, mViewSpacingLeft, mViewSpacingTop, mViewSpacingRight, 1011 mViewSpacingBottom); 1012 } else { 1013 dialog.setView(mView); 1014 } 1015 } else if (mViewLayoutResId != 0) { 1016 dialog.setView(mViewLayoutResId); 1017 } 1018 1019 /* 1020 dialog.setCancelable(mCancelable); 1021 dialog.setOnCancelListener(mOnCancelListener); 1022 if (mOnKeyListener != null) { 1023 dialog.setOnKeyListener(mOnKeyListener); 1024 } 1025 */ 1026 } 1027 1028 private void createListView(final AlertController dialog) { 1029 final RecycleListView listView = 1030 (RecycleListView) mInflater.inflate(dialog.mListLayout, null); 1031 final ListAdapter adapter; 1032 1033 if (mIsMultiChoice) { 1034 if (mCursor == null) { 1035 adapter = new ArrayAdapter<CharSequence>( 1036 mContext, dialog.mMultiChoiceItemLayout, R.id.text1, mItems) { 1037 @Override 1038 public View getView(int position, View convertView, ViewGroup parent) { 1039 View view = super.getView(position, convertView, parent); 1040 if (mCheckedItems != null) { 1041 boolean isItemChecked = mCheckedItems[position]; 1042 if (isItemChecked) { 1043 listView.setItemChecked(position, true); 1044 } 1045 } 1046 return view; 1047 } 1048 }; 1049 } else { 1050 adapter = new CursorAdapter(mContext, mCursor, false) { 1051 private final int mLabelIndex; 1052 private final int mIsCheckedIndex; 1053 1054 { 1055 final Cursor cursor = getCursor(); 1056 mLabelIndex = cursor.getColumnIndexOrThrow(mLabelColumn); 1057 mIsCheckedIndex = cursor.getColumnIndexOrThrow(mIsCheckedColumn); 1058 } 1059 1060 @Override 1061 public void bindView(View view, Context context, Cursor cursor) { 1062 CheckedTextView text = (CheckedTextView) view.findViewById(R.id.text1); 1063 text.setText(cursor.getString(mLabelIndex)); 1064 listView.setItemChecked(cursor.getPosition(), 1065 cursor.getInt(mIsCheckedIndex) == 1); 1066 } 1067 1068 @Override 1069 public View newView(Context context, Cursor cursor, ViewGroup parent) { 1070 return mInflater.inflate(dialog.mMultiChoiceItemLayout, 1071 parent, false); 1072 } 1073 1074 }; 1075 } 1076 } else { 1077 final int layout; 1078 if (mIsSingleChoice) { 1079 layout = dialog.mSingleChoiceItemLayout; 1080 } else { 1081 layout = dialog.mListItemLayout; 1082 } 1083 1084 if (mCursor != null) { 1085 adapter = new SimpleCursorAdapter(mContext, layout, mCursor, 1086 new String[] { mLabelColumn }, new int[] { R.id.text1 }); 1087 } else if (mAdapter != null) { 1088 adapter = mAdapter; 1089 } else { 1090 adapter = new CheckedItemAdapter(mContext, layout, R.id.text1, mItems); 1091 } 1092 } 1093 1094 if (mOnPrepareListViewListener != null) { 1095 mOnPrepareListViewListener.onPrepareListView(listView); 1096 } 1097 1098 /* Don't directly set the adapter on the ListView as we might 1099 * want to add a footer to the ListView later. 1100 */ 1101 dialog.mAdapter = adapter; 1102 dialog.mCheckedItem = mCheckedItem; 1103 1104 if (mOnClickListener != null) { 1105 listView.setOnItemClickListener(new OnItemClickListener() { 1106 @Override 1107 public void onItemClick(AdapterView<?> parent, View v, int position, long id) { 1108 mOnClickListener.onClick(dialog.mDialogInterface, position); 1109 if (!mIsSingleChoice) { 1110 dialog.mDialogInterface.dismiss(); 1111 } 1112 } 1113 }); 1114 } else if (mOnCheckboxClickListener != null) { 1115 listView.setOnItemClickListener(new OnItemClickListener() { 1116 @Override 1117 public void onItemClick(AdapterView<?> parent, View v, int position, long id) { 1118 if (mCheckedItems != null) { 1119 mCheckedItems[position] = listView.isItemChecked(position); 1120 } 1121 mOnCheckboxClickListener.onClick( 1122 dialog.mDialogInterface, position, listView.isItemChecked(position)); 1123 } 1124 }); 1125 } 1126 1127 // Attach a given OnItemSelectedListener to the ListView 1128 if (mOnItemSelectedListener != null) { 1129 listView.setOnItemSelectedListener(mOnItemSelectedListener); 1130 } 1131 1132 if (mIsSingleChoice) { 1133 listView.setChoiceMode(ListView.CHOICE_MODE_SINGLE); 1134 } else if (mIsMultiChoice) { 1135 listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); 1136 } 1137 listView.mRecycleOnMeasure = mRecycleOnMeasure; 1138 dialog.mListView = listView; 1139 } 1140 } 1141 1142 private static class CheckedItemAdapter extends ArrayAdapter<CharSequence> { 1143 public CheckedItemAdapter(Context context, int resource, int textViewResourceId, 1144 CharSequence[] objects) { 1145 super(context, resource, textViewResourceId, objects); 1146 } 1147 1148 @Override 1149 public boolean hasStableIds() { 1150 return true; 1151 } 1152 1153 @Override 1154 public long getItemId(int position) { 1155 return position; 1156 } 1157 } 1158 } 1159