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.app.AlertDialog; 24 import android.content.Context; 25 import android.content.DialogInterface; 26 import android.content.res.TypedArray; 27 import android.database.Cursor; 28 import android.graphics.drawable.Drawable; 29 import android.os.Handler; 30 import android.os.Message; 31 import android.text.TextUtils; 32 import android.util.AttributeSet; 33 import android.util.TypedValue; 34 import android.view.Gravity; 35 import android.view.KeyEvent; 36 import android.view.LayoutInflater; 37 import android.view.View; 38 import android.view.ViewGroup; 39 import android.view.ViewGroup.LayoutParams; 40 import android.view.ViewParent; 41 import android.view.ViewTreeObserver; 42 import android.view.Window; 43 import android.view.WindowInsets; 44 import android.view.WindowManager; 45 import android.widget.AbsListView; 46 import android.widget.AdapterView; 47 import android.widget.AdapterView.OnItemClickListener; 48 import android.widget.ArrayAdapter; 49 import android.widget.Button; 50 import android.widget.CheckedTextView; 51 import android.widget.CursorAdapter; 52 import android.widget.FrameLayout; 53 import android.widget.ImageView; 54 import android.widget.LinearLayout; 55 import android.widget.ListAdapter; 56 import android.widget.ListView; 57 import android.widget.ScrollView; 58 import android.widget.SimpleCursorAdapter; 59 import android.widget.TextView; 60 61 import java.lang.ref.WeakReference; 62 63 public 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 int mButtonPanelLayoutHint = AlertDialog.LAYOUT_HINT_NONE; 118 119 private Handler mHandler; 120 121 private final View.OnClickListener mButtonHandler = new View.OnClickListener() { 122 @Override 123 public void onClick(View v) { 124 final Message m; 125 if (v == mButtonPositive && mButtonPositiveMessage != null) { 126 m = Message.obtain(mButtonPositiveMessage); 127 } else if (v == mButtonNegative && mButtonNegativeMessage != null) { 128 m = Message.obtain(mButtonNegativeMessage); 129 } else if (v == mButtonNeutral && mButtonNeutralMessage != null) { 130 m = Message.obtain(mButtonNeutralMessage); 131 } else { 132 m = null; 133 } 134 135 if (m != null) { 136 m.sendToTarget(); 137 } 138 139 // Post a message so we dismiss after the above handlers are executed 140 mHandler.obtainMessage(ButtonHandler.MSG_DISMISS_DIALOG, mDialogInterface) 141 .sendToTarget(); 142 } 143 }; 144 145 private static final class ButtonHandler extends Handler { 146 // Button clicks have Message.what as the BUTTON{1,2,3} constant 147 private static final int MSG_DISMISS_DIALOG = 1; 148 149 private WeakReference<DialogInterface> mDialog; 150 151 public ButtonHandler(DialogInterface dialog) { 152 mDialog = new WeakReference<DialogInterface>(dialog); 153 } 154 155 @Override 156 public void handleMessage(Message msg) { 157 switch (msg.what) { 158 159 case DialogInterface.BUTTON_POSITIVE: 160 case DialogInterface.BUTTON_NEGATIVE: 161 case DialogInterface.BUTTON_NEUTRAL: 162 ((DialogInterface.OnClickListener) msg.obj).onClick(mDialog.get(), msg.what); 163 break; 164 165 case MSG_DISMISS_DIALOG: 166 ((DialogInterface) msg.obj).dismiss(); 167 } 168 } 169 } 170 171 private static boolean shouldCenterSingleButton(Context context) { 172 TypedValue outValue = new TypedValue(); 173 context.getTheme().resolveAttribute(com.android.internal.R.attr.alertDialogCenterButtons, 174 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 TypedArray a = context.obtainStyledAttributes(null, 185 com.android.internal.R.styleable.AlertDialog, 186 com.android.internal.R.attr.alertDialogStyle, 0); 187 188 mAlertDialogLayout = a.getResourceId(com.android.internal.R.styleable.AlertDialog_layout, 189 com.android.internal.R.layout.alert_dialog); 190 mButtonPanelSideLayout = a.getResourceId( 191 com.android.internal.R.styleable.AlertDialog_buttonPanelSideLayout, 0); 192 193 mListLayout = a.getResourceId( 194 com.android.internal.R.styleable.AlertDialog_listLayout, 195 com.android.internal.R.layout.select_dialog); 196 mMultiChoiceItemLayout = a.getResourceId( 197 com.android.internal.R.styleable.AlertDialog_multiChoiceItemLayout, 198 com.android.internal.R.layout.select_dialog_multichoice); 199 mSingleChoiceItemLayout = a.getResourceId( 200 com.android.internal.R.styleable.AlertDialog_singleChoiceItemLayout, 201 com.android.internal.R.layout.select_dialog_singlechoice); 202 mListItemLayout = a.getResourceId( 203 com.android.internal.R.styleable.AlertDialog_listItemLayout, 204 com.android.internal.R.layout.select_dialog_item); 205 206 a.recycle(); 207 } 208 209 static boolean canTextInput(View v) { 210 if (v.onCheckIsTextEditor()) { 211 return true; 212 } 213 214 if (!(v instanceof ViewGroup)) { 215 return false; 216 } 217 218 ViewGroup vg = (ViewGroup)v; 219 int i = vg.getChildCount(); 220 while (i > 0) { 221 i--; 222 v = vg.getChildAt(i); 223 if (canTextInput(v)) { 224 return true; 225 } 226 } 227 228 return false; 229 } 230 231 public void installContent() { 232 /* We use a custom title so never request a window title */ 233 mWindow.requestFeature(Window.FEATURE_NO_TITLE); 234 int contentView = selectContentView(); 235 mWindow.setContentView(contentView); 236 setupView(); 237 setupDecor(); 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.setImageResource(mIconId); 365 } else { 366 mIconView.setVisibility(View.GONE); 367 } 368 } 369 } 370 371 /** 372 * Specifies the icon to display next to the alert title. 373 * 374 * @param icon the drawable to use as the icon or null for no icon 375 */ 376 public void setIcon(Drawable icon) { 377 mIcon = icon; 378 mIconId = 0; 379 380 if (mIconView != null) { 381 if (icon != null) { 382 mIconView.setImageDrawable(icon); 383 } else { 384 mIconView.setVisibility(View.GONE); 385 } 386 } 387 } 388 389 /** 390 * @param attrId the attributeId of the theme-specific drawable 391 * to resolve the resourceId for. 392 * 393 * @return resId the resourceId of the theme-specific drawable 394 */ 395 public int getIconAttributeResId(int attrId) { 396 TypedValue out = new TypedValue(); 397 mContext.getTheme().resolveAttribute(attrId, out, true); 398 return out.resourceId; 399 } 400 401 public void setInverseBackgroundForced(boolean forceInverseBackground) { 402 mForceInverseBackground = forceInverseBackground; 403 } 404 405 public ListView getListView() { 406 return mListView; 407 } 408 409 public Button getButton(int whichButton) { 410 switch (whichButton) { 411 case DialogInterface.BUTTON_POSITIVE: 412 return mButtonPositive; 413 case DialogInterface.BUTTON_NEGATIVE: 414 return mButtonNegative; 415 case DialogInterface.BUTTON_NEUTRAL: 416 return mButtonNeutral; 417 default: 418 return null; 419 } 420 } 421 422 @SuppressWarnings({"UnusedDeclaration"}) 423 public boolean onKeyDown(int keyCode, KeyEvent event) { 424 return mScrollView != null && mScrollView.executeKeyEvent(event); 425 } 426 427 @SuppressWarnings({"UnusedDeclaration"}) 428 public boolean onKeyUp(int keyCode, KeyEvent event) { 429 return mScrollView != null && mScrollView.executeKeyEvent(event); 430 } 431 432 private void setupDecor() { 433 final View decor = mWindow.getDecorView(); 434 final View parent = mWindow.findViewById(R.id.parentPanel); 435 if (parent != null && decor != null) { 436 decor.setOnApplyWindowInsetsListener(new View.OnApplyWindowInsetsListener() { 437 @Override 438 public WindowInsets onApplyWindowInsets(View view, WindowInsets insets) { 439 if (insets.isRound()) { 440 // TODO: Get the padding as a function of the window size. 441 int roundOffset = mContext.getResources().getDimensionPixelOffset( 442 R.dimen.alert_dialog_round_padding); 443 parent.setPadding(roundOffset, roundOffset, roundOffset, roundOffset); 444 } 445 return insets.consumeSystemWindowInsets(); 446 } 447 }); 448 decor.setFitsSystemWindows(true); 449 decor.requestApplyInsets(); 450 } 451 } 452 453 private void setupView() { 454 final ViewGroup contentPanel = (ViewGroup) mWindow.findViewById(R.id.contentPanel); 455 setupContent(contentPanel); 456 final boolean hasButtons = setupButtons(); 457 458 final ViewGroup topPanel = (ViewGroup) mWindow.findViewById(R.id.topPanel); 459 final TypedArray a = mContext.obtainStyledAttributes( 460 null, R.styleable.AlertDialog, R.attr.alertDialogStyle, 0); 461 final boolean hasTitle = setupTitle(topPanel); 462 463 final View buttonPanel = mWindow.findViewById(R.id.buttonPanel); 464 if (!hasButtons) { 465 buttonPanel.setVisibility(View.GONE); 466 final View spacer = mWindow.findViewById(R.id.textSpacerNoButtons); 467 if (spacer != null) { 468 spacer.setVisibility(View.VISIBLE); 469 } 470 mWindow.setCloseOnTouchOutsideIfNotSet(true); 471 } 472 473 final FrameLayout customPanel = (FrameLayout) mWindow.findViewById(R.id.customPanel); 474 final View customView; 475 if (mView != null) { 476 customView = mView; 477 } else if (mViewLayoutResId != 0) { 478 final LayoutInflater inflater = LayoutInflater.from(mContext); 479 customView = inflater.inflate(mViewLayoutResId, customPanel, false); 480 } else { 481 customView = null; 482 } 483 484 final boolean hasCustomView = customView != null; 485 if (!hasCustomView || !canTextInput(customView)) { 486 mWindow.setFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM, 487 WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM); 488 } 489 490 if (hasCustomView) { 491 final FrameLayout custom = (FrameLayout) mWindow.findViewById(R.id.custom); 492 custom.addView(customView, new LayoutParams(MATCH_PARENT, MATCH_PARENT)); 493 494 if (mViewSpacingSpecified) { 495 custom.setPadding( 496 mViewSpacingLeft, mViewSpacingTop, mViewSpacingRight, mViewSpacingBottom); 497 } 498 499 if (mListView != null) { 500 ((LinearLayout.LayoutParams) customPanel.getLayoutParams()).weight = 0; 501 } 502 } else { 503 customPanel.setVisibility(View.GONE); 504 } 505 506 // Only display the divider if we have a title and a custom view or a 507 // message. 508 if (hasTitle) { 509 final View divider; 510 if (mMessage != null || customView != null || mListView != null) { 511 divider = mWindow.findViewById(R.id.titleDivider); 512 } else { 513 divider = mWindow.findViewById(R.id.titleDividerTop); 514 } 515 516 if (divider != null) { 517 divider.setVisibility(View.VISIBLE); 518 } 519 } 520 521 setBackground(a, topPanel, contentPanel, customPanel, buttonPanel, hasTitle, hasCustomView, 522 hasButtons); 523 a.recycle(); 524 } 525 526 private boolean setupTitle(ViewGroup topPanel) { 527 boolean hasTitle = true; 528 529 if (mCustomTitleView != null) { 530 // Add the custom title view directly to the topPanel layout 531 LayoutParams lp = new LayoutParams( 532 LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); 533 534 topPanel.addView(mCustomTitleView, 0, lp); 535 536 // Hide the title template 537 View titleTemplate = mWindow.findViewById(R.id.title_template); 538 titleTemplate.setVisibility(View.GONE); 539 } else { 540 mIconView = (ImageView) mWindow.findViewById(R.id.icon); 541 542 final boolean hasTextTitle = !TextUtils.isEmpty(mTitle); 543 if (hasTextTitle) { 544 // Display the title if a title is supplied, else hide it. 545 mTitleView = (TextView) mWindow.findViewById(R.id.alertTitle); 546 mTitleView.setText(mTitle); 547 548 // Do this last so that if the user has supplied any icons we 549 // use them instead of the default ones. If the user has 550 // specified 0 then make it disappear. 551 if (mIconId != 0) { 552 mIconView.setImageResource(mIconId); 553 } else if (mIcon != null) { 554 mIconView.setImageDrawable(mIcon); 555 } else { 556 // Apply the padding from the icon to ensure the title is 557 // aligned correctly. 558 mTitleView.setPadding(mIconView.getPaddingLeft(), 559 mIconView.getPaddingTop(), 560 mIconView.getPaddingRight(), 561 mIconView.getPaddingBottom()); 562 mIconView.setVisibility(View.GONE); 563 } 564 } else { 565 // Hide the title template 566 final View titleTemplate = mWindow.findViewById(R.id.title_template); 567 titleTemplate.setVisibility(View.GONE); 568 mIconView.setVisibility(View.GONE); 569 topPanel.setVisibility(View.GONE); 570 hasTitle = false; 571 } 572 } 573 return hasTitle; 574 } 575 576 private void setupContent(ViewGroup contentPanel) { 577 mScrollView = (ScrollView) mWindow.findViewById(R.id.scrollView); 578 mScrollView.setFocusable(false); 579 580 // Special case for users that only want to display a String 581 mMessageView = (TextView) mWindow.findViewById(R.id.message); 582 if (mMessageView == null) { 583 return; 584 } 585 586 if (mMessage != null) { 587 mMessageView.setText(mMessage); 588 } else { 589 mMessageView.setVisibility(View.GONE); 590 mScrollView.removeView(mMessageView); 591 592 if (mListView != null) { 593 final ViewGroup scrollParent = (ViewGroup) mScrollView.getParent(); 594 final int childIndex = scrollParent.indexOfChild(mScrollView); 595 scrollParent.removeViewAt(childIndex); 596 scrollParent.addView(mListView, childIndex, 597 new LayoutParams(MATCH_PARENT, MATCH_PARENT)); 598 } else { 599 contentPanel.setVisibility(View.GONE); 600 } 601 } 602 603 // Set up scroll indicators (if present). 604 final View indicatorUp = mWindow.findViewById(R.id.scrollIndicatorUp); 605 final View indicatorDown = mWindow.findViewById(R.id.scrollIndicatorDown); 606 if (indicatorUp != null || indicatorDown != null) { 607 if (mMessage != null) { 608 // We're just showing the ScrollView, set up listener. 609 mScrollView.setOnScrollChangeListener(new View.OnScrollChangeListener() { 610 @Override 611 public void onScrollChange(View v, int scrollX, int scrollY, 612 int oldScrollX, int oldScrollY) { 613 manageScrollIndicators(v, indicatorUp, indicatorDown); 614 } 615 }); 616 // Set up the indicators following layout. 617 mScrollView.post(new Runnable() { 618 @Override 619 public void run() { 620 manageScrollIndicators(mScrollView, indicatorUp, indicatorDown); 621 } 622 }); 623 624 } else if (mListView != null) { 625 // We're just showing the AbsListView, set up listener. 626 mListView.setOnScrollListener(new AbsListView.OnScrollListener() { 627 @Override 628 public void onScrollStateChanged(AbsListView view, int scrollState) { 629 // That's cool, I guess? 630 } 631 632 @Override 633 public void onScroll(AbsListView v, int firstVisibleItem, 634 int visibleItemCount, int totalItemCount) { 635 manageScrollIndicators(v, indicatorUp, indicatorDown); 636 } 637 }); 638 // Set up the indicators following layout. 639 mListView.post(new Runnable() { 640 @Override 641 public void run() { 642 manageScrollIndicators(mListView, indicatorUp, indicatorDown); 643 } 644 }); 645 } else { 646 // We don't have any content to scroll, remove the indicators. 647 if (indicatorUp != null) { 648 contentPanel.removeView(indicatorUp); 649 } 650 if (indicatorDown != null) { 651 contentPanel.removeView(indicatorDown); 652 } 653 } 654 } 655 } 656 657 private static void manageScrollIndicators(View v, View upIndicator, View downIndicator) { 658 if (upIndicator != null) { 659 upIndicator.setVisibility(v.canScrollVertically(-1) ? View.VISIBLE : View.INVISIBLE); 660 } 661 if (downIndicator != null) { 662 downIndicator.setVisibility(v.canScrollVertically(1) ? View.VISIBLE : View.INVISIBLE); 663 } 664 } 665 666 private boolean setupButtons() { 667 int BIT_BUTTON_POSITIVE = 1; 668 int BIT_BUTTON_NEGATIVE = 2; 669 int BIT_BUTTON_NEUTRAL = 4; 670 int whichButtons = 0; 671 mButtonPositive = (Button) mWindow.findViewById(R.id.button1); 672 mButtonPositive.setOnClickListener(mButtonHandler); 673 674 if (TextUtils.isEmpty(mButtonPositiveText)) { 675 mButtonPositive.setVisibility(View.GONE); 676 } else { 677 mButtonPositive.setText(mButtonPositiveText); 678 mButtonPositive.setVisibility(View.VISIBLE); 679 whichButtons = whichButtons | BIT_BUTTON_POSITIVE; 680 } 681 682 mButtonNegative = (Button) mWindow.findViewById(R.id.button2); 683 mButtonNegative.setOnClickListener(mButtonHandler); 684 685 if (TextUtils.isEmpty(mButtonNegativeText)) { 686 mButtonNegative.setVisibility(View.GONE); 687 } else { 688 mButtonNegative.setText(mButtonNegativeText); 689 mButtonNegative.setVisibility(View.VISIBLE); 690 691 whichButtons = whichButtons | BIT_BUTTON_NEGATIVE; 692 } 693 694 mButtonNeutral = (Button) mWindow.findViewById(R.id.button3); 695 mButtonNeutral.setOnClickListener(mButtonHandler); 696 697 if (TextUtils.isEmpty(mButtonNeutralText)) { 698 mButtonNeutral.setVisibility(View.GONE); 699 } else { 700 mButtonNeutral.setText(mButtonNeutralText); 701 mButtonNeutral.setVisibility(View.VISIBLE); 702 703 whichButtons = whichButtons | BIT_BUTTON_NEUTRAL; 704 } 705 706 if (shouldCenterSingleButton(mContext)) { 707 /* 708 * If we only have 1 button it should be centered on the layout and 709 * expand to fill 50% of the available space. 710 */ 711 if (whichButtons == BIT_BUTTON_POSITIVE) { 712 centerButton(mButtonPositive); 713 } else if (whichButtons == BIT_BUTTON_NEGATIVE) { 714 centerButton(mButtonNegative); 715 } else if (whichButtons == BIT_BUTTON_NEUTRAL) { 716 centerButton(mButtonNeutral); 717 } 718 } 719 720 return whichButtons != 0; 721 } 722 723 private void centerButton(Button button) { 724 LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) button.getLayoutParams(); 725 params.gravity = Gravity.CENTER_HORIZONTAL; 726 params.weight = 0.5f; 727 button.setLayoutParams(params); 728 View leftSpacer = mWindow.findViewById(R.id.leftSpacer); 729 if (leftSpacer != null) { 730 leftSpacer.setVisibility(View.VISIBLE); 731 } 732 View rightSpacer = mWindow.findViewById(R.id.rightSpacer); 733 if (rightSpacer != null) { 734 rightSpacer.setVisibility(View.VISIBLE); 735 } 736 } 737 738 private void setBackground(TypedArray a, View topPanel, View contentPanel, View customPanel, 739 View buttonPanel, boolean hasTitle, boolean hasCustomView, boolean hasButtons) { 740 int fullDark = 0; 741 int topDark = 0; 742 int centerDark = 0; 743 int bottomDark = 0; 744 int fullBright = 0; 745 int topBright = 0; 746 int centerBright = 0; 747 int bottomBright = 0; 748 int bottomMedium = 0; 749 750 // If the needsDefaultBackgrounds attribute is set, we know we're 751 // inheriting from a framework style. 752 final boolean needsDefaultBackgrounds = a.getBoolean( 753 R.styleable.AlertDialog_needsDefaultBackgrounds, true); 754 if (needsDefaultBackgrounds) { 755 fullDark = R.drawable.popup_full_dark; 756 topDark = R.drawable.popup_top_dark; 757 centerDark = R.drawable.popup_center_dark; 758 bottomDark = R.drawable.popup_bottom_dark; 759 fullBright = R.drawable.popup_full_bright; 760 topBright = R.drawable.popup_top_bright; 761 centerBright = R.drawable.popup_center_bright; 762 bottomBright = R.drawable.popup_bottom_bright; 763 bottomMedium = R.drawable.popup_bottom_medium; 764 } 765 766 topBright = a.getResourceId(R.styleable.AlertDialog_topBright, topBright); 767 topDark = a.getResourceId(R.styleable.AlertDialog_topDark, topDark); 768 centerBright = a.getResourceId(R.styleable.AlertDialog_centerBright, centerBright); 769 centerDark = a.getResourceId(R.styleable.AlertDialog_centerDark, centerDark); 770 771 /* We now set the background of all of the sections of the alert. 772 * First collect together each section that is being displayed along 773 * with whether it is on a light or dark background, then run through 774 * them setting their backgrounds. This is complicated because we need 775 * to correctly use the full, top, middle, and bottom graphics depending 776 * on how many views they are and where they appear. 777 */ 778 779 final View[] views = new View[4]; 780 final boolean[] light = new boolean[4]; 781 View lastView = null; 782 boolean lastLight = false; 783 784 int pos = 0; 785 if (hasTitle) { 786 views[pos] = topPanel; 787 light[pos] = false; 788 pos++; 789 } 790 791 /* The contentPanel displays either a custom text message or 792 * a ListView. If it's text we should use the dark background 793 * for ListView we should use the light background. If neither 794 * are there the contentPanel will be hidden so set it as null. 795 */ 796 views[pos] = contentPanel.getVisibility() == View.GONE ? null : contentPanel; 797 light[pos] = mListView != null; 798 pos++; 799 800 if (hasCustomView) { 801 views[pos] = customPanel; 802 light[pos] = mForceInverseBackground; 803 pos++; 804 } 805 806 if (hasButtons) { 807 views[pos] = buttonPanel; 808 light[pos] = true; 809 } 810 811 boolean setView = false; 812 for (pos = 0; pos < views.length; pos++) { 813 final View v = views[pos]; 814 if (v == null) { 815 continue; 816 } 817 818 if (lastView != null) { 819 if (!setView) { 820 lastView.setBackgroundResource(lastLight ? topBright : topDark); 821 } else { 822 lastView.setBackgroundResource(lastLight ? centerBright : centerDark); 823 } 824 setView = true; 825 } 826 827 lastView = v; 828 lastLight = light[pos]; 829 } 830 831 if (lastView != null) { 832 if (setView) { 833 bottomBright = a.getResourceId(R.styleable.AlertDialog_bottomBright, bottomBright); 834 bottomMedium = a.getResourceId(R.styleable.AlertDialog_bottomMedium, bottomMedium); 835 bottomDark = a.getResourceId(R.styleable.AlertDialog_bottomDark, bottomDark); 836 837 // ListViews will use the Bright background, but buttons use the 838 // Medium background. 839 lastView.setBackgroundResource( 840 lastLight ? (hasButtons ? bottomMedium : bottomBright) : bottomDark); 841 } else { 842 fullBright = a.getResourceId(R.styleable.AlertDialog_fullBright, fullBright); 843 fullDark = a.getResourceId(R.styleable.AlertDialog_fullDark, fullDark); 844 845 lastView.setBackgroundResource(lastLight ? fullBright : fullDark); 846 } 847 } 848 849 final ListView listView = mListView; 850 if (listView != null && mAdapter != null) { 851 listView.setAdapter(mAdapter); 852 final int checkedItem = mCheckedItem; 853 if (checkedItem > -1) { 854 listView.setItemChecked(checkedItem, true); 855 listView.setSelection(checkedItem); 856 } 857 } 858 } 859 860 public static class RecycleListView extends ListView { 861 boolean mRecycleOnMeasure = true; 862 863 public RecycleListView(Context context) { 864 super(context); 865 } 866 867 public RecycleListView(Context context, AttributeSet attrs) { 868 super(context, attrs); 869 } 870 871 public RecycleListView(Context context, AttributeSet attrs, int defStyleAttr) { 872 super(context, attrs, defStyleAttr); 873 } 874 875 public RecycleListView( 876 Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 877 super(context, attrs, defStyleAttr, defStyleRes); 878 } 879 880 @Override 881 protected boolean recycleOnMeasure() { 882 return mRecycleOnMeasure; 883 } 884 } 885 886 public static class AlertParams { 887 public final Context mContext; 888 public final LayoutInflater mInflater; 889 890 public int mIconId = 0; 891 public Drawable mIcon; 892 public int mIconAttrId = 0; 893 public CharSequence mTitle; 894 public View mCustomTitleView; 895 public CharSequence mMessage; 896 public CharSequence mPositiveButtonText; 897 public DialogInterface.OnClickListener mPositiveButtonListener; 898 public CharSequence mNegativeButtonText; 899 public DialogInterface.OnClickListener mNegativeButtonListener; 900 public CharSequence mNeutralButtonText; 901 public DialogInterface.OnClickListener mNeutralButtonListener; 902 public boolean mCancelable; 903 public DialogInterface.OnCancelListener mOnCancelListener; 904 public DialogInterface.OnDismissListener mOnDismissListener; 905 public DialogInterface.OnKeyListener mOnKeyListener; 906 public CharSequence[] mItems; 907 public ListAdapter mAdapter; 908 public DialogInterface.OnClickListener mOnClickListener; 909 public int mViewLayoutResId; 910 public View mView; 911 public int mViewSpacingLeft; 912 public int mViewSpacingTop; 913 public int mViewSpacingRight; 914 public int mViewSpacingBottom; 915 public boolean mViewSpacingSpecified = false; 916 public boolean[] mCheckedItems; 917 public boolean mIsMultiChoice; 918 public boolean mIsSingleChoice; 919 public int mCheckedItem = -1; 920 public DialogInterface.OnMultiChoiceClickListener mOnCheckboxClickListener; 921 public Cursor mCursor; 922 public String mLabelColumn; 923 public String mIsCheckedColumn; 924 public boolean mForceInverseBackground; 925 public AdapterView.OnItemSelectedListener mOnItemSelectedListener; 926 public OnPrepareListViewListener mOnPrepareListViewListener; 927 public boolean mRecycleOnMeasure = true; 928 929 /** 930 * Interface definition for a callback to be invoked before the ListView 931 * will be bound to an adapter. 932 */ 933 public interface OnPrepareListViewListener { 934 935 /** 936 * Called before the ListView is bound to an adapter. 937 * @param listView The ListView that will be shown in the dialog. 938 */ 939 void onPrepareListView(ListView listView); 940 } 941 942 public AlertParams(Context context) { 943 mContext = context; 944 mCancelable = true; 945 mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 946 } 947 948 public void apply(AlertController dialog) { 949 if (mCustomTitleView != null) { 950 dialog.setCustomTitle(mCustomTitleView); 951 } else { 952 if (mTitle != null) { 953 dialog.setTitle(mTitle); 954 } 955 if (mIcon != null) { 956 dialog.setIcon(mIcon); 957 } 958 if (mIconId != 0) { 959 dialog.setIcon(mIconId); 960 } 961 if (mIconAttrId != 0) { 962 dialog.setIcon(dialog.getIconAttributeResId(mIconAttrId)); 963 } 964 } 965 if (mMessage != null) { 966 dialog.setMessage(mMessage); 967 } 968 if (mPositiveButtonText != null) { 969 dialog.setButton(DialogInterface.BUTTON_POSITIVE, mPositiveButtonText, 970 mPositiveButtonListener, null); 971 } 972 if (mNegativeButtonText != null) { 973 dialog.setButton(DialogInterface.BUTTON_NEGATIVE, mNegativeButtonText, 974 mNegativeButtonListener, null); 975 } 976 if (mNeutralButtonText != null) { 977 dialog.setButton(DialogInterface.BUTTON_NEUTRAL, mNeutralButtonText, 978 mNeutralButtonListener, null); 979 } 980 if (mForceInverseBackground) { 981 dialog.setInverseBackgroundForced(true); 982 } 983 // For a list, the client can either supply an array of items or an 984 // adapter or a cursor 985 if ((mItems != null) || (mCursor != null) || (mAdapter != null)) { 986 createListView(dialog); 987 } 988 if (mView != null) { 989 if (mViewSpacingSpecified) { 990 dialog.setView(mView, mViewSpacingLeft, mViewSpacingTop, mViewSpacingRight, 991 mViewSpacingBottom); 992 } else { 993 dialog.setView(mView); 994 } 995 } else if (mViewLayoutResId != 0) { 996 dialog.setView(mViewLayoutResId); 997 } 998 999 /* 1000 dialog.setCancelable(mCancelable); 1001 dialog.setOnCancelListener(mOnCancelListener); 1002 if (mOnKeyListener != null) { 1003 dialog.setOnKeyListener(mOnKeyListener); 1004 } 1005 */ 1006 } 1007 1008 private void createListView(final AlertController dialog) { 1009 final RecycleListView listView = (RecycleListView) 1010 mInflater.inflate(dialog.mListLayout, null); 1011 ListAdapter adapter; 1012 1013 if (mIsMultiChoice) { 1014 if (mCursor == null) { 1015 adapter = new ArrayAdapter<CharSequence>( 1016 mContext, dialog.mMultiChoiceItemLayout, R.id.text1, mItems) { 1017 @Override 1018 public View getView(int position, View convertView, ViewGroup parent) { 1019 View view = super.getView(position, convertView, parent); 1020 if (mCheckedItems != null) { 1021 boolean isItemChecked = mCheckedItems[position]; 1022 if (isItemChecked) { 1023 listView.setItemChecked(position, true); 1024 } 1025 } 1026 return view; 1027 } 1028 }; 1029 } else { 1030 adapter = new CursorAdapter(mContext, mCursor, false) { 1031 private final int mLabelIndex; 1032 private final int mIsCheckedIndex; 1033 1034 { 1035 final Cursor cursor = getCursor(); 1036 mLabelIndex = cursor.getColumnIndexOrThrow(mLabelColumn); 1037 mIsCheckedIndex = cursor.getColumnIndexOrThrow(mIsCheckedColumn); 1038 } 1039 1040 @Override 1041 public void bindView(View view, Context context, Cursor cursor) { 1042 CheckedTextView text = (CheckedTextView) view.findViewById(R.id.text1); 1043 text.setText(cursor.getString(mLabelIndex)); 1044 listView.setItemChecked(cursor.getPosition(), 1045 cursor.getInt(mIsCheckedIndex) == 1); 1046 } 1047 1048 @Override 1049 public View newView(Context context, Cursor cursor, ViewGroup parent) { 1050 return mInflater.inflate(dialog.mMultiChoiceItemLayout, 1051 parent, false); 1052 } 1053 1054 }; 1055 } 1056 } else { 1057 int layout = mIsSingleChoice 1058 ? dialog.mSingleChoiceItemLayout : dialog.mListItemLayout; 1059 if (mCursor == null) { 1060 adapter = (mAdapter != null) ? mAdapter 1061 : new CheckedItemAdapter(mContext, layout, R.id.text1, mItems); 1062 } else { 1063 adapter = new SimpleCursorAdapter(mContext, layout, 1064 mCursor, new String[]{mLabelColumn}, new int[]{R.id.text1}); 1065 } 1066 } 1067 1068 if (mOnPrepareListViewListener != null) { 1069 mOnPrepareListViewListener.onPrepareListView(listView); 1070 } 1071 1072 /* Don't directly set the adapter on the ListView as we might 1073 * want to add a footer to the ListView later. 1074 */ 1075 dialog.mAdapter = adapter; 1076 dialog.mCheckedItem = mCheckedItem; 1077 1078 if (mOnClickListener != null) { 1079 listView.setOnItemClickListener(new OnItemClickListener() { 1080 @Override 1081 public void onItemClick(AdapterView<?> parent, View v, int position, long id) { 1082 mOnClickListener.onClick(dialog.mDialogInterface, position); 1083 if (!mIsSingleChoice) { 1084 dialog.mDialogInterface.dismiss(); 1085 } 1086 } 1087 }); 1088 } else if (mOnCheckboxClickListener != null) { 1089 listView.setOnItemClickListener(new OnItemClickListener() { 1090 @Override 1091 public void onItemClick(AdapterView<?> parent, View v, int position, long id) { 1092 if (mCheckedItems != null) { 1093 mCheckedItems[position] = listView.isItemChecked(position); 1094 } 1095 mOnCheckboxClickListener.onClick( 1096 dialog.mDialogInterface, position, listView.isItemChecked(position)); 1097 } 1098 }); 1099 } 1100 1101 // Attach a given OnItemSelectedListener to the ListView 1102 if (mOnItemSelectedListener != null) { 1103 listView.setOnItemSelectedListener(mOnItemSelectedListener); 1104 } 1105 1106 if (mIsSingleChoice) { 1107 listView.setChoiceMode(ListView.CHOICE_MODE_SINGLE); 1108 } else if (mIsMultiChoice) { 1109 listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); 1110 } 1111 listView.mRecycleOnMeasure = mRecycleOnMeasure; 1112 dialog.mListView = listView; 1113 } 1114 } 1115 1116 private static class CheckedItemAdapter extends ArrayAdapter<CharSequence> { 1117 public CheckedItemAdapter(Context context, int resource, int textViewResourceId, 1118 CharSequence[] objects) { 1119 super(context, resource, textViewResourceId, objects); 1120 } 1121 1122 @Override 1123 public boolean hasStableIds() { 1124 return true; 1125 } 1126 1127 @Override 1128 public long getItemId(int position) { 1129 return position; 1130 } 1131 } 1132 } 1133