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