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