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 
17 package com.android.incallui;
18 
19 import com.google.common.base.Preconditions;
20 
21 import android.Manifest;
22 import android.content.Context;
23 import android.content.Intent;
24 import android.content.pm.ApplicationInfo;
25 import android.content.pm.PackageManager;
26 import android.graphics.drawable.Drawable;
27 import android.net.Uri;
28 import android.os.Bundle;
29 import android.support.annotation.Nullable;
30 import android.telecom.Call.Details;
31 import android.telecom.DisconnectCause;
32 import android.telecom.PhoneAccount;
33 import android.telecom.PhoneAccountHandle;
34 import android.telecom.StatusHints;
35 import android.telecom.TelecomManager;
36 import android.telecom.VideoProfile;
37 import android.telephony.PhoneNumberUtils;
38 import android.text.TextUtils;
39 import android.view.View;
40 import android.view.accessibility.AccessibilityManager;
41 import android.widget.ListAdapter;
42 
43 import com.android.contacts.common.ContactsUtils;
44 import com.android.contacts.common.compat.telecom.TelecomManagerCompat;
45 import com.android.contacts.common.preference.ContactsPreferences;
46 import com.android.contacts.common.testing.NeededForTesting;
47 import com.android.contacts.common.util.ContactDisplayUtils;
48 import com.android.dialer.R;
49 import com.android.incallui.Call.State;
50 import com.android.incallui.ContactInfoCache.ContactCacheEntry;
51 import com.android.incallui.ContactInfoCache.ContactInfoCacheCallback;
52 import com.android.incallui.InCallPresenter.InCallDetailsListener;
53 import com.android.incallui.InCallPresenter.InCallEventListener;
54 import com.android.incallui.InCallPresenter.InCallState;
55 import com.android.incallui.InCallPresenter.InCallStateListener;
56 import com.android.incallui.InCallPresenter.IncomingCallListener;
57 import com.android.incalluibind.ObjectFactory;
58 
59 import java.lang.ref.WeakReference;
60 
61 import static com.android.contacts.common.compat.CallSdkCompat.Details.PROPERTY_ENTERPRISE_CALL;
62 /**
63  * Presenter for the Call Card Fragment.
64  * <p>
65  * This class listens for changes to InCallState and passes it along to the fragment.
66  */
67 public class CallCardPresenter extends Presenter<CallCardPresenter.CallCardUi>
68         implements InCallStateListener, IncomingCallListener, InCallDetailsListener,
69         InCallEventListener, CallList.CallUpdateListener, DistanceHelper.Listener {
70 
71     public interface EmergencyCallListener {
onCallUpdated(BaseFragment fragment, boolean isEmergency)72         public void onCallUpdated(BaseFragment fragment, boolean isEmergency);
73     }
74 
75     private static final String TAG = CallCardPresenter.class.getSimpleName();
76     private static final long CALL_TIME_UPDATE_INTERVAL_MS = 1000;
77 
78     private final EmergencyCallListener mEmergencyCallListener =
79             ObjectFactory.newEmergencyCallListener();
80     private DistanceHelper mDistanceHelper;
81 
82     private Call mPrimary;
83     private Call mSecondary;
84     private ContactCacheEntry mPrimaryContactInfo;
85     private ContactCacheEntry mSecondaryContactInfo;
86     private CallTimer mCallTimer;
87     private Context mContext;
88     @Nullable private ContactsPreferences mContactsPreferences;
89     private boolean mSpinnerShowing = false;
90     private boolean mHasShownToast = false;
91     private InCallContactInteractions mInCallContactInteractions;
92     private boolean mIsFullscreen = false;
93 
94     public static class ContactLookupCallback implements ContactInfoCacheCallback {
95         private final WeakReference<CallCardPresenter> mCallCardPresenter;
96         private final boolean mIsPrimary;
97 
ContactLookupCallback(CallCardPresenter callCardPresenter, boolean isPrimary)98         public ContactLookupCallback(CallCardPresenter callCardPresenter, boolean isPrimary) {
99             mCallCardPresenter = new WeakReference<CallCardPresenter>(callCardPresenter);
100             mIsPrimary = isPrimary;
101         }
102 
103         @Override
onContactInfoComplete(String callId, ContactCacheEntry entry)104         public void onContactInfoComplete(String callId, ContactCacheEntry entry) {
105             CallCardPresenter presenter = mCallCardPresenter.get();
106             if (presenter != null) {
107                 presenter.onContactInfoComplete(callId, entry, mIsPrimary);
108             }
109         }
110 
111         @Override
onImageLoadComplete(String callId, ContactCacheEntry entry)112         public void onImageLoadComplete(String callId, ContactCacheEntry entry) {
113             CallCardPresenter presenter = mCallCardPresenter.get();
114             if (presenter != null) {
115                 presenter.onImageLoadComplete(callId, entry);
116             }
117         }
118 
119         @Override
onContactInteractionsInfoComplete(String callId, ContactCacheEntry entry)120         public void onContactInteractionsInfoComplete(String callId, ContactCacheEntry entry) {
121             CallCardPresenter presenter = mCallCardPresenter.get();
122             if (presenter != null) {
123                 presenter.onContactInteractionsInfoComplete(callId, entry);
124             }
125         }
126     }
127 
CallCardPresenter()128     public CallCardPresenter() {
129         // create the call timer
130         mCallTimer = new CallTimer(new Runnable() {
131             @Override
132             public void run() {
133                 updateCallTime();
134             }
135         });
136     }
137 
init(Context context, Call call)138     public void init(Context context, Call call) {
139         mContext = Preconditions.checkNotNull(context);
140         mDistanceHelper = ObjectFactory.newDistanceHelper(mContext, this);
141         mContactsPreferences = ContactsPreferencesFactory.newContactsPreferences(mContext);
142 
143         // Call may be null if disconnect happened already.
144         if (call != null) {
145             mPrimary = call;
146             if (shouldShowNoteSentToast(mPrimary)) {
147                 final CallCardUi ui = getUi();
148                 if (ui != null) {
149                     ui.showNoteSentToast();
150                 }
151             }
152             CallList.getInstance().addCallUpdateListener(call.getId(), this);
153 
154             // start processing lookups right away.
155             if (!call.isConferenceCall()) {
156                 startContactInfoSearch(call, true, call.getState() == Call.State.INCOMING);
157             } else {
158                 updateContactEntry(null, true);
159             }
160         }
161 
162         onStateChange(null, InCallPresenter.getInstance().getInCallState(), CallList.getInstance());
163     }
164 
165     @Override
onUiReady(CallCardUi ui)166     public void onUiReady(CallCardUi ui) {
167         super.onUiReady(ui);
168 
169         if (mContactsPreferences != null) {
170             mContactsPreferences.refreshValue(ContactsPreferences.DISPLAY_ORDER_KEY);
171         }
172 
173         // Contact search may have completed before ui is ready.
174         if (mPrimaryContactInfo != null) {
175             updatePrimaryDisplayInfo();
176         }
177 
178         // Register for call state changes last
179         InCallPresenter.getInstance().addListener(this);
180         InCallPresenter.getInstance().addIncomingCallListener(this);
181         InCallPresenter.getInstance().addDetailsListener(this);
182         InCallPresenter.getInstance().addInCallEventListener(this);
183     }
184 
185     @Override
onUiUnready(CallCardUi ui)186     public void onUiUnready(CallCardUi ui) {
187         super.onUiUnready(ui);
188 
189         // stop getting call state changes
190         InCallPresenter.getInstance().removeListener(this);
191         InCallPresenter.getInstance().removeIncomingCallListener(this);
192         InCallPresenter.getInstance().removeDetailsListener(this);
193         InCallPresenter.getInstance().removeInCallEventListener(this);
194         if (mPrimary != null) {
195             CallList.getInstance().removeCallUpdateListener(mPrimary.getId(), this);
196         }
197 
198         if (mDistanceHelper != null) {
199             mDistanceHelper.cleanUp();
200         }
201 
202         mPrimary = null;
203         mPrimaryContactInfo = null;
204         mSecondaryContactInfo = null;
205     }
206 
207     @Override
onIncomingCall(InCallState oldState, InCallState newState, Call call)208     public void onIncomingCall(InCallState oldState, InCallState newState, Call call) {
209         // same logic should happen as with onStateChange()
210         onStateChange(oldState, newState, CallList.getInstance());
211     }
212 
213     @Override
onStateChange(InCallState oldState, InCallState newState, CallList callList)214     public void onStateChange(InCallState oldState, InCallState newState, CallList callList) {
215         Log.d(this, "onStateChange() " + newState);
216         final CallCardUi ui = getUi();
217         if (ui == null) {
218             return;
219         }
220 
221         Call primary = null;
222         Call secondary = null;
223 
224         if (newState == InCallState.INCOMING) {
225             primary = callList.getIncomingCall();
226         } else if (newState == InCallState.PENDING_OUTGOING || newState == InCallState.OUTGOING) {
227             primary = callList.getOutgoingCall();
228             if (primary == null) {
229                 primary = callList.getPendingOutgoingCall();
230             }
231 
232             // getCallToDisplay doesn't go through outgoing or incoming calls. It will return the
233             // highest priority call to display as the secondary call.
234             secondary = getCallToDisplay(callList, null, true);
235         } else if (newState == InCallState.INCALL) {
236             primary = getCallToDisplay(callList, null, false);
237             secondary = getCallToDisplay(callList, primary, true);
238         }
239 
240         if (mInCallContactInteractions != null &&
241                 (oldState == InCallState.INCOMING || newState == InCallState.INCOMING)) {
242             ui.showContactContext(newState != InCallState.INCOMING);
243         }
244 
245         Log.d(this, "Primary call: " + primary);
246         Log.d(this, "Secondary call: " + secondary);
247 
248         final boolean primaryChanged = !(Call.areSame(mPrimary, primary) &&
249                 Call.areSameNumber(mPrimary, primary));
250         final boolean secondaryChanged = !(Call.areSame(mSecondary, secondary) &&
251                 Call.areSameNumber(mSecondary, secondary));
252 
253         mSecondary = secondary;
254         Call previousPrimary = mPrimary;
255         mPrimary = primary;
256 
257         if (primaryChanged && shouldShowNoteSentToast(primary)) {
258             ui.showNoteSentToast();
259         }
260 
261         // Refresh primary call information if either:
262         // 1. Primary call changed.
263         // 2. The call's ability to manage conference has changed.
264         // 3. The call subject should be shown or hidden.
265         if (shouldRefreshPrimaryInfo(primaryChanged, ui, shouldShowCallSubject(mPrimary))) {
266             // primary call has changed
267             if (previousPrimary != null) {
268                 //clear progess spinner (if any) related to previous primary call
269                 maybeShowProgressSpinner(previousPrimary.getState(),
270                         Call.SessionModificationState.NO_REQUEST);
271                 CallList.getInstance().removeCallUpdateListener(previousPrimary.getId(), this);
272             }
273             CallList.getInstance().addCallUpdateListener(mPrimary.getId(), this);
274 
275             mPrimaryContactInfo = ContactInfoCache.buildCacheEntryFromCall(mContext, mPrimary,
276                     mPrimary.getState() == Call.State.INCOMING);
277             updatePrimaryDisplayInfo();
278             maybeStartSearch(mPrimary, true);
279             maybeClearSessionModificationState(mPrimary);
280         }
281 
282         if (previousPrimary != null && mPrimary == null) {
283             //clear progess spinner (if any) related to previous primary call
284             maybeShowProgressSpinner(previousPrimary.getState(),
285                     Call.SessionModificationState.NO_REQUEST);
286             CallList.getInstance().removeCallUpdateListener(previousPrimary.getId(), this);
287         }
288 
289         if (mSecondary == null) {
290             // Secondary call may have ended.  Update the ui.
291             mSecondaryContactInfo = null;
292             updateSecondaryDisplayInfo();
293         } else if (secondaryChanged) {
294             // secondary call has changed
295             mSecondaryContactInfo = ContactInfoCache.buildCacheEntryFromCall(mContext, mSecondary,
296                     mSecondary.getState() == Call.State.INCOMING);
297             updateSecondaryDisplayInfo();
298             maybeStartSearch(mSecondary, false);
299             maybeClearSessionModificationState(mSecondary);
300         }
301 
302         // Start/stop timers.
303         if (isPrimaryCallActive()) {
304             Log.d(this, "Starting the calltime timer");
305             mCallTimer.start(CALL_TIME_UPDATE_INTERVAL_MS);
306         } else {
307             Log.d(this, "Canceling the calltime timer");
308             mCallTimer.cancel();
309             ui.setPrimaryCallElapsedTime(false, 0);
310         }
311 
312         // Set the call state
313         int callState = Call.State.IDLE;
314         if (mPrimary != null) {
315             callState = mPrimary.getState();
316             updatePrimaryCallState();
317         } else {
318             getUi().setCallState(
319                     callState,
320                     VideoProfile.STATE_AUDIO_ONLY,
321                     Call.SessionModificationState.NO_REQUEST,
322                     new DisconnectCause(DisconnectCause.UNKNOWN),
323                     null,
324                     null,
325                     null,
326                     false /* isWifi */,
327                     false /* isConference */,
328                     false /* isWorkCall */);
329             getUi().showHdAudioIndicator(false);
330         }
331 
332         maybeShowManageConferenceCallButton();
333 
334         // Hide the end call button instantly if we're receiving an incoming call.
335         getUi().setEndCallButtonEnabled(shouldShowEndCallButton(mPrimary, callState),
336                 callState != Call.State.INCOMING /* animate */);
337 
338         maybeSendAccessibilityEvent(oldState, newState, primaryChanged);
339     }
340 
341     @Override
onDetailsChanged(Call call, Details details)342     public void onDetailsChanged(Call call, Details details) {
343         updatePrimaryCallState();
344 
345         if (call.can(Details.CAPABILITY_MANAGE_CONFERENCE) !=
346                 details.can(Details.CAPABILITY_MANAGE_CONFERENCE)) {
347             maybeShowManageConferenceCallButton();
348         }
349     }
350 
351     @Override
onCallChanged(Call call)352     public void onCallChanged(Call call) {
353         // No-op; specific call updates handled elsewhere.
354     }
355 
356     /**
357      * Handles a change to the session modification state for a call.  Triggers showing the progress
358      * spinner, as well as updating the call state label.
359      *
360      * @param sessionModificationState The new session modification state.
361      */
362     @Override
onSessionModificationStateChange(int sessionModificationState)363     public void onSessionModificationStateChange(int sessionModificationState) {
364         Log.d(this, "onSessionModificationStateChange : sessionModificationState = " +
365                 sessionModificationState);
366 
367         if (mPrimary == null) {
368             return;
369         }
370         maybeShowProgressSpinner(mPrimary.getState(), sessionModificationState);
371         getUi().setEndCallButtonEnabled(sessionModificationState !=
372                         Call.SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST,
373                 true /* shouldAnimate */);
374         updatePrimaryCallState();
375     }
376 
377     /**
378      * Handles a change to the last forwarding number by refreshing the primary call info.
379      */
380     @Override
onLastForwardedNumberChange()381     public void onLastForwardedNumberChange() {
382         Log.v(this, "onLastForwardedNumberChange");
383 
384         if (mPrimary == null) {
385             return;
386         }
387         updatePrimaryDisplayInfo();
388     }
389 
390     /**
391      * Handles a change to the child number by refreshing the primary call info.
392      */
393     @Override
onChildNumberChange()394     public void onChildNumberChange() {
395         Log.v(this, "onChildNumberChange");
396 
397         if (mPrimary == null) {
398             return;
399         }
400         updatePrimaryDisplayInfo();
401     }
402 
shouldRefreshPrimaryInfo(boolean primaryChanged, CallCardUi ui, boolean shouldShowCallSubject)403     private boolean shouldRefreshPrimaryInfo(boolean primaryChanged, CallCardUi ui,
404             boolean shouldShowCallSubject) {
405         if (mPrimary == null) {
406             return false;
407         }
408         return primaryChanged ||
409                 ui.isManageConferenceVisible() != shouldShowManageConference() ||
410                 ui.isCallSubjectVisible() != shouldShowCallSubject;
411     }
412 
getSubscriptionNumber()413     private String getSubscriptionNumber() {
414         // If it's an emergency call, and they're not populating the callback number,
415         // then try to fall back to the phone sub info (to hopefully get the SIM's
416         // number directly from the telephony layer).
417         PhoneAccountHandle accountHandle = mPrimary.getAccountHandle();
418         if (accountHandle != null) {
419             TelecomManager mgr = InCallPresenter.getInstance().getTelecomManager();
420             PhoneAccount account = TelecomManagerCompat.getPhoneAccount(mgr, accountHandle);
421             if (account != null) {
422                 return getNumberFromHandle(account.getSubscriptionAddress());
423             }
424         }
425         return null;
426     }
427 
updatePrimaryCallState()428     private void updatePrimaryCallState() {
429         if (getUi() != null && mPrimary != null) {
430             boolean isWorkCall = mPrimary.hasProperty(PROPERTY_ENTERPRISE_CALL)
431                     || (mPrimaryContactInfo == null ? false
432                             : mPrimaryContactInfo.userType == ContactsUtils.USER_TYPE_WORK);
433             getUi().setCallState(
434                     mPrimary.getState(),
435                     mPrimary.getVideoState(),
436                     mPrimary.getSessionModificationState(),
437                     mPrimary.getDisconnectCause(),
438                     getConnectionLabel(),
439                     getCallStateIcon(),
440                     getGatewayNumber(),
441                     mPrimary.hasProperty(Details.PROPERTY_WIFI),
442                     mPrimary.isConferenceCall(),
443                     isWorkCall);
444 
445             maybeShowHdAudioIcon();
446             setCallbackNumber();
447         }
448     }
449 
450     /**
451      * Show the HD icon if the call is active and has {@link Details#PROPERTY_HIGH_DEF_AUDIO},
452      * except if the call has a last forwarded number (we will show that icon instead).
453      */
maybeShowHdAudioIcon()454     private void maybeShowHdAudioIcon() {
455         boolean showHdAudioIndicator =
456                 isPrimaryCallActive() && mPrimary.hasProperty(Details.PROPERTY_HIGH_DEF_AUDIO) &&
457                 TextUtils.isEmpty(mPrimary.getLastForwardedNumber());
458         getUi().showHdAudioIndicator(showHdAudioIndicator);
459     }
460 
461     /**
462      * Only show the conference call button if we can manage the conference.
463      */
maybeShowManageConferenceCallButton()464     private void maybeShowManageConferenceCallButton() {
465         getUi().showManageConferenceCallButton(shouldShowManageConference());
466     }
467 
468     /**
469      * Determines if a pending session modification exists for the current call.  If so, the
470      * progress spinner is shown, and the call state is updated.
471      *
472      * @param callState The call state.
473      * @param sessionModificationState The session modification state.
474      */
maybeShowProgressSpinner(int callState, int sessionModificationState)475     private void maybeShowProgressSpinner(int callState, int sessionModificationState) {
476         final boolean show = sessionModificationState ==
477                 Call.SessionModificationState.WAITING_FOR_RESPONSE
478                 && callState == Call.State.ACTIVE;
479         if (show != mSpinnerShowing) {
480             getUi().setProgressSpinnerVisible(show);
481             mSpinnerShowing = show;
482         }
483     }
484 
485     /**
486      * Determines if the manage conference button should be visible, based on the current primary
487      * call.
488      *
489      * @return {@code True} if the manage conference button should be visible.
490      */
shouldShowManageConference()491     private boolean shouldShowManageConference() {
492         if (mPrimary == null) {
493             return false;
494         }
495 
496         return mPrimary.can(android.telecom.Call.Details.CAPABILITY_MANAGE_CONFERENCE)
497                 && !mIsFullscreen;
498     }
499 
setCallbackNumber()500     private void setCallbackNumber() {
501         String callbackNumber = null;
502 
503         // Show the emergency callback number if either:
504         // 1. This is an emergency call.
505         // 2. The phone is in Emergency Callback Mode, which means we should show the callback
506         //    number.
507         boolean showCallbackNumber = mPrimary.hasProperty(Details.PROPERTY_EMERGENCY_CALLBACK_MODE);
508 
509         if (mPrimary.isEmergencyCall() || showCallbackNumber) {
510             callbackNumber = getSubscriptionNumber();
511         } else {
512             StatusHints statusHints = mPrimary.getTelecomCall().getDetails().getStatusHints();
513             if (statusHints != null) {
514                 Bundle extras = statusHints.getExtras();
515                 if (extras != null) {
516                     callbackNumber = extras.getString(TelecomManager.EXTRA_CALL_BACK_NUMBER);
517                 }
518             }
519         }
520 
521         final String simNumber = TelecomManagerCompat.getLine1Number(
522                 InCallPresenter.getInstance().getTelecomManager(),
523                 InCallPresenter.getInstance().getTelephonyManager(),
524                 mPrimary.getAccountHandle());
525         if (!showCallbackNumber && PhoneNumberUtils.compare(callbackNumber, simNumber)) {
526             Log.d(this, "Numbers are the same (and callback number is not being forced to show);" +
527                     " not showing the callback number");
528             callbackNumber = null;
529         }
530 
531         getUi().setCallbackNumber(callbackNumber, mPrimary.isEmergencyCall() || showCallbackNumber);
532     }
533 
updateCallTime()534     public void updateCallTime() {
535         final CallCardUi ui = getUi();
536 
537         if (ui == null) {
538             mCallTimer.cancel();
539         } else if (!isPrimaryCallActive()) {
540             ui.setPrimaryCallElapsedTime(false, 0);
541             mCallTimer.cancel();
542         } else {
543             final long callStart = mPrimary.getConnectTimeMillis();
544             final long duration = System.currentTimeMillis() - callStart;
545             ui.setPrimaryCallElapsedTime(true, duration);
546         }
547     }
548 
onCallStateButtonTouched()549     public void onCallStateButtonTouched() {
550         Intent broadcastIntent = ObjectFactory.getCallStateButtonBroadcastIntent(mContext);
551         if (broadcastIntent != null) {
552             Log.d(this, "Sending call state button broadcast: ", broadcastIntent);
553             mContext.sendBroadcast(broadcastIntent, Manifest.permission.READ_PHONE_STATE);
554         }
555     }
556 
557     /**
558      * Handles click on the contact photo by toggling fullscreen mode if the current call is a video
559      * call.
560      */
onContactPhotoClick()561     public void onContactPhotoClick() {
562         if (mPrimary != null && mPrimary.isVideoCall(mContext)) {
563             InCallPresenter.getInstance().toggleFullscreenMode();
564         }
565     }
566 
maybeStartSearch(Call call, boolean isPrimary)567     private void maybeStartSearch(Call call, boolean isPrimary) {
568         // no need to start search for conference calls which show generic info.
569         if (call != null && !call.isConferenceCall()) {
570             startContactInfoSearch(call, isPrimary, call.getState() == Call.State.INCOMING);
571         }
572     }
573 
maybeClearSessionModificationState(Call call)574     private void maybeClearSessionModificationState(Call call) {
575         if (call.getSessionModificationState() !=
576                 Call.SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
577             call.setSessionModificationState(Call.SessionModificationState.NO_REQUEST);
578         }
579     }
580 
581     /**
582      * Starts a query for more contact data for the save primary and secondary calls.
583      */
startContactInfoSearch(final Call call, final boolean isPrimary, boolean isIncoming)584     private void startContactInfoSearch(final Call call, final boolean isPrimary,
585             boolean isIncoming) {
586         final ContactInfoCache cache = ContactInfoCache.getInstance(mContext);
587 
588         cache.findInfo(call, isIncoming, new ContactLookupCallback(this, isPrimary));
589     }
590 
onContactInfoComplete(String callId, ContactCacheEntry entry, boolean isPrimary)591     private void onContactInfoComplete(String callId, ContactCacheEntry entry, boolean isPrimary) {
592         final boolean entryMatchesExistingCall =
593                 (isPrimary && mPrimary != null && TextUtils.equals(callId,  mPrimary.getId())) ||
594                 (!isPrimary && mSecondary != null && TextUtils.equals(callId, mSecondary.getId()));
595         if (entryMatchesExistingCall) {
596             updateContactEntry(entry, isPrimary);
597         } else {
598             Log.w(this, "Dropping stale contact lookup info for " + callId);
599         }
600 
601         final Call call = CallList.getInstance().getCallById(callId);
602         if (call != null) {
603             call.getLogState().contactLookupResult = entry.contactLookupResult;
604         }
605         if (entry.contactUri != null) {
606             CallerInfoUtils.sendViewNotification(mContext, entry.contactUri);
607         }
608     }
609 
onImageLoadComplete(String callId, ContactCacheEntry entry)610     private void onImageLoadComplete(String callId, ContactCacheEntry entry) {
611         if (getUi() == null) {
612             return;
613         }
614 
615         if (entry.photo != null) {
616             if (mPrimary != null && callId.equals(mPrimary.getId())) {
617                 boolean showContactPhoto = !VideoCallPresenter.showIncomingVideo(
618                         mPrimary.getVideoState(), mPrimary.getState());
619                 getUi().setPrimaryImage(entry.photo, showContactPhoto);
620             }
621         }
622     }
623 
onContactInteractionsInfoComplete(String callId, ContactCacheEntry entry)624     private void onContactInteractionsInfoComplete(String callId, ContactCacheEntry entry) {
625         if (getUi() == null) {
626             return;
627         }
628 
629         if (mPrimary != null && callId.equals(mPrimary.getId())) {
630             mPrimaryContactInfo.locationAddress = entry.locationAddress;
631             updateContactInteractions();
632         }
633     }
634 
635     @Override
onLocationReady()636     public void onLocationReady() {
637         // This will only update the contacts interactions data if the location returns after
638         // the contact information is found.
639         updateContactInteractions();
640     }
641 
updateContactInteractions()642     private void updateContactInteractions() {
643         if (mPrimary != null && mPrimaryContactInfo != null
644                 && (mPrimaryContactInfo.locationAddress != null
645                         || mPrimaryContactInfo.openingHours != null)) {
646             // TODO: This is hardcoded to "isBusiness" because functionality to differentiate
647             // between business and personal has not yet been added.
648             if (setInCallContactInteractionsType(true /* isBusiness */)) {
649                 getUi().setContactContextTitle(
650                         mInCallContactInteractions.getBusinessListHeaderView());
651             }
652 
653             mInCallContactInteractions.setBusinessInfo(
654                     mPrimaryContactInfo.locationAddress,
655                     mDistanceHelper.calculateDistance(mPrimaryContactInfo.locationAddress),
656                     mPrimaryContactInfo.openingHours);
657             getUi().setContactContextContent(mInCallContactInteractions.getListAdapter());
658             getUi().showContactContext(mPrimary.getState() != State.INCOMING);
659         } else {
660             getUi().showContactContext(false);
661         }
662     }
663 
664     /**
665      * Update the contact interactions type so that the correct UI is shown.
666      *
667      * @param isBusiness {@code true} if the interaction is a business interaction, {@code false} if
668      * it is a personal contact.
669      *
670      * @return {@code true} if this is a new type of contact interaction (business or personal).
671      * {@code false} if it hasn't changed.
672      */
setInCallContactInteractionsType(boolean isBusiness)673     private boolean setInCallContactInteractionsType(boolean isBusiness) {
674         if (mInCallContactInteractions == null) {
675             mInCallContactInteractions =
676                     new InCallContactInteractions(mContext, isBusiness);
677             return true;
678         }
679 
680         return mInCallContactInteractions.switchContactType(isBusiness);
681     }
682 
updateContactEntry(ContactCacheEntry entry, boolean isPrimary)683     private void updateContactEntry(ContactCacheEntry entry, boolean isPrimary) {
684         if (isPrimary) {
685             mPrimaryContactInfo = entry;
686             updatePrimaryDisplayInfo();
687         } else {
688             mSecondaryContactInfo = entry;
689             updateSecondaryDisplayInfo();
690         }
691     }
692 
693     /**
694      * Get the highest priority call to display.
695      * Goes through the calls and chooses which to return based on priority of which type of call
696      * to display to the user. Callers can use the "ignore" feature to get the second best call
697      * by passing a previously found primary call as ignore.
698      *
699      * @param ignore A call to ignore if found.
700      */
getCallToDisplay(CallList callList, Call ignore, boolean skipDisconnected)701     private Call getCallToDisplay(CallList callList, Call ignore, boolean skipDisconnected) {
702         // Active calls come second.  An active call always gets precedent.
703         Call retval = callList.getActiveCall();
704         if (retval != null && retval != ignore) {
705             return retval;
706         }
707 
708         // Sometimes there is intemediate state that two calls are in active even one is about
709         // to be on hold.
710         retval = callList.getSecondActiveCall();
711         if (retval != null && retval != ignore) {
712             return retval;
713         }
714 
715         // Disconnected calls get primary position if there are no active calls
716         // to let user know quickly what call has disconnected. Disconnected
717         // calls are very short lived.
718         if (!skipDisconnected) {
719             retval = callList.getDisconnectingCall();
720             if (retval != null && retval != ignore) {
721                 return retval;
722             }
723             retval = callList.getDisconnectedCall();
724             if (retval != null && retval != ignore) {
725                 return retval;
726             }
727         }
728 
729         // Then we go to background call (calls on hold)
730         retval = callList.getBackgroundCall();
731         if (retval != null && retval != ignore) {
732             return retval;
733         }
734 
735         // Lastly, we go to a second background call.
736         retval = callList.getSecondBackgroundCall();
737 
738         return retval;
739     }
740 
updatePrimaryDisplayInfo()741     private void updatePrimaryDisplayInfo() {
742         final CallCardUi ui = getUi();
743         if (ui == null) {
744             // TODO: May also occur if search result comes back after ui is destroyed. Look into
745             // removing that case completely.
746             Log.d(TAG, "updatePrimaryDisplayInfo called but ui is null!");
747             return;
748         }
749 
750         if (mPrimary == null) {
751             // Clear the primary display info.
752             ui.setPrimary(null, null, false, null, null, false, false, false);
753             return;
754         }
755 
756         // Hide the contact photo if we are in a video call and the incoming video surface is
757         // showing.
758         boolean showContactPhoto = !VideoCallPresenter
759                 .showIncomingVideo(mPrimary.getVideoState(), mPrimary.getState());
760 
761         // Call placed through a work phone account.
762         boolean hasWorkCallProperty = mPrimary.hasProperty(PROPERTY_ENTERPRISE_CALL);
763 
764         if (mPrimary.isConferenceCall()) {
765             Log.d(TAG, "Update primary display info for conference call.");
766 
767             ui.setPrimary(
768                     null /* number */,
769                     getConferenceString(mPrimary),
770                     false /* nameIsNumber */,
771                     null /* label */,
772                     getConferencePhoto(mPrimary),
773                     false /* isSipCall */,
774                     showContactPhoto,
775                     hasWorkCallProperty);
776         } else if (mPrimaryContactInfo != null) {
777             Log.d(TAG, "Update primary display info for " + mPrimaryContactInfo);
778 
779             String name = getNameForCall(mPrimaryContactInfo);
780             String number;
781 
782             boolean isChildNumberShown = !TextUtils.isEmpty(mPrimary.getChildNumber());
783             boolean isForwardedNumberShown = !TextUtils.isEmpty(mPrimary.getLastForwardedNumber());
784             boolean isCallSubjectShown = shouldShowCallSubject(mPrimary);
785 
786             if (isCallSubjectShown) {
787                 ui.setCallSubject(mPrimary.getCallSubject());
788             } else {
789                 ui.setCallSubject(null);
790             }
791 
792             if (isCallSubjectShown) {
793                 number = null;
794             } else if (isChildNumberShown) {
795                 number = mContext.getString(R.string.child_number, mPrimary.getChildNumber());
796             } else if (isForwardedNumberShown) {
797                 // Use last forwarded number instead of second line, if present.
798                 number = mPrimary.getLastForwardedNumber();
799             } else {
800                 number = getNumberForCall(mPrimaryContactInfo);
801             }
802 
803             ui.showForwardIndicator(isForwardedNumberShown);
804             maybeShowHdAudioIcon();
805 
806             boolean nameIsNumber = name != null && name.equals(mPrimaryContactInfo.number);
807             // Call with caller that is a work contact.
808             boolean isWorkContact = (mPrimaryContactInfo.userType == ContactsUtils.USER_TYPE_WORK);
809             ui.setPrimary(
810                     number,
811                     name,
812                     nameIsNumber,
813                     isChildNumberShown || isCallSubjectShown ? null : mPrimaryContactInfo.label,
814                     mPrimaryContactInfo.photo,
815                     mPrimaryContactInfo.isSipCall,
816                     showContactPhoto,
817                     hasWorkCallProperty || isWorkContact);
818 
819             updateContactInteractions();
820         } else {
821             // Clear the primary display info.
822             ui.setPrimary(null, null, false, null, null, false, false, false);
823         }
824 
825         if (mEmergencyCallListener != null) {
826             boolean isEmergencyCall = mPrimary.isEmergencyCall();
827             mEmergencyCallListener.onCallUpdated((BaseFragment) ui, isEmergencyCall);
828         }
829     }
830 
updateSecondaryDisplayInfo()831     private void updateSecondaryDisplayInfo() {
832         final CallCardUi ui = getUi();
833         if (ui == null) {
834             return;
835         }
836 
837         if (mSecondary == null) {
838             // Clear the secondary display info.
839             ui.setSecondary(false, null, false, null, null, false /* isConference */,
840                     false /* isVideoCall */, mIsFullscreen);
841             return;
842         }
843 
844         if (mSecondary.isConferenceCall()) {
845             ui.setSecondary(
846                     true /* show */,
847                     getConferenceString(mSecondary),
848                     false /* nameIsNumber */,
849                     null /* label */,
850                     getCallProviderLabel(mSecondary),
851                     true /* isConference */,
852                     mSecondary.isVideoCall(mContext),
853                     mIsFullscreen);
854         } else if (mSecondaryContactInfo != null) {
855             Log.d(TAG, "updateSecondaryDisplayInfo() " + mSecondaryContactInfo);
856             String name = getNameForCall(mSecondaryContactInfo);
857             boolean nameIsNumber = name != null && name.equals(mSecondaryContactInfo.number);
858             ui.setSecondary(
859                     true /* show */,
860                     name,
861                     nameIsNumber,
862                     mSecondaryContactInfo.label,
863                     getCallProviderLabel(mSecondary),
864                     false /* isConference */,
865                     mSecondary.isVideoCall(mContext),
866                     mIsFullscreen);
867         } else {
868             // Clear the secondary display info.
869             ui.setSecondary(false, null, false, null, null, false /* isConference */,
870                     false /* isVideoCall */, mIsFullscreen);
871         }
872     }
873 
874 
875     /**
876      * Gets the phone account to display for a call.
877      */
getAccountForCall(Call call)878     private PhoneAccount getAccountForCall(Call call) {
879         PhoneAccountHandle accountHandle = call.getAccountHandle();
880         if (accountHandle == null) {
881             return null;
882         }
883         return TelecomManagerCompat.getPhoneAccount(
884                 InCallPresenter.getInstance().getTelecomManager(),
885                 accountHandle);
886     }
887 
888     /**
889      * Returns the gateway number for any existing outgoing call.
890      */
getGatewayNumber()891     private String getGatewayNumber() {
892         if (hasOutgoingGatewayCall()) {
893             return getNumberFromHandle(mPrimary.getGatewayInfo().getGatewayAddress());
894         }
895         return null;
896     }
897 
898     /**
899      * Return the string label to represent the call provider
900      */
getCallProviderLabel(Call call)901     private String getCallProviderLabel(Call call) {
902         PhoneAccount account = getAccountForCall(call);
903         TelecomManager mgr = InCallPresenter.getInstance().getTelecomManager();
904         if (account != null && !TextUtils.isEmpty(account.getLabel())
905                 && TelecomManagerCompat.getCallCapablePhoneAccounts(mgr).size() > 1) {
906             return account.getLabel().toString();
907         }
908         return null;
909     }
910 
911     /**
912      * Returns the label (line of text above the number/name) for any given call.
913      * For example, "calling via [Account/Google Voice]" for outgoing calls.
914      */
getConnectionLabel()915     private String getConnectionLabel() {
916         StatusHints statusHints = mPrimary.getTelecomCall().getDetails().getStatusHints();
917         if (statusHints != null && !TextUtils.isEmpty(statusHints.getLabel())) {
918             return statusHints.getLabel().toString();
919         }
920 
921         if (hasOutgoingGatewayCall() && getUi() != null) {
922             // Return the label for the gateway app on outgoing calls.
923             final PackageManager pm = mContext.getPackageManager();
924             try {
925                 ApplicationInfo info = pm.getApplicationInfo(
926                         mPrimary.getGatewayInfo().getGatewayProviderPackageName(), 0);
927                 return pm.getApplicationLabel(info).toString();
928             } catch (PackageManager.NameNotFoundException e) {
929                 Log.e(this, "Gateway Application Not Found.", e);
930                 return null;
931             }
932         }
933         return getCallProviderLabel(mPrimary);
934     }
935 
getCallStateIcon()936     private Drawable getCallStateIcon() {
937         // Return connection icon if one exists.
938         StatusHints statusHints = mPrimary.getTelecomCall().getDetails().getStatusHints();
939         if (statusHints != null && statusHints.getIcon() != null) {
940             Drawable icon = statusHints.getIcon().loadDrawable(mContext);
941             if (icon != null) {
942                 return icon;
943             }
944         }
945 
946         return null;
947     }
948 
hasOutgoingGatewayCall()949     private boolean hasOutgoingGatewayCall() {
950         // We only display the gateway information while STATE_DIALING so return false for any other
951         // call state.
952         // TODO: mPrimary can be null because this is called from updatePrimaryDisplayInfo which
953         // is also called after a contact search completes (call is not present yet).  Split the
954         // UI update so it can receive independent updates.
955         if (mPrimary == null) {
956             return false;
957         }
958         return Call.State.isDialing(mPrimary.getState()) && mPrimary.getGatewayInfo() != null &&
959                 !mPrimary.getGatewayInfo().isEmpty();
960     }
961 
962     /**
963      * Gets the name to display for the call.
964      */
965     @NeededForTesting
getNameForCall(ContactCacheEntry contactInfo)966     String getNameForCall(ContactCacheEntry contactInfo) {
967         String preferredName = ContactDisplayUtils.getPreferredDisplayName(
968                 contactInfo.namePrimary,
969                 contactInfo.nameAlternative,
970                 mContactsPreferences);
971         if (TextUtils.isEmpty(preferredName)) {
972             return contactInfo.number;
973         }
974         return preferredName;
975     }
976 
977     /**
978      * Gets the number to display for a call.
979      */
980     @NeededForTesting
getNumberForCall(ContactCacheEntry contactInfo)981     String getNumberForCall(ContactCacheEntry contactInfo) {
982         // If the name is empty, we use the number for the name...so don't show a second
983         // number in the number field
984         String preferredName = ContactDisplayUtils.getPreferredDisplayName(
985                     contactInfo.namePrimary,
986                     contactInfo.nameAlternative,
987                     mContactsPreferences);
988         if (TextUtils.isEmpty(preferredName)) {
989             return contactInfo.location;
990         }
991         return contactInfo.number;
992     }
993 
secondaryInfoClicked()994     public void secondaryInfoClicked() {
995         if (mSecondary == null) {
996             Log.w(this, "Secondary info clicked but no secondary call.");
997             return;
998         }
999 
1000         Log.i(this, "Swapping call to foreground: " + mSecondary);
1001         TelecomAdapter.getInstance().unholdCall(mSecondary.getId());
1002     }
1003 
endCallClicked()1004     public void endCallClicked() {
1005         if (mPrimary == null) {
1006             return;
1007         }
1008 
1009         Log.i(this, "Disconnecting call: " + mPrimary);
1010         final String callId = mPrimary.getId();
1011         mPrimary.setState(Call.State.DISCONNECTING);
1012         CallList.getInstance().onUpdate(mPrimary);
1013         TelecomAdapter.getInstance().disconnectCall(callId);
1014     }
1015 
getNumberFromHandle(Uri handle)1016     private String getNumberFromHandle(Uri handle) {
1017         return handle == null ? "" : handle.getSchemeSpecificPart();
1018     }
1019 
1020     /**
1021      * Handles a change to the fullscreen mode of the in-call UI.
1022      *
1023      * @param isFullscreenMode {@code True} if the in-call UI is entering full screen mode.
1024      */
1025     @Override
onFullscreenModeChanged(boolean isFullscreenMode)1026     public void onFullscreenModeChanged(boolean isFullscreenMode) {
1027         mIsFullscreen = isFullscreenMode;
1028         final CallCardUi ui = getUi();
1029         if (ui == null) {
1030             return;
1031         }
1032         ui.setCallCardVisible(!isFullscreenMode);
1033         ui.setSecondaryInfoVisible(!isFullscreenMode);
1034         maybeShowManageConferenceCallButton();
1035     }
1036 
1037     @Override
onSecondaryCallerInfoVisibilityChanged(boolean isVisible, int height)1038     public void onSecondaryCallerInfoVisibilityChanged(boolean isVisible, int height) {
1039         // No-op - the Call Card is the origin of this event.
1040     }
1041 
isPrimaryCallActive()1042     private boolean isPrimaryCallActive() {
1043         return mPrimary != null && mPrimary.getState() == Call.State.ACTIVE;
1044     }
1045 
getConferenceString(Call call)1046     private String getConferenceString(Call call) {
1047         boolean isGenericConference = call.hasProperty(Details.PROPERTY_GENERIC_CONFERENCE);
1048         Log.v(this, "getConferenceString: " + isGenericConference);
1049 
1050         final int resId = isGenericConference
1051                 ? R.string.card_title_in_call : R.string.card_title_conf_call;
1052         return mContext.getResources().getString(resId);
1053     }
1054 
getConferencePhoto(Call call)1055     private Drawable getConferencePhoto(Call call) {
1056         boolean isGenericConference = call.hasProperty(Details.PROPERTY_GENERIC_CONFERENCE);
1057         Log.v(this, "getConferencePhoto: " + isGenericConference);
1058 
1059         final int resId = isGenericConference
1060                 ? R.drawable.img_phone : R.drawable.img_conference;
1061         Drawable photo = mContext.getResources().getDrawable(resId);
1062         photo.setAutoMirrored(true);
1063         return photo;
1064     }
1065 
shouldShowEndCallButton(Call primary, int callState)1066     private boolean shouldShowEndCallButton(Call primary, int callState) {
1067         if (primary == null) {
1068             return false;
1069         }
1070         if ((!Call.State.isConnectingOrConnected(callState)
1071                 && callState != Call.State.DISCONNECTING) || callState == Call.State.INCOMING) {
1072             return false;
1073         }
1074         if (mPrimary.getSessionModificationState()
1075                 == Call.SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
1076             return false;
1077         }
1078         return true;
1079     }
1080 
maybeSendAccessibilityEvent(InCallState oldState, InCallState newState, boolean primaryChanged)1081     private void maybeSendAccessibilityEvent(InCallState oldState, InCallState newState,
1082                                              boolean primaryChanged) {
1083         if (mContext == null) {
1084             return;
1085         }
1086         final AccessibilityManager am = (AccessibilityManager) mContext.getSystemService(
1087                 Context.ACCESSIBILITY_SERVICE);
1088         if (!am.isEnabled()) {
1089             return;
1090         }
1091         // Announce the current call if it's new incoming/outgoing call or primary call is changed
1092         // due to switching calls between two ongoing calls (one is on hold).
1093         if ((oldState != InCallState.OUTGOING && newState == InCallState.OUTGOING)
1094                 || (oldState != InCallState.INCOMING && newState == InCallState.INCOMING)
1095                 || primaryChanged) {
1096             if (getUi() != null) {
1097                 getUi().sendAccessibilityAnnouncement();
1098             }
1099         }
1100     }
1101 
1102     /**
1103      * Determines whether the call subject should be visible on the UI.  For the call subject to be
1104      * visible, the call has to be in an incoming or waiting state, and the subject must not be
1105      * empty.
1106      *
1107      * @param call The call.
1108      * @return {@code true} if the subject should be shown, {@code false} otherwise.
1109      */
shouldShowCallSubject(Call call)1110     private boolean shouldShowCallSubject(Call call) {
1111         if (call == null) {
1112             return false;
1113         }
1114 
1115         boolean isIncomingOrWaiting = mPrimary.getState() == Call.State.INCOMING ||
1116                 mPrimary.getState() == Call.State.CALL_WAITING;
1117         return isIncomingOrWaiting && !TextUtils.isEmpty(call.getCallSubject()) &&
1118                 call.getNumberPresentation() == TelecomManager.PRESENTATION_ALLOWED &&
1119                 call.isCallSubjectSupported();
1120     }
1121 
1122     /**
1123      * Determines whether the "note sent" toast should be shown.  It should be shown for a new
1124      * outgoing call with a subject.
1125      *
1126      * @param call The call
1127      * @return {@code true} if the toast should be shown, {@code false} otherwise.
1128      */
shouldShowNoteSentToast(Call call)1129     private boolean shouldShowNoteSentToast(Call call) {
1130         return call != null && hasCallSubject(call) && (call.getState() == Call.State.DIALING
1131                 || call.getState() == Call.State.CONNECTING);
1132     }
1133 
hasCallSubject(Call call)1134     private static boolean hasCallSubject(Call call) {
1135         return !TextUtils.isEmpty(call.getTelecomCall().getDetails().getIntentExtras()
1136                 .getString(TelecomManager.EXTRA_CALL_SUBJECT));
1137     }
1138 
1139     public interface CallCardUi extends Ui {
setVisible(boolean on)1140         void setVisible(boolean on);
setContactContextTitle(View listHeaderView)1141         void setContactContextTitle(View listHeaderView);
setContactContextContent(ListAdapter listAdapter)1142         void setContactContextContent(ListAdapter listAdapter);
showContactContext(boolean show)1143         void showContactContext(boolean show);
setCallCardVisible(boolean visible)1144         void setCallCardVisible(boolean visible);
setPrimary(String number, String name, boolean nameIsNumber, String label, Drawable photo, boolean isSipCall, boolean isContactPhotoShown, boolean isWorkCall)1145         void setPrimary(String number, String name, boolean nameIsNumber, String label,
1146                 Drawable photo, boolean isSipCall, boolean isContactPhotoShown, boolean isWorkCall);
setSecondary(boolean show, String name, boolean nameIsNumber, String label, String providerLabel, boolean isConference, boolean isVideoCall, boolean isFullscreen)1147         void setSecondary(boolean show, String name, boolean nameIsNumber, String label,
1148                 String providerLabel, boolean isConference, boolean isVideoCall,
1149                 boolean isFullscreen);
setSecondaryInfoVisible(boolean visible)1150         void setSecondaryInfoVisible(boolean visible);
setCallState(int state, int videoState, int sessionModificationState, DisconnectCause disconnectCause, String connectionLabel, Drawable connectionIcon, String gatewayNumber, boolean isWifi, boolean isConference, boolean isWorkCall)1151         void setCallState(int state, int videoState, int sessionModificationState,
1152                 DisconnectCause disconnectCause, String connectionLabel,
1153                 Drawable connectionIcon, String gatewayNumber, boolean isWifi,
1154                 boolean isConference, boolean isWorkCall);
setPrimaryCallElapsedTime(boolean show, long duration)1155         void setPrimaryCallElapsedTime(boolean show, long duration);
setPrimaryName(String name, boolean nameIsNumber)1156         void setPrimaryName(String name, boolean nameIsNumber);
setPrimaryImage(Drawable image, boolean isVisible)1157         void setPrimaryImage(Drawable image, boolean isVisible);
setPrimaryPhoneNumber(String phoneNumber)1158         void setPrimaryPhoneNumber(String phoneNumber);
setPrimaryLabel(String label)1159         void setPrimaryLabel(String label);
setEndCallButtonEnabled(boolean enabled, boolean animate)1160         void setEndCallButtonEnabled(boolean enabled, boolean animate);
setCallbackNumber(String number, boolean isEmergencyCalls)1161         void setCallbackNumber(String number, boolean isEmergencyCalls);
setCallSubject(String callSubject)1162         void setCallSubject(String callSubject);
setProgressSpinnerVisible(boolean visible)1163         void setProgressSpinnerVisible(boolean visible);
showHdAudioIndicator(boolean visible)1164         void showHdAudioIndicator(boolean visible);
showForwardIndicator(boolean visible)1165         void showForwardIndicator(boolean visible);
showManageConferenceCallButton(boolean visible)1166         void showManageConferenceCallButton(boolean visible);
isManageConferenceVisible()1167         boolean isManageConferenceVisible();
isCallSubjectVisible()1168         boolean isCallSubjectVisible();
animateForNewOutgoingCall()1169         void animateForNewOutgoingCall();
sendAccessibilityAnnouncement()1170         void sendAccessibilityAnnouncement();
showNoteSentToast()1171         void showNoteSentToast();
1172     }
1173 }
1174