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