CallCardPresenter.java revision 9e335e2d4fb43b22c7f95b2e9d4e048798e8e239
1/* 2 * Copyright (C) 2013 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 17package com.android.incallui; 18 19import static com.android.contacts.common.compat.CallCompat.Details.PROPERTY_ENTERPRISE_CALL; 20 21import android.Manifest; 22import android.content.Context; 23import android.content.Intent; 24import android.content.IntentFilter; 25import android.content.pm.ApplicationInfo; 26import android.content.pm.PackageManager; 27import android.graphics.drawable.Drawable; 28import android.hardware.display.DisplayManager; 29import android.os.BatteryManager; 30import android.os.Handler; 31import android.os.Trace; 32import android.support.annotation.NonNull; 33import android.support.annotation.Nullable; 34import android.support.v4.app.Fragment; 35import android.support.v4.content.ContextCompat; 36import android.telecom.Call.Details; 37import android.telecom.StatusHints; 38import android.telecom.TelecomManager; 39import android.text.TextUtils; 40import android.view.Display; 41import android.view.View; 42import android.view.accessibility.AccessibilityEvent; 43import android.view.accessibility.AccessibilityManager; 44import com.android.contacts.common.ContactsUtils; 45import com.android.contacts.common.preference.ContactsPreferences; 46import com.android.contacts.common.util.ContactDisplayUtils; 47import com.android.dialer.common.Assert; 48import com.android.dialer.common.LogUtil; 49import com.android.dialer.compat.ActivityCompat; 50import com.android.dialer.configprovider.ConfigProviderBindings; 51import com.android.dialer.logging.DialerImpression; 52import com.android.dialer.logging.Logger; 53import com.android.dialer.multimedia.MultimediaData; 54import com.android.dialer.oem.MotorolaUtils; 55import com.android.dialer.phonenumberutil.PhoneNumberHelper; 56import com.android.dialer.postcall.PostCall; 57import com.android.incallui.ContactInfoCache.ContactCacheEntry; 58import com.android.incallui.ContactInfoCache.ContactInfoCacheCallback; 59import com.android.incallui.InCallPresenter.InCallDetailsListener; 60import com.android.incallui.InCallPresenter.InCallEventListener; 61import com.android.incallui.InCallPresenter.InCallState; 62import com.android.incallui.InCallPresenter.InCallStateListener; 63import com.android.incallui.InCallPresenter.IncomingCallListener; 64import com.android.incallui.call.CallList; 65import com.android.incallui.call.DialerCall; 66import com.android.incallui.call.DialerCall.State; 67import com.android.incallui.call.DialerCallListener; 68import com.android.incallui.calllocation.CallLocation; 69import com.android.incallui.calllocation.CallLocationComponent; 70import com.android.incallui.incall.protocol.ContactPhotoType; 71import com.android.incallui.incall.protocol.InCallScreen; 72import com.android.incallui.incall.protocol.InCallScreenDelegate; 73import com.android.incallui.incall.protocol.PrimaryCallState; 74import com.android.incallui.incall.protocol.PrimaryCallState.ButtonState; 75import com.android.incallui.incall.protocol.PrimaryInfo; 76import com.android.incallui.incall.protocol.SecondaryInfo; 77import com.android.incallui.videotech.utils.SessionModificationState; 78import java.lang.ref.WeakReference; 79 80/** 81 * Controller for the Call Card Fragment. This class listens for changes to InCallState and passes 82 * it along to the fragment. 83 */ 84public class CallCardPresenter 85 implements InCallStateListener, 86 IncomingCallListener, 87 InCallDetailsListener, 88 InCallEventListener, 89 InCallScreenDelegate, 90 DialerCallListener { 91 92 /** 93 * Amount of time to wait before sending an announcement via the accessibility manager. When the 94 * call state changes to an outgoing or incoming state for the first time, the UI can often be 95 * changing due to call updates or contact lookup. This allows the UI to settle to a stable state 96 * to ensure that the correct information is announced. 97 */ 98 private static final long ACCESSIBILITY_ANNOUNCEMENT_DELAY_MILLIS = 500; 99 100 /** Flag to allow the user's current location to be shown during emergency calls. */ 101 private static final String CONFIG_ENABLE_EMERGENCY_LOCATION = "config_enable_emergency_location"; 102 103 private static final boolean CONFIG_ENABLE_EMERGENCY_LOCATION_DEFAULT = true; 104 105 /** 106 * Make it possible to not get location during an emergency call if the battery is too low, since 107 * doing so could trigger gps and thus potentially cause the phone to die in the middle of the 108 * call. 109 */ 110 private static final String CONFIG_MIN_BATTERY_PERCENT_FOR_EMERGENCY_LOCATION = 111 "min_battery_percent_for_emergency_location"; 112 113 private static final long CONFIG_MIN_BATTERY_PERCENT_FOR_EMERGENCY_LOCATION_DEFAULT = 10; 114 115 private final Context context; 116 private final Handler handler = new Handler(); 117 118 private DialerCall primary; 119 private String primaryNumber; 120 private DialerCall secondary; 121 private String secondaryNumber; 122 private ContactCacheEntry primaryContactInfo; 123 private ContactCacheEntry secondaryContactInfo; 124 @Nullable private ContactsPreferences contactsPreferences; 125 private boolean isFullscreen = false; 126 private InCallScreen inCallScreen; 127 private boolean isInCallScreenReady; 128 private boolean shouldSendAccessibilityEvent; 129 130 @NonNull private final CallLocation callLocation; 131 private final Runnable sendAccessibilityEventRunnable = 132 new Runnable() { 133 @Override 134 public void run() { 135 shouldSendAccessibilityEvent = !sendAccessibilityEvent(context, getUi()); 136 LogUtil.i( 137 "CallCardPresenter.sendAccessibilityEventRunnable", 138 "still should send: %b", 139 shouldSendAccessibilityEvent); 140 if (!shouldSendAccessibilityEvent) { 141 handler.removeCallbacks(this); 142 } 143 } 144 }; 145 146 public CallCardPresenter(Context context) { 147 LogUtil.i("CallCardPresenter.constructor", null); 148 this.context = Assert.isNotNull(context).getApplicationContext(); 149 callLocation = CallLocationComponent.get(this.context).getCallLocation(); 150 } 151 152 private static boolean hasCallSubject(DialerCall call) { 153 return !TextUtils.isEmpty(call.getCallSubject()); 154 } 155 156 @Override 157 public void onInCallScreenDelegateInit(InCallScreen inCallScreen) { 158 Assert.isNotNull(inCallScreen); 159 this.inCallScreen = inCallScreen; 160 contactsPreferences = ContactsPreferencesFactory.newContactsPreferences(context); 161 162 // Call may be null if disconnect happened already. 163 DialerCall call = CallList.getInstance().getFirstCall(); 164 if (call != null) { 165 primary = call; 166 if (shouldShowNoteSentToast(primary)) { 167 this.inCallScreen.showNoteSentToast(); 168 } 169 call.addListener(this); 170 // start processing lookups right away. 171 if (!call.isConferenceCall()) { 172 startContactInfoSearch(call, true, call.getState() == DialerCall.State.INCOMING); 173 } else { 174 updateContactEntry(null, true); 175 } 176 } 177 178 onStateChange(null, InCallPresenter.getInstance().getInCallState(), CallList.getInstance()); 179 } 180 181 @Override 182 public void onInCallScreenReady() { 183 LogUtil.i("CallCardPresenter.onInCallScreenReady", null); 184 Assert.checkState(!isInCallScreenReady); 185 if (contactsPreferences != null) { 186 contactsPreferences.refreshValue(ContactsPreferences.DISPLAY_ORDER_KEY); 187 } 188 189 // Contact search may have completed before ui is ready. 190 if (primaryContactInfo != null) { 191 updatePrimaryDisplayInfo(); 192 } 193 194 // Register for call state changes last 195 InCallPresenter.getInstance().addListener(this); 196 InCallPresenter.getInstance().addIncomingCallListener(this); 197 InCallPresenter.getInstance().addDetailsListener(this); 198 InCallPresenter.getInstance().addInCallEventListener(this); 199 isInCallScreenReady = true; 200 201 // Log location impressions 202 if (isOutgoingEmergencyCall(primary)) { 203 Logger.get(context).logImpression(DialerImpression.Type.EMERGENCY_NEW_EMERGENCY_CALL); 204 } else if (isIncomingEmergencyCall(primary) || isIncomingEmergencyCall(secondary)) { 205 Logger.get(context).logImpression(DialerImpression.Type.EMERGENCY_CALLBACK); 206 } 207 208 // Showing the location may have been skipped if the UI wasn't ready during previous layout. 209 if (shouldShowLocation()) { 210 inCallScreen.showLocationUi(getLocationFragment()); 211 212 // Log location impressions 213 if (!hasLocationPermission()) { 214 Logger.get(context).logImpression(DialerImpression.Type.EMERGENCY_NO_LOCATION_PERMISSION); 215 } else if (isBatteryTooLowForEmergencyLocation()) { 216 Logger.get(context) 217 .logImpression(DialerImpression.Type.EMERGENCY_BATTERY_TOO_LOW_TO_GET_LOCATION); 218 } else if (!callLocation.canGetLocation(context)) { 219 Logger.get(context).logImpression(DialerImpression.Type.EMERGENCY_CANT_GET_LOCATION); 220 } 221 } 222 } 223 224 @Override 225 public void onInCallScreenUnready() { 226 LogUtil.i("CallCardPresenter.onInCallScreenUnready", null); 227 Assert.checkState(isInCallScreenReady); 228 229 // stop getting call state changes 230 InCallPresenter.getInstance().removeListener(this); 231 InCallPresenter.getInstance().removeIncomingCallListener(this); 232 InCallPresenter.getInstance().removeDetailsListener(this); 233 InCallPresenter.getInstance().removeInCallEventListener(this); 234 if (primary != null) { 235 primary.removeListener(this); 236 } 237 238 callLocation.close(); 239 240 primary = null; 241 primaryContactInfo = null; 242 secondaryContactInfo = null; 243 isInCallScreenReady = false; 244 } 245 246 @Override 247 public void onIncomingCall(InCallState oldState, InCallState newState, DialerCall call) { 248 // same logic should happen as with onStateChange() 249 onStateChange(oldState, newState, CallList.getInstance()); 250 } 251 252 @Override 253 public void onStateChange(InCallState oldState, InCallState newState, CallList callList) { 254 Trace.beginSection("CallCardPresenter.onStateChange"); 255 LogUtil.v("CallCardPresenter.onStateChange", "oldState: %s, newState: %s", oldState, newState); 256 if (inCallScreen == null) { 257 Trace.endSection(); 258 return; 259 } 260 261 DialerCall primary = null; 262 DialerCall secondary = null; 263 264 if (newState == InCallState.INCOMING) { 265 primary = callList.getIncomingCall(); 266 } else if (newState == InCallState.PENDING_OUTGOING || newState == InCallState.OUTGOING) { 267 primary = callList.getOutgoingCall(); 268 if (primary == null) { 269 primary = callList.getPendingOutgoingCall(); 270 } 271 272 // getCallToDisplay doesn't go through outgoing or incoming calls. It will return the 273 // highest priority call to display as the secondary call. 274 secondary = getCallToDisplay(callList, null, true); 275 } else if (newState == InCallState.INCALL) { 276 primary = getCallToDisplay(callList, null, false); 277 secondary = getCallToDisplay(callList, primary, true); 278 } 279 280 LogUtil.v("CallCardPresenter.onStateChange", "primary call: " + primary); 281 LogUtil.v("CallCardPresenter.onStateChange", "secondary call: " + secondary); 282 String primaryNumber = null; 283 String secondaryNumber = null; 284 if (primary != null) { 285 primaryNumber = primary.getNumber(); 286 } 287 if (secondary != null) { 288 secondaryNumber = secondary.getNumber(); 289 } 290 291 final boolean primaryChanged = 292 !(DialerCall.areSame(this.primary, primary) 293 && TextUtils.equals(this.primaryNumber, primaryNumber)); 294 final boolean secondaryChanged = 295 !(DialerCall.areSame(this.secondary, secondary) 296 && TextUtils.equals(this.secondaryNumber, secondaryNumber)); 297 298 this.secondary = secondary; 299 this.secondaryNumber = secondaryNumber; 300 DialerCall previousPrimary = this.primary; 301 this.primary = primary; 302 this.primaryNumber = primaryNumber; 303 304 if (this.primary != null) { 305 InCallPresenter.getInstance().onForegroundCallChanged(this.primary); 306 inCallScreen.updateInCallScreenColors(); 307 } 308 309 if (primaryChanged && shouldShowNoteSentToast(primary)) { 310 inCallScreen.showNoteSentToast(); 311 } 312 313 // Refresh primary call information if either: 314 // 1. Primary call changed. 315 // 2. The call's ability to manage conference has changed. 316 if (shouldRefreshPrimaryInfo(primaryChanged)) { 317 // primary call has changed 318 if (previousPrimary != null) { 319 previousPrimary.removeListener(this); 320 } 321 this.primary.addListener(this); 322 323 primaryContactInfo = 324 ContactInfoCache.buildCacheEntryFromCall( 325 context, this.primary, this.primary.getState() == DialerCall.State.INCOMING); 326 updatePrimaryDisplayInfo(); 327 maybeStartSearch(this.primary, true); 328 } 329 330 if (previousPrimary != null && this.primary == null) { 331 previousPrimary.removeListener(this); 332 } 333 334 if (secondaryChanged) { 335 if (this.secondary == null) { 336 // Secondary call may have ended. Update the ui. 337 secondaryContactInfo = null; 338 updateSecondaryDisplayInfo(); 339 } else { 340 // secondary call has changed 341 secondaryContactInfo = 342 ContactInfoCache.buildCacheEntryFromCall( 343 context, this.secondary, this.secondary.getState() == DialerCall.State.INCOMING); 344 updateSecondaryDisplayInfo(); 345 maybeStartSearch(this.secondary, false); 346 } 347 } 348 349 // Set the call state 350 int callState = DialerCall.State.IDLE; 351 if (this.primary != null) { 352 callState = this.primary.getState(); 353 updatePrimaryCallState(); 354 } else { 355 getUi().setCallState(PrimaryCallState.empty()); 356 } 357 358 maybeShowManageConferenceCallButton(); 359 360 // Hide the end call button instantly if we're receiving an incoming call. 361 getUi() 362 .setEndCallButtonEnabled( 363 shouldShowEndCallButton(this.primary, callState), 364 callState != DialerCall.State.INCOMING /* animate */); 365 366 maybeSendAccessibilityEvent(oldState, newState, primaryChanged); 367 Trace.endSection(); 368 } 369 370 @Override 371 public void onDetailsChanged(DialerCall call, Details details) { 372 updatePrimaryCallState(); 373 374 if (call.can(Details.CAPABILITY_MANAGE_CONFERENCE) 375 != details.can(Details.CAPABILITY_MANAGE_CONFERENCE)) { 376 maybeShowManageConferenceCallButton(); 377 } 378 } 379 380 @Override 381 public void onDialerCallDisconnect() {} 382 383 @Override 384 public void onDialerCallUpdate() { 385 // No-op; specific call updates handled elsewhere. 386 } 387 388 @Override 389 public void onWiFiToLteHandover() {} 390 391 @Override 392 public void onHandoverToWifiFailure() {} 393 394 @Override 395 public void onInternationalCallOnWifi() {} 396 397 @Override 398 public void onEnrichedCallSessionUpdate() { 399 LogUtil.enterBlock("CallCardPresenter.onEnrichedCallSessionUpdate"); 400 updatePrimaryDisplayInfo(); 401 } 402 403 /** Handles a change to the child number by refreshing the primary call info. */ 404 @Override 405 public void onDialerCallChildNumberChange() { 406 LogUtil.v("CallCardPresenter.onDialerCallChildNumberChange", ""); 407 408 if (primary == null) { 409 return; 410 } 411 updatePrimaryDisplayInfo(); 412 } 413 414 /** Handles a change to the last forwarding number by refreshing the primary call info. */ 415 @Override 416 public void onDialerCallLastForwardedNumberChange() { 417 LogUtil.v("CallCardPresenter.onDialerCallLastForwardedNumberChange", ""); 418 419 if (primary == null) { 420 return; 421 } 422 updatePrimaryDisplayInfo(); 423 updatePrimaryCallState(); 424 } 425 426 @Override 427 public void onDialerCallUpgradeToVideo() {} 428 429 /** Handles a change to the session modification state for a call. */ 430 @Override 431 public void onDialerCallSessionModificationStateChange() { 432 LogUtil.enterBlock("CallCardPresenter.onDialerCallSessionModificationStateChange"); 433 434 if (primary == null) { 435 return; 436 } 437 getUi() 438 .setEndCallButtonEnabled( 439 primary.getVideoTech().getSessionModificationState() 440 != SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST, 441 true /* shouldAnimate */); 442 updatePrimaryCallState(); 443 } 444 445 private boolean shouldRefreshPrimaryInfo(boolean primaryChanged) { 446 if (primary == null) { 447 return false; 448 } 449 return primaryChanged 450 || inCallScreen.isManageConferenceVisible() != shouldShowManageConference(); 451 } 452 453 private void updatePrimaryCallState() { 454 if (getUi() != null && primary != null) { 455 boolean isWorkCall = 456 primary.hasProperty(PROPERTY_ENTERPRISE_CALL) 457 || (primaryContactInfo != null 458 && primaryContactInfo.userType == ContactsUtils.USER_TYPE_WORK); 459 boolean isHdAudioCall = 460 isPrimaryCallActive() && primary.hasProperty(Details.PROPERTY_HIGH_DEF_AUDIO); 461 boolean isAttemptingHdAudioCall = 462 !isHdAudioCall 463 && !primary.hasProperty(DialerCall.PROPERTY_CODEC_KNOWN) 464 && MotorolaUtils.shouldBlinkHdIconWhenConnectingCall(context); 465 466 boolean isBusiness = primaryContactInfo != null && primaryContactInfo.isBusiness; 467 468 // Check for video state change and update the visibility of the contact photo. The contact 469 // photo is hidden when the incoming video surface is shown. 470 // The contact photo visibility can also change in setPrimary(). 471 boolean shouldShowContactPhoto = 472 !VideoCallPresenter.showIncomingVideo(primary.getVideoState(), primary.getState()); 473 getUi() 474 .setCallState( 475 PrimaryCallState.builder() 476 .setState(primary.getState()) 477 .setIsVideoCall(primary.isVideoCall()) 478 .setSessionModificationState(primary.getVideoTech().getSessionModificationState()) 479 .setDisconnectCause(primary.getDisconnectCause()) 480 .setConnectionLabel(getConnectionLabel()) 481 .setConnectionIcon(getCallStateIcon()) 482 .setGatewayNumber(getGatewayNumber()) 483 .setCallSubject(shouldShowCallSubject(primary) ? primary.getCallSubject() : null) 484 .setCallbackNumber( 485 PhoneNumberHelper.formatNumber( 486 primary.getCallbackNumber(), primary.getSimCountryIso())) 487 .setIsWifi(primary.hasProperty(Details.PROPERTY_WIFI)) 488 .setIsConference( 489 primary.isConferenceCall() 490 && !primary.hasProperty(Details.PROPERTY_GENERIC_CONFERENCE)) 491 .setIsWorkCall(isWorkCall) 492 .setIsHdAttempting(isAttemptingHdAudioCall) 493 .setIsHdAudioCall(isHdAudioCall) 494 .setIsForwardedNumber( 495 !TextUtils.isEmpty(primary.getLastForwardedNumber()) 496 || primary.isCallForwarded()) 497 .setShouldShowContactPhoto(shouldShowContactPhoto) 498 .setConnectTimeMillis(primary.getConnectTimeMillis()) 499 .setIsVoiceMailNumber(primary.isVoiceMailNumber()) 500 .setIsRemotelyHeld(primary.isRemotelyHeld()) 501 .setIsBusinessNumber(isBusiness) 502 .setSupportsCallOnHold(supports2ndCallOnHold()) 503 .setSwapToSecondaryButtonState(getSwapToSecondaryButtonState()) 504 .setIsAssistedDialed(primary.isAssistedDialed()) 505 .setCustomLabel(null) 506 .setAssistedDialingExtras(primary.getAssistedDialingExtras()) 507 .build()); 508 509 InCallActivity activity = 510 (InCallActivity) (inCallScreen.getInCallScreenFragment().getActivity()); 511 if (activity != null) { 512 activity.onPrimaryCallStateChanged(); 513 } 514 } 515 } 516 517 private @ButtonState int getSwapToSecondaryButtonState() { 518 if (secondary == null) { 519 return ButtonState.NOT_SUPPORT; 520 } 521 if (primary.getState() == State.ACTIVE) { 522 return ButtonState.ENABLED; 523 } 524 return ButtonState.DISABLED; 525 } 526 527 /** Only show the conference call button if we can manage the conference. */ 528 private void maybeShowManageConferenceCallButton() { 529 getUi().showManageConferenceCallButton(shouldShowManageConference()); 530 } 531 532 /** 533 * Determines if the manage conference button should be visible, based on the current primary 534 * call. 535 * 536 * @return {@code True} if the manage conference button should be visible. 537 */ 538 private boolean shouldShowManageConference() { 539 if (primary == null) { 540 return false; 541 } 542 543 return primary.can(android.telecom.Call.Details.CAPABILITY_MANAGE_CONFERENCE) && !isFullscreen; 544 } 545 546 private boolean supports2ndCallOnHold() { 547 DialerCall firstCall = CallList.getInstance().getActiveOrBackgroundCall(); 548 DialerCall incomingCall = CallList.getInstance().getIncomingCall(); 549 if (firstCall != null && incomingCall != null && firstCall != incomingCall) { 550 return incomingCall.can(Details.CAPABILITY_HOLD); 551 } 552 return true; 553 } 554 555 @Override 556 public void onCallStateButtonClicked() { 557 Intent broadcastIntent = Bindings.get(context).getCallStateButtonBroadcastIntent(context); 558 if (broadcastIntent != null) { 559 LogUtil.v( 560 "CallCardPresenter.onCallStateButtonClicked", 561 "sending call state button broadcast: " + broadcastIntent); 562 context.sendBroadcast(broadcastIntent, Manifest.permission.READ_PHONE_STATE); 563 } 564 } 565 566 @Override 567 public void onManageConferenceClicked() { 568 InCallActivity activity = 569 (InCallActivity) (inCallScreen.getInCallScreenFragment().getActivity()); 570 activity.showConferenceFragment(true); 571 } 572 573 @Override 574 public void onShrinkAnimationComplete() { 575 InCallPresenter.getInstance().onShrinkAnimationComplete(); 576 } 577 578 private void maybeStartSearch(DialerCall call, boolean isPrimary) { 579 // no need to start search for conference calls which show generic info. 580 if (call != null && !call.isConferenceCall()) { 581 startContactInfoSearch(call, isPrimary, call.getState() == DialerCall.State.INCOMING); 582 } 583 } 584 585 /** Starts a query for more contact data for the save primary and secondary calls. */ 586 private void startContactInfoSearch( 587 final DialerCall call, final boolean isPrimary, boolean isIncoming) { 588 final ContactInfoCache cache = ContactInfoCache.getInstance(context); 589 590 cache.findInfo(call, isIncoming, new ContactLookupCallback(this, isPrimary)); 591 } 592 593 private void onContactInfoComplete(String callId, ContactCacheEntry entry, boolean isPrimary) { 594 final boolean entryMatchesExistingCall = 595 (isPrimary && primary != null && TextUtils.equals(callId, primary.getId())) 596 || (!isPrimary && secondary != null && TextUtils.equals(callId, secondary.getId())); 597 if (entryMatchesExistingCall) { 598 updateContactEntry(entry, isPrimary); 599 } else { 600 LogUtil.e( 601 "CallCardPresenter.onContactInfoComplete", 602 "dropping stale contact lookup info for " + callId); 603 } 604 605 final DialerCall call = CallList.getInstance().getCallById(callId); 606 if (call != null) { 607 call.getLogState().contactLookupResult = entry.contactLookupResult; 608 } 609 if (entry.lookupUri != null) { 610 CallerInfoUtils.sendViewNotification(context, entry.lookupUri); 611 } 612 } 613 614 private void onImageLoadComplete(String callId, ContactCacheEntry entry) { 615 if (getUi() == null) { 616 return; 617 } 618 619 if (entry.photo != null) { 620 if (primary != null && callId.equals(primary.getId())) { 621 updateContactEntry(entry, true /* isPrimary */); 622 } else if (secondary != null && callId.equals(secondary.getId())) { 623 updateContactEntry(entry, false /* isPrimary */); 624 } 625 } 626 } 627 628 private void updateContactEntry(ContactCacheEntry entry, boolean isPrimary) { 629 if (isPrimary) { 630 primaryContactInfo = entry; 631 updatePrimaryDisplayInfo(); 632 } else { 633 secondaryContactInfo = entry; 634 updateSecondaryDisplayInfo(); 635 } 636 } 637 638 /** 639 * Get the highest priority call to display. Goes through the calls and chooses which to return 640 * based on priority of which type of call to display to the user. Callers can use the "ignore" 641 * feature to get the second best call by passing a previously found primary call as ignore. 642 * 643 * @param ignore A call to ignore if found. 644 */ 645 private DialerCall getCallToDisplay( 646 CallList callList, DialerCall ignore, boolean skipDisconnected) { 647 // Active calls come second. An active call always gets precedent. 648 DialerCall retval = callList.getActiveCall(); 649 if (retval != null && retval != ignore) { 650 return retval; 651 } 652 653 // Sometimes there is intemediate state that two calls are in active even one is about 654 // to be on hold. 655 retval = callList.getSecondActiveCall(); 656 if (retval != null && retval != ignore) { 657 return retval; 658 } 659 660 // Disconnected calls get primary position if there are no active calls 661 // to let user know quickly what call has disconnected. Disconnected 662 // calls are very short lived. 663 if (!skipDisconnected) { 664 retval = callList.getDisconnectingCall(); 665 if (retval != null && retval != ignore) { 666 return retval; 667 } 668 retval = callList.getDisconnectedCall(); 669 if (retval != null && retval != ignore) { 670 return retval; 671 } 672 } 673 674 // Then we go to background call (calls on hold) 675 retval = callList.getBackgroundCall(); 676 if (retval != null && retval != ignore) { 677 return retval; 678 } 679 680 // Lastly, we go to a second background call. 681 retval = callList.getSecondBackgroundCall(); 682 683 return retval; 684 } 685 686 private void updatePrimaryDisplayInfo() { 687 if (inCallScreen == null) { 688 // TODO: May also occur if search result comes back after ui is destroyed. Look into 689 // removing that case completely. 690 LogUtil.v( 691 "CallCardPresenter.updatePrimaryDisplayInfo", 692 "updatePrimaryDisplayInfo called but ui is null!"); 693 return; 694 } 695 696 if (primary == null) { 697 // Clear the primary display info. 698 inCallScreen.setPrimary(PrimaryInfo.empty()); 699 return; 700 } 701 702 // Hide the contact photo if we are in a video call and the incoming video surface is 703 // showing. 704 boolean showContactPhoto = 705 !VideoCallPresenter.showIncomingVideo(primary.getVideoState(), primary.getState()); 706 707 // DialerCall placed through a work phone account. 708 boolean hasWorkCallProperty = primary.hasProperty(PROPERTY_ENTERPRISE_CALL); 709 710 MultimediaData multimediaData = null; 711 if (primary.getEnrichedCallSession() != null) { 712 multimediaData = primary.getEnrichedCallSession().getMultimediaData(); 713 } 714 715 if (primary.isConferenceCall()) { 716 LogUtil.v( 717 "CallCardPresenter.updatePrimaryDisplayInfo", 718 "update primary display info for conference call."); 719 720 inCallScreen.setPrimary( 721 PrimaryInfo.builder() 722 .setName( 723 CallerInfoUtils.getConferenceString( 724 context, primary.hasProperty(Details.PROPERTY_GENERIC_CONFERENCE))) 725 .setNameIsNumber(false) 726 .setPhotoType(ContactPhotoType.DEFAULT_PLACEHOLDER) 727 .setIsSipCall(false) 728 .setIsContactPhotoShown(showContactPhoto) 729 .setIsWorkCall(hasWorkCallProperty) 730 .setIsSpam(false) 731 .setIsLocalContact(false) 732 .setAnsweringDisconnectsOngoingCall(false) 733 .setShouldShowLocation(shouldShowLocation()) 734 .setShowInCallButtonGrid(true) 735 .setNumberPresentation(primary.getNumberPresentation()) 736 .build()); 737 } else if (primaryContactInfo != null) { 738 LogUtil.v( 739 "CallCardPresenter.updatePrimaryDisplayInfo", 740 "update primary display info for " + primaryContactInfo); 741 742 String name = getNameForCall(primaryContactInfo); 743 String number; 744 745 boolean isChildNumberShown = !TextUtils.isEmpty(primary.getChildNumber()); 746 boolean isForwardedNumberShown = !TextUtils.isEmpty(primary.getLastForwardedNumber()); 747 boolean isCallSubjectShown = shouldShowCallSubject(primary); 748 749 if (isCallSubjectShown) { 750 number = null; 751 } else if (isChildNumberShown) { 752 number = context.getString(R.string.child_number, primary.getChildNumber()); 753 } else if (isForwardedNumberShown) { 754 // Use last forwarded number instead of second line, if present. 755 number = primary.getLastForwardedNumber(); 756 } else { 757 number = primaryContactInfo.number; 758 } 759 760 boolean nameIsNumber = name != null && name.equals(primaryContactInfo.number); 761 762 // DialerCall with caller that is a work contact. 763 boolean isWorkContact = (primaryContactInfo.userType == ContactsUtils.USER_TYPE_WORK); 764 inCallScreen.setPrimary( 765 PrimaryInfo.builder() 766 .setNumber(number) 767 .setName(primary.updateNameIfRestricted(name)) 768 .setNameIsNumber(nameIsNumber) 769 .setLabel( 770 shouldShowLocationAsLabel(nameIsNumber, primaryContactInfo.shouldShowLocation) 771 ? primaryContactInfo.location 772 : null) 773 .setLocation( 774 isChildNumberShown || isCallSubjectShown ? null : primaryContactInfo.label) 775 .setPhoto(primaryContactInfo.photo) 776 .setPhotoType(primaryContactInfo.photoType) 777 .setIsSipCall(primaryContactInfo.isSipCall) 778 .setIsContactPhotoShown(showContactPhoto) 779 .setIsWorkCall(hasWorkCallProperty || isWorkContact) 780 .setIsSpam(primary.isSpam()) 781 .setIsLocalContact(primaryContactInfo.isLocalContact()) 782 .setAnsweringDisconnectsOngoingCall(primary.answeringDisconnectsForegroundVideoCall()) 783 .setShouldShowLocation(shouldShowLocation()) 784 .setContactInfoLookupKey(primaryContactInfo.lookupKey) 785 .setMultimediaData(multimediaData) 786 .setShowInCallButtonGrid(true) 787 .setNumberPresentation(primary.getNumberPresentation()) 788 .build()); 789 } else { 790 // Clear the primary display info. 791 inCallScreen.setPrimary(PrimaryInfo.empty()); 792 } 793 794 if (isInCallScreenReady) { 795 inCallScreen.showLocationUi(getLocationFragment()); 796 } else { 797 LogUtil.i("CallCardPresenter.updatePrimaryDisplayInfo", "UI not ready, not showing location"); 798 } 799 } 800 801 private static boolean shouldShowLocationAsLabel( 802 boolean nameIsNumber, boolean shouldShowLocation) { 803 if (nameIsNumber) { 804 return true; 805 } 806 if (shouldShowLocation) { 807 return true; 808 } 809 return false; 810 } 811 812 private Fragment getLocationFragment() { 813 if (!shouldShowLocation()) { 814 return null; 815 } 816 LogUtil.i("CallCardPresenter.getLocationFragment", "returning location fragment"); 817 return callLocation.getLocationFragment(context); 818 } 819 820 private boolean shouldShowLocation() { 821 if (!ConfigProviderBindings.get(context) 822 .getBoolean(CONFIG_ENABLE_EMERGENCY_LOCATION, CONFIG_ENABLE_EMERGENCY_LOCATION_DEFAULT)) { 823 LogUtil.i("CallCardPresenter.getLocationFragment", "disabled by config."); 824 return false; 825 } 826 if (!isPotentialEmergencyCall()) { 827 LogUtil.i("CallCardPresenter.getLocationFragment", "shouldn't show location"); 828 return false; 829 } 830 if (!hasLocationPermission()) { 831 LogUtil.i("CallCardPresenter.getLocationFragment", "no location permission."); 832 return false; 833 } 834 if (isBatteryTooLowForEmergencyLocation()) { 835 LogUtil.i("CallCardPresenter.getLocationFragment", "low battery."); 836 return false; 837 } 838 if (ActivityCompat.isInMultiWindowMode(inCallScreen.getInCallScreenFragment().getActivity())) { 839 LogUtil.i("CallCardPresenter.getLocationFragment", "in multi-window mode"); 840 return false; 841 } 842 if (primary.isVideoCall()) { 843 LogUtil.i("CallCardPresenter.getLocationFragment", "emergency video calls not supported"); 844 return false; 845 } 846 if (!callLocation.canGetLocation(context)) { 847 LogUtil.i("CallCardPresenter.getLocationFragment", "can't get current location"); 848 return false; 849 } 850 return true; 851 } 852 853 private boolean isPotentialEmergencyCall() { 854 if (isOutgoingEmergencyCall(primary)) { 855 LogUtil.i("CallCardPresenter.shouldShowLocation", "new emergency call"); 856 return true; 857 } else if (isIncomingEmergencyCall(primary)) { 858 LogUtil.i("CallCardPresenter.shouldShowLocation", "potential emergency callback"); 859 return true; 860 } else if (isIncomingEmergencyCall(secondary)) { 861 LogUtil.i("CallCardPresenter.shouldShowLocation", "has potential emergency callback"); 862 return true; 863 } 864 return false; 865 } 866 867 private static boolean isOutgoingEmergencyCall(@Nullable DialerCall call) { 868 return call != null && !call.isIncoming() && call.isEmergencyCall(); 869 } 870 871 private static boolean isIncomingEmergencyCall(@Nullable DialerCall call) { 872 return call != null && call.isIncoming() && call.isPotentialEmergencyCallback(); 873 } 874 875 private boolean hasLocationPermission() { 876 return ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) 877 == PackageManager.PERMISSION_GRANTED; 878 } 879 880 private boolean isBatteryTooLowForEmergencyLocation() { 881 Intent batteryStatus = 882 context.registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); 883 int status = batteryStatus.getIntExtra(BatteryManager.EXTRA_STATUS, -1); 884 if (status == BatteryManager.BATTERY_STATUS_CHARGING 885 || status == BatteryManager.BATTERY_STATUS_FULL) { 886 // Plugged in or full battery 887 return false; 888 } 889 int level = batteryStatus.getIntExtra(BatteryManager.EXTRA_LEVEL, -1); 890 int scale = batteryStatus.getIntExtra(BatteryManager.EXTRA_SCALE, -1); 891 float batteryPercent = (100f * level) / scale; 892 long threshold = 893 ConfigProviderBindings.get(context) 894 .getLong( 895 CONFIG_MIN_BATTERY_PERCENT_FOR_EMERGENCY_LOCATION, 896 CONFIG_MIN_BATTERY_PERCENT_FOR_EMERGENCY_LOCATION_DEFAULT); 897 LogUtil.i( 898 "CallCardPresenter.isBatteryTooLowForEmergencyLocation", 899 "percent charged: " + batteryPercent + ", min required charge: " + threshold); 900 return batteryPercent < threshold; 901 } 902 903 private void updateSecondaryDisplayInfo() { 904 if (inCallScreen == null) { 905 return; 906 } 907 908 if (secondary == null) { 909 // Clear the secondary display info. 910 inCallScreen.setSecondary(SecondaryInfo.builder().setIsFullscreen(isFullscreen).build()); 911 return; 912 } 913 914 if (secondary.isMergeInProcess()) { 915 LogUtil.i( 916 "CallCardPresenter.updateSecondaryDisplayInfo", 917 "secondary call is merge in process, clearing info"); 918 inCallScreen.setSecondary(SecondaryInfo.builder().setIsFullscreen(isFullscreen).build()); 919 return; 920 } 921 922 if (secondary.isConferenceCall()) { 923 inCallScreen.setSecondary( 924 SecondaryInfo.builder() 925 .setShouldShow(true) 926 .setName( 927 CallerInfoUtils.getConferenceString( 928 context, secondary.hasProperty(Details.PROPERTY_GENERIC_CONFERENCE))) 929 .setProviderLabel(secondary.getCallProviderLabel()) 930 .setIsConference(true) 931 .setIsVideoCall(secondary.isVideoCall()) 932 .setIsFullscreen(isFullscreen) 933 .build()); 934 } else if (secondaryContactInfo != null) { 935 LogUtil.v("CallCardPresenter.updateSecondaryDisplayInfo", "" + secondaryContactInfo); 936 String name = getNameForCall(secondaryContactInfo); 937 boolean nameIsNumber = name != null && name.equals(secondaryContactInfo.number); 938 inCallScreen.setSecondary( 939 SecondaryInfo.builder() 940 .setShouldShow(true) 941 .setName(secondary.updateNameIfRestricted(name)) 942 .setNameIsNumber(nameIsNumber) 943 .setLabel(secondaryContactInfo.label) 944 .setProviderLabel(secondary.getCallProviderLabel()) 945 .setIsVideoCall(secondary.isVideoCall()) 946 .setIsFullscreen(isFullscreen) 947 .build()); 948 } else { 949 // Clear the secondary display info. 950 inCallScreen.setSecondary(SecondaryInfo.builder().setIsFullscreen(isFullscreen).build()); 951 } 952 } 953 954 /** Returns the gateway number for any existing outgoing call. */ 955 private String getGatewayNumber() { 956 if (hasOutgoingGatewayCall()) { 957 return DialerCall.getNumberFromHandle(primary.getGatewayInfo().getGatewayAddress()); 958 } 959 return null; 960 } 961 962 /** 963 * Returns the label (line of text above the number/name) for any given call. For example, 964 * "calling via [Account/Google Voice]" for outgoing calls. 965 */ 966 private String getConnectionLabel() { 967 if (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_PHONE_STATE) 968 != PackageManager.PERMISSION_GRANTED) { 969 return null; 970 } 971 StatusHints statusHints = primary.getStatusHints(); 972 if (statusHints != null && !TextUtils.isEmpty(statusHints.getLabel())) { 973 return statusHints.getLabel().toString(); 974 } 975 976 if (hasOutgoingGatewayCall() && getUi() != null) { 977 // Return the label for the gateway app on outgoing calls. 978 final PackageManager pm = context.getPackageManager(); 979 try { 980 ApplicationInfo info = 981 pm.getApplicationInfo(primary.getGatewayInfo().getGatewayProviderPackageName(), 0); 982 return pm.getApplicationLabel(info).toString(); 983 } catch (PackageManager.NameNotFoundException e) { 984 LogUtil.e("CallCardPresenter.getConnectionLabel", "gateway Application Not Found.", e); 985 return null; 986 } 987 } 988 return primary.getCallProviderLabel(); 989 } 990 991 private Drawable getCallStateIcon() { 992 // Return connection icon if one exists. 993 StatusHints statusHints = primary.getStatusHints(); 994 if (statusHints != null && statusHints.getIcon() != null) { 995 Drawable icon = statusHints.getIcon().loadDrawable(context); 996 if (icon != null) { 997 return icon; 998 } 999 } 1000 1001 return null; 1002 } 1003 1004 private boolean hasOutgoingGatewayCall() { 1005 // We only display the gateway information while STATE_DIALING so return false for any other 1006 // call state. 1007 // TODO: mPrimary can be null because this is called from updatePrimaryDisplayInfo which 1008 // is also called after a contact search completes (call is not present yet). Split the 1009 // UI update so it can receive independent updates. 1010 if (primary == null) { 1011 return false; 1012 } 1013 return DialerCall.State.isDialing(primary.getState()) 1014 && primary.getGatewayInfo() != null 1015 && !primary.getGatewayInfo().isEmpty(); 1016 } 1017 1018 /** Gets the name to display for the call. */ 1019 private String getNameForCall(ContactCacheEntry contactInfo) { 1020 String preferredName = 1021 ContactDisplayUtils.getPreferredDisplayName( 1022 contactInfo.namePrimary, contactInfo.nameAlternative, contactsPreferences); 1023 if (TextUtils.isEmpty(preferredName)) { 1024 return contactInfo.number; 1025 } 1026 return preferredName; 1027 } 1028 1029 @Override 1030 public void onSecondaryInfoClicked() { 1031 if (secondary == null) { 1032 LogUtil.e( 1033 "CallCardPresenter.onSecondaryInfoClicked", 1034 "secondary info clicked but no secondary call."); 1035 return; 1036 } 1037 1038 Logger.get(context) 1039 .logCallImpression( 1040 DialerImpression.Type.IN_CALL_SWAP_SECONDARY_BUTTON_PRESSED, 1041 primary.getUniqueCallId(), 1042 primary.getTimeAddedMs()); 1043 LogUtil.i( 1044 "CallCardPresenter.onSecondaryInfoClicked", "swapping call to foreground: " + secondary); 1045 secondary.unhold(); 1046 } 1047 1048 @Override 1049 public void onEndCallClicked() { 1050 LogUtil.i("CallCardPresenter.onEndCallClicked", "disconnecting call: " + primary); 1051 if (primary != null) { 1052 primary.disconnect(); 1053 } 1054 PostCall.onDisconnectPressed(context); 1055 } 1056 1057 /** 1058 * Handles a change to the fullscreen mode of the in-call UI. 1059 * 1060 * @param isFullscreenMode {@code True} if the in-call UI is entering full screen mode. 1061 */ 1062 @Override 1063 public void onFullscreenModeChanged(boolean isFullscreenMode) { 1064 isFullscreen = isFullscreenMode; 1065 if (inCallScreen == null) { 1066 return; 1067 } 1068 maybeShowManageConferenceCallButton(); 1069 } 1070 1071 private boolean isPrimaryCallActive() { 1072 return primary != null && primary.getState() == DialerCall.State.ACTIVE; 1073 } 1074 1075 private boolean shouldShowEndCallButton(DialerCall primary, int callState) { 1076 if (primary == null) { 1077 return false; 1078 } 1079 if ((!DialerCall.State.isConnectingOrConnected(callState) 1080 && callState != DialerCall.State.DISCONNECTING 1081 && callState != DialerCall.State.DISCONNECTED) 1082 || callState == DialerCall.State.INCOMING) { 1083 return false; 1084 } 1085 if (this.primary.getVideoTech().getSessionModificationState() 1086 == SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST) { 1087 return false; 1088 } 1089 return true; 1090 } 1091 1092 @Override 1093 public void onInCallScreenResumed() { 1094 updatePrimaryDisplayInfo(); 1095 1096 if (shouldSendAccessibilityEvent) { 1097 handler.postDelayed(sendAccessibilityEventRunnable, ACCESSIBILITY_ANNOUNCEMENT_DELAY_MILLIS); 1098 } 1099 } 1100 1101 @Override 1102 public void onInCallScreenPaused() {} 1103 1104 static boolean sendAccessibilityEvent(Context context, InCallScreen inCallScreen) { 1105 AccessibilityManager am = 1106 (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE); 1107 if (!am.isEnabled()) { 1108 LogUtil.w("CallCardPresenter.sendAccessibilityEvent", "accessibility is off"); 1109 return false; 1110 } 1111 if (inCallScreen == null) { 1112 LogUtil.w("CallCardPresenter.sendAccessibilityEvent", "incallscreen is null"); 1113 return false; 1114 } 1115 Fragment fragment = inCallScreen.getInCallScreenFragment(); 1116 if (fragment == null || fragment.getView() == null || fragment.getView().getParent() == null) { 1117 LogUtil.w("CallCardPresenter.sendAccessibilityEvent", "fragment/view/parent is null"); 1118 return false; 1119 } 1120 1121 DisplayManager displayManager = 1122 (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE); 1123 Display display = displayManager.getDisplay(Display.DEFAULT_DISPLAY); 1124 boolean screenIsOn = display.getState() == Display.STATE_ON; 1125 LogUtil.d("CallCardPresenter.sendAccessibilityEvent", "screen is on: %b", screenIsOn); 1126 if (!screenIsOn) { 1127 return false; 1128 } 1129 1130 AccessibilityEvent event = AccessibilityEvent.obtain(AccessibilityEvent.TYPE_ANNOUNCEMENT); 1131 inCallScreen.dispatchPopulateAccessibilityEvent(event); 1132 View view = inCallScreen.getInCallScreenFragment().getView(); 1133 view.getParent().requestSendAccessibilityEvent(view, event); 1134 return true; 1135 } 1136 1137 private void maybeSendAccessibilityEvent( 1138 InCallState oldState, final InCallState newState, boolean primaryChanged) { 1139 shouldSendAccessibilityEvent = false; 1140 if (context == null) { 1141 return; 1142 } 1143 final AccessibilityManager am = 1144 (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE); 1145 if (!am.isEnabled()) { 1146 return; 1147 } 1148 // Announce the current call if it's new incoming/outgoing call or primary call is changed 1149 // due to switching calls between two ongoing calls (one is on hold). 1150 if ((oldState != InCallState.OUTGOING && newState == InCallState.OUTGOING) 1151 || (oldState != InCallState.INCOMING && newState == InCallState.INCOMING) 1152 || primaryChanged) { 1153 LogUtil.i( 1154 "CallCardPresenter.maybeSendAccessibilityEvent", "schedule accessibility announcement"); 1155 shouldSendAccessibilityEvent = true; 1156 handler.postDelayed(sendAccessibilityEventRunnable, ACCESSIBILITY_ANNOUNCEMENT_DELAY_MILLIS); 1157 } 1158 } 1159 1160 /** 1161 * Determines whether the call subject should be visible on the UI. For the call subject to be 1162 * visible, the call has to be in an incoming or waiting state, and the subject must not be empty. 1163 * 1164 * @param call The call. 1165 * @return {@code true} if the subject should be shown, {@code false} otherwise. 1166 */ 1167 private boolean shouldShowCallSubject(DialerCall call) { 1168 if (call == null) { 1169 return false; 1170 } 1171 1172 boolean isIncomingOrWaiting = 1173 primary.getState() == DialerCall.State.INCOMING 1174 || primary.getState() == DialerCall.State.CALL_WAITING; 1175 return isIncomingOrWaiting 1176 && !TextUtils.isEmpty(call.getCallSubject()) 1177 && call.getNumberPresentation() == TelecomManager.PRESENTATION_ALLOWED 1178 && call.isCallSubjectSupported(); 1179 } 1180 1181 /** 1182 * Determines whether the "note sent" toast should be shown. It should be shown for a new outgoing 1183 * call with a subject. 1184 * 1185 * @param call The call 1186 * @return {@code true} if the toast should be shown, {@code false} otherwise. 1187 */ 1188 private boolean shouldShowNoteSentToast(DialerCall call) { 1189 return call != null 1190 && hasCallSubject(call) 1191 && (call.getState() == DialerCall.State.DIALING 1192 || call.getState() == DialerCall.State.CONNECTING); 1193 } 1194 1195 private InCallScreen getUi() { 1196 return inCallScreen; 1197 } 1198 1199 /** Callback for contact lookup. */ 1200 public static class ContactLookupCallback implements ContactInfoCacheCallback { 1201 1202 private final WeakReference<CallCardPresenter> callCardPresenter; 1203 private final boolean isPrimary; 1204 1205 public ContactLookupCallback(CallCardPresenter callCardPresenter, boolean isPrimary) { 1206 this.callCardPresenter = new WeakReference<CallCardPresenter>(callCardPresenter); 1207 this.isPrimary = isPrimary; 1208 } 1209 1210 @Override 1211 public void onContactInfoComplete(String callId, ContactCacheEntry entry) { 1212 CallCardPresenter presenter = callCardPresenter.get(); 1213 if (presenter != null) { 1214 presenter.onContactInfoComplete(callId, entry, isPrimary); 1215 } 1216 } 1217 1218 @Override 1219 public void onImageLoadComplete(String callId, ContactCacheEntry entry) { 1220 CallCardPresenter presenter = callCardPresenter.get(); 1221 if (presenter != null) { 1222 presenter.onImageLoadComplete(callId, entry); 1223 } 1224 } 1225 } 1226} 1227