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