Home | History | Annotate | Download | only in telecom
      1 /*
      2  * Copyright (C) 2015 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 package com.android.car.dialer.telecom;
     17 
     18 import android.content.ComponentName;
     19 import android.content.Context;
     20 import android.content.Intent;
     21 import android.content.ServiceConnection;
     22 import android.database.Cursor;
     23 import android.net.Uri;
     24 import android.os.IBinder;
     25 import android.provider.CallLog;
     26 import android.telecom.Call;
     27 import android.telecom.CallAudioState;
     28 import android.telecom.DisconnectCause;
     29 import android.telecom.GatewayInfo;
     30 import android.telecom.InCallService;
     31 import android.telecom.TelecomManager;
     32 import android.telephony.TelephonyManager;
     33 import android.text.TextUtils;
     34 import android.util.Log;
     35 import android.view.KeyEvent;
     36 
     37 import com.android.car.dialer.R;
     38 
     39 import java.lang.ref.WeakReference;
     40 import java.util.ArrayList;
     41 import java.util.Calendar;
     42 import java.util.Collections;
     43 import java.util.Comparator;
     44 import java.util.HashMap;
     45 import java.util.List;
     46 import java.util.Map;
     47 import java.util.concurrent.CopyOnWriteArrayList;
     48 
     49 /**
     50  * The entry point for all interactions between UI and telecom.
     51  */
     52 public class UiCallManager {
     53     private static String TAG = "Em.TelecomMgr";
     54 
     55     // Rate limit how often you can place outgoing calls.
     56     private static final long MIN_TIME_BETWEEN_CALLS_MS = 3000;
     57     private static final List<Integer> sCallStateRank = new ArrayList<>();
     58 
     59     // Used to assign id's to UiCall objects as they're created.
     60     private static int nextCarPhoneCallId = 0;
     61 
     62     static {
     63         // States should be added from lowest rank to highest
     64         sCallStateRank.add(Call.STATE_DISCONNECTED);
     65         sCallStateRank.add(Call.STATE_DISCONNECTING);
     66         sCallStateRank.add(Call.STATE_NEW);
     67         sCallStateRank.add(Call.STATE_CONNECTING);
     68         sCallStateRank.add(Call.STATE_SELECT_PHONE_ACCOUNT);
     69         sCallStateRank.add(Call.STATE_HOLDING);
     70         sCallStateRank.add(Call.STATE_ACTIVE);
     71         sCallStateRank.add(Call.STATE_DIALING);
     72         sCallStateRank.add(Call.STATE_RINGING);
     73     }
     74 
     75     private Context mContext;
     76     private TelephonyManager mTelephonyManager;
     77     private long mLastPlacedCallTimeMs;
     78 
     79     private TelecomManager mTelecomManager;
     80     private InCallServiceImpl mInCallService;
     81     private final Map<UiCall, Call> mCallMapping = new HashMap<>();
     82     private final List<CallListener> mCallListeners = new CopyOnWriteArrayList<>();
     83 
     84     public UiCallManager(Context context) {
     85         if (Log.isLoggable(TAG, Log.DEBUG)) {
     86             Log.d(TAG, "SetUp");
     87         }
     88 
     89         mContext = context;
     90         mTelephonyManager = (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
     91 
     92         mTelecomManager = (TelecomManager) context.getSystemService(Context.TELECOM_SERVICE);
     93         Intent intent = new Intent(context, InCallServiceImpl.class);
     94         intent.setAction(InCallServiceImpl.ACTION_LOCAL_BIND);
     95         context.bindService(intent, mInCallServiceConnection, Context.BIND_AUTO_CREATE);
     96     }
     97 
     98     private final ServiceConnection mInCallServiceConnection = new ServiceConnection() {
     99 
    100         @Override
    101         public void onServiceConnected(ComponentName name, IBinder binder) {
    102             if (Log.isLoggable(TAG, Log.DEBUG)) {
    103                 Log.d(TAG, "onServiceConnected: " + name + ", service: " + binder);
    104             }
    105             mInCallService = ((InCallServiceImpl.LocalBinder) binder).getService();
    106             mInCallService.registerCallback(mInCallServiceCallback);
    107 
    108             // The InCallServiceImpl could be bound when we already have some active calls, let's
    109             // notify UI about these calls.
    110             for (Call telecomCall : mInCallService.getCalls()) {
    111                 UiCall uiCall = doTelecomCallAdded(telecomCall);
    112                 onStateChanged(uiCall, uiCall.getState());
    113             }
    114         }
    115 
    116         @Override
    117         public void onServiceDisconnected(ComponentName name) {
    118             if (Log.isLoggable(TAG, Log.DEBUG)) {
    119                 Log.d(TAG, "onServiceDisconnected: " + name);
    120             }
    121             mInCallService.unregisterCallback(mInCallServiceCallback);
    122         }
    123 
    124         private InCallServiceImpl.Callback mInCallServiceCallback =
    125                 new InCallServiceImpl.Callback() {
    126                     @Override
    127                     public void onTelecomCallAdded(Call telecomCall) {
    128                         doTelecomCallAdded(telecomCall);
    129                     }
    130 
    131                     @Override
    132                     public void onTelecomCallRemoved(Call telecomCall) {
    133                         doTelecomCallRemoved(telecomCall);
    134                     }
    135 
    136                     @Override
    137                     public void onCallAudioStateChanged(CallAudioState audioState) {
    138                         doCallAudioStateChanged(audioState);
    139                     }
    140                 };
    141     };
    142 
    143     public void tearDown() {
    144         if (mInCallService != null) {
    145             mContext.unbindService(mInCallServiceConnection);
    146             mInCallService = null;
    147         }
    148         mCallMapping.clear();
    149     }
    150 
    151     public void addListener(CallListener listener) {
    152         if (Log.isLoggable(TAG, Log.DEBUG)) {
    153             Log.d(TAG, "addListener: " + listener);
    154         }
    155         mCallListeners.add(listener);
    156     }
    157 
    158     public void removeListener(CallListener listener) {
    159         if (Log.isLoggable(TAG, Log.DEBUG)) {
    160             Log.d(TAG, "removeListener: " + listener);
    161         }
    162         mCallListeners.remove(listener);
    163     }
    164 
    165     protected void placeCall(String number) {
    166         if (Log.isLoggable(TAG, Log.DEBUG)) {
    167             Log.d(TAG, "placeCall: " + number);
    168         }
    169         Uri uri = Uri.fromParts("tel", number, null);
    170         Log.d(TAG, "android.telecom.TelecomManager#placeCall: " + uri);
    171         mTelecomManager.placeCall(uri, null);
    172     }
    173 
    174     public void answerCall(UiCall uiCall) {
    175         if (Log.isLoggable(TAG, Log.DEBUG)) {
    176             Log.d(TAG, "answerCall: " + uiCall);
    177         }
    178 
    179         Call telecomCall = mCallMapping.get(uiCall);
    180         if (telecomCall != null) {
    181             telecomCall.answer(0);
    182         }
    183     }
    184 
    185     public void rejectCall(UiCall uiCall, boolean rejectWithMessage, String textMessage) {
    186         if (Log.isLoggable(TAG, Log.DEBUG)) {
    187             Log.d(TAG, "rejectCall: " + uiCall + ", rejectWithMessage: " + rejectWithMessage
    188                     + "textMessage: " + textMessage);
    189         }
    190 
    191         Call telecomCall = mCallMapping.get(uiCall);
    192         if (telecomCall != null) {
    193             telecomCall.reject(rejectWithMessage, textMessage);
    194         }
    195     }
    196 
    197     public void disconnectCall(UiCall uiCall) {
    198         if (Log.isLoggable(TAG, Log.DEBUG)) {
    199             Log.d(TAG, "disconnectCall: " + uiCall);
    200         }
    201 
    202         Call telecomCall = mCallMapping.get(uiCall);
    203         if (telecomCall != null) {
    204             telecomCall.disconnect();
    205         }
    206     }
    207 
    208     public List<UiCall> getCalls() {
    209         return new ArrayList<>(mCallMapping.keySet());
    210     }
    211 
    212     public boolean getMuted() {
    213         if (Log.isLoggable(TAG, Log.DEBUG)) {
    214             Log.d(TAG, "getMuted");
    215         }
    216         if (mInCallService == null) {
    217             return false;
    218         }
    219         CallAudioState audioState = mInCallService.getCallAudioState();
    220         return audioState != null && audioState.isMuted();
    221     }
    222 
    223     public void setMuted(boolean muted) {
    224         if (Log.isLoggable(TAG, Log.DEBUG)) {
    225             Log.d(TAG, "setMuted: " + muted);
    226         }
    227         if (mInCallService == null) {
    228             return;
    229         }
    230         mInCallService.setMuted(muted);
    231     }
    232 
    233     public int getSupportedAudioRouteMask() {
    234         if (Log.isLoggable(TAG, Log.DEBUG)) {
    235             Log.d(TAG, "getSupportedAudioRouteMask");
    236         }
    237 
    238         CallAudioState audioState = getCallAudioStateOrNull();
    239         return audioState != null ? audioState.getSupportedRouteMask() : 0;
    240     }
    241 
    242     public int getAudioRoute() {
    243         CallAudioState audioState = getCallAudioStateOrNull();
    244         int audioRoute = audioState != null ? audioState.getRoute() : 0;
    245         if (Log.isLoggable(TAG, Log.DEBUG)) {
    246             Log.d(TAG, "getAudioRoute " + audioRoute);
    247         }
    248         return audioRoute;
    249     }
    250 
    251     public void setAudioRoute(int audioRoute) {
    252         // In case of embedded where the CarKitt is always connected to one kind of speaker we
    253         // should simply ignore any setAudioRoute requests.
    254         Log.w(TAG, "setAudioRoute ignoring request " + audioRoute);
    255     }
    256 
    257     public void holdCall(UiCall uiCall) {
    258         if (Log.isLoggable(TAG, Log.DEBUG)) {
    259             Log.d(TAG, "holdCall: " + uiCall);
    260         }
    261 
    262         Call telecomCall = mCallMapping.get(uiCall);
    263         if (telecomCall != null) {
    264             telecomCall.hold();
    265         }
    266     }
    267 
    268     public void unholdCall(UiCall uiCall) {
    269         if (Log.isLoggable(TAG, Log.DEBUG)) {
    270             Log.d(TAG, "unholdCall: " + uiCall);
    271         }
    272 
    273         Call telecomCall = mCallMapping.get(uiCall);
    274         if (telecomCall != null) {
    275             telecomCall.unhold();
    276         }
    277     }
    278 
    279     public void playDtmfTone(UiCall uiCall, char digit) {
    280         if (Log.isLoggable(TAG, Log.DEBUG)) {
    281             Log.d(TAG, "playDtmfTone: call: " + uiCall + ", digit: " + digit);
    282         }
    283 
    284         Call telecomCall = mCallMapping.get(uiCall);
    285         if (telecomCall != null) {
    286             telecomCall.playDtmfTone(digit);
    287         }
    288     }
    289 
    290     public void stopDtmfTone(UiCall uiCall) {
    291         if (Log.isLoggable(TAG, Log.DEBUG)) {
    292             Log.d(TAG, "stopDtmfTone: call: " + uiCall);
    293         }
    294 
    295         Call telecomCall = mCallMapping.get(uiCall);
    296         if (telecomCall != null) {
    297             telecomCall.stopDtmfTone();
    298         }
    299     }
    300 
    301     public void postDialContinue(UiCall uiCall, boolean proceed) {
    302         if (Log.isLoggable(TAG, Log.DEBUG)) {
    303             Log.d(TAG, "postDialContinue: call: " + uiCall + ", proceed: " + proceed);
    304         }
    305 
    306         Call telecomCall = mCallMapping.get(uiCall);
    307         if (telecomCall != null) {
    308             telecomCall.postDialContinue(proceed);
    309         }
    310     }
    311 
    312     public void conference(UiCall uiCall, UiCall otherUiCall) {
    313         if (Log.isLoggable(TAG, Log.DEBUG)) {
    314             Log.d(TAG, "conference: call: " + uiCall + ", otherCall: " + otherUiCall);
    315         }
    316 
    317         Call telecomCall = mCallMapping.get(uiCall);
    318         Call otherTelecomCall = mCallMapping.get(otherUiCall);
    319         if (telecomCall != null) {
    320             telecomCall.conference(otherTelecomCall);
    321         }
    322     }
    323 
    324     public void splitFromConference(UiCall uiCall) {
    325         if (Log.isLoggable(TAG, Log.DEBUG)) {
    326             Log.d(TAG, "splitFromConference: call: " + uiCall);
    327         }
    328 
    329         Call telecomCall = mCallMapping.get(uiCall);
    330         if (telecomCall != null) {
    331             telecomCall.splitFromConference();
    332         }
    333     }
    334 
    335     private UiCall doTelecomCallAdded(final Call telecomCall) {
    336         Log.d(TAG, "doTelecomCallAdded: " + telecomCall);
    337 
    338         UiCall uiCall = getOrCreateCallContainer(telecomCall);
    339         telecomCall.registerCallback(new TelecomCallListener(this, uiCall));
    340         for (CallListener listener : mCallListeners) {
    341             listener.onCallAdded(uiCall);
    342         }
    343         Log.d(TAG, "Call backs registered");
    344 
    345         if (telecomCall.getState() == Call.STATE_SELECT_PHONE_ACCOUNT) {
    346             // TODO(b/26189994): need to show Phone Account picker to let user choose a phone
    347             // account. It should be an account from TelecomManager#getCallCapablePhoneAccounts
    348             // list.
    349             Log.w(TAG, "Need to select phone account for the given call: " + telecomCall + ", "
    350                     + "but this feature is not implemented yet.");
    351             telecomCall.disconnect();
    352         }
    353         return uiCall;
    354     }
    355 
    356     private void doTelecomCallRemoved(Call telecomCall) {
    357         UiCall uiCall = getOrCreateCallContainer(telecomCall);
    358 
    359         mCallMapping.remove(uiCall);
    360 
    361         for (CallListener listener : mCallListeners) {
    362             listener.onCallRemoved(uiCall);
    363         }
    364     }
    365 
    366     private void doCallAudioStateChanged(CallAudioState audioState) {
    367         for (CallListener listener : mCallListeners) {
    368             listener.onAudioStateChanged(audioState.isMuted(), audioState.getRoute(),
    369                     audioState.getSupportedRouteMask());
    370         }
    371     }
    372 
    373     private void onStateChanged(UiCall uiCall, int state) {
    374         for (CallListener listener : mCallListeners) {
    375             listener.onStateChanged(uiCall, state);
    376         }
    377     }
    378 
    379     private void onCallUpdated(UiCall uiCall) {
    380         for (CallListener listener : mCallListeners) {
    381             listener.onCallUpdated(uiCall);
    382         }
    383     }
    384 
    385     private UiCall getOrCreateCallContainer(Call telecomCall) {
    386         for (Map.Entry<UiCall, Call> entry : mCallMapping.entrySet()) {
    387             if (entry.getValue() == telecomCall) {
    388                 return entry.getKey();
    389             }
    390         }
    391 
    392         UiCall uiCall = new UiCall(nextCarPhoneCallId++);
    393         updateCallContainerFromTelecom(uiCall, telecomCall);
    394         mCallMapping.put(uiCall, telecomCall);
    395         return uiCall;
    396     }
    397 
    398     private static void updateCallContainerFromTelecom(UiCall uiCall, Call telecomCall) {
    399         if (Log.isLoggable(TAG, Log.DEBUG)) {
    400             Log.d(TAG, "updateCallContainerFromTelecom: call: " + uiCall + ", telecomCall: "
    401                     + telecomCall);
    402         }
    403 
    404         uiCall.setState(telecomCall.getState());
    405         uiCall.setHasChildren(!telecomCall.getChildren().isEmpty());
    406         uiCall.setHasParent(telecomCall.getParent() != null);
    407 
    408         Call.Details details = telecomCall.getDetails();
    409         if (details == null) {
    410             return;
    411         }
    412 
    413         uiCall.setConnectTimeMillis(details.getConnectTimeMillis());
    414 
    415         DisconnectCause cause = details.getDisconnectCause();
    416         uiCall.setDisconnectCause(cause == null ? null : cause.getLabel());
    417 
    418         GatewayInfo gatewayInfo = details.getGatewayInfo();
    419         uiCall.setGatewayInfoOriginalAddress(
    420                 gatewayInfo == null ? null : gatewayInfo.getOriginalAddress());
    421 
    422         String number = "";
    423         if (gatewayInfo != null) {
    424             number = gatewayInfo.getOriginalAddress().getSchemeSpecificPart();
    425         } else if (details.getHandle() != null) {
    426             number = details.getHandle().getSchemeSpecificPart();
    427         }
    428         uiCall.setNumber(number);
    429     }
    430 
    431     private CallAudioState getCallAudioStateOrNull() {
    432         return mInCallService != null ? mInCallService.getCallAudioState() : null;
    433     }
    434 
    435     public static class CallListener {
    436         @SuppressWarnings("unused")
    437         public void dispatchPhoneKeyEvent(KeyEvent event) {}
    438         @SuppressWarnings("unused")
    439         public void onAudioStateChanged(boolean isMuted, int route, int supportedRouteMask) {}
    440         @SuppressWarnings("unused")
    441         public void onCallAdded(UiCall call) {}
    442         @SuppressWarnings("unused")
    443         public void onStateChanged(UiCall call, int state) {}
    444         @SuppressWarnings("unused")
    445         public void onCallUpdated(UiCall call) {}
    446         @SuppressWarnings("unused")
    447         public void onCallRemoved(UiCall call) {}
    448     }
    449 
    450     /** Returns a first call that matches at least one provided call state */
    451     public UiCall getCallWithState(int... callStates) {
    452         if (Log.isLoggable(TAG, Log.DEBUG)) {
    453             Log.d(TAG, "getCallWithState: " + callStates);
    454         }
    455         for (UiCall call : getCalls()) {
    456             for (int callState : callStates) {
    457                 if (call.getState() == callState) {
    458                     return call;
    459                 }
    460             }
    461         }
    462         return null;
    463     }
    464 
    465     public UiCall getPrimaryCall() {
    466         if (Log.isLoggable(TAG, Log.DEBUG)) {
    467             Log.d(TAG, "getPrimaryCall");
    468         }
    469         List<UiCall> calls = getCalls();
    470         if (calls.isEmpty()) {
    471             return null;
    472         }
    473 
    474         Collections.sort(calls, getCallComparator());
    475         UiCall uiCall = calls.get(0);
    476         if (uiCall.hasParent()) {
    477             return null;
    478         }
    479         return uiCall;
    480     }
    481 
    482     public UiCall getSecondaryCall() {
    483         if (Log.isLoggable(TAG, Log.DEBUG)) {
    484             Log.d(TAG, "getSecondaryCall");
    485         }
    486         List<UiCall> calls = getCalls();
    487         if (calls.size() < 2) {
    488             return null;
    489         }
    490 
    491         Collections.sort(calls, getCallComparator());
    492         UiCall uiCall = calls.get(1);
    493         if (uiCall.hasParent()) {
    494             return null;
    495         }
    496         return uiCall;
    497     }
    498 
    499     public static final int CAN_PLACE_CALL_RESULT_OK = 0;
    500     public static final int CAN_PLACE_CALL_RESULT_NETWORK_UNAVAILABLE = 1;
    501     public static final int CAN_PLACE_CALL_RESULT_HFP_UNAVAILABLE = 2;
    502     public static final int CAN_PLACE_CALL_RESULT_AIRPLANE_MODE = 3;
    503 
    504     public int getCanPlaceCallStatus(String number, boolean bluetoothRequired) {
    505         // TODO(b/26191392): figure out the logic for projected and embedded modes
    506         return CAN_PLACE_CALL_RESULT_OK;
    507     }
    508 
    509     public String getFailToPlaceCallMessage(int canPlaceCallResult) {
    510         switch (canPlaceCallResult) {
    511             case CAN_PLACE_CALL_RESULT_OK:
    512                 return "";
    513             case CAN_PLACE_CALL_RESULT_HFP_UNAVAILABLE:
    514                 return mContext.getString(R.string.error_no_hfp);
    515             case CAN_PLACE_CALL_RESULT_AIRPLANE_MODE:
    516                 return mContext.getString(R.string.error_airplane_mode);
    517             case CAN_PLACE_CALL_RESULT_NETWORK_UNAVAILABLE:
    518             default:
    519                 return mContext.getString(R.string.error_network_not_available);
    520         }
    521     }
    522 
    523     /** Places call only if there's no outgoing call right now */
    524     public void safePlaceCall(String number, boolean bluetoothRequired) {
    525         if (Log.isLoggable(TAG, Log.DEBUG)) {
    526             Log.d(TAG, "safePlaceCall: " + number);
    527         }
    528 
    529         int placeCallStatus = getCanPlaceCallStatus(number, bluetoothRequired);
    530         if (placeCallStatus != CAN_PLACE_CALL_RESULT_OK) {
    531             if (Log.isLoggable(TAG, Log.DEBUG)) {
    532                 Log.d(TAG, "Unable to place a call: " + placeCallStatus);
    533             }
    534             return;
    535         }
    536 
    537         UiCall outgoingCall = getCallWithState(
    538                 Call.STATE_CONNECTING, Call.STATE_NEW, Call.STATE_DIALING);
    539         if (outgoingCall == null) {
    540             long now = Calendar.getInstance().getTimeInMillis();
    541             if (now - mLastPlacedCallTimeMs > MIN_TIME_BETWEEN_CALLS_MS) {
    542                 placeCall(number);
    543                 mLastPlacedCallTimeMs = now;
    544             } else {
    545                 if (Log.isLoggable(TAG, Log.INFO)) {
    546                     Log.i(TAG, "You have to wait " + MIN_TIME_BETWEEN_CALLS_MS
    547                             + "ms between making calls");
    548                 }
    549             }
    550         }
    551     }
    552 
    553     public void callVoicemail() {
    554         if (Log.isLoggable(TAG, Log.DEBUG)) {
    555             Log.d(TAG, "callVoicemail");
    556         }
    557 
    558         String voicemailNumber = TelecomUtils.getVoicemailNumber(mContext);
    559         if (TextUtils.isEmpty(voicemailNumber)) {
    560             Log.w(TAG, "Unable to get voicemail number.");
    561             return;
    562         }
    563         safePlaceCall(voicemailNumber, false);
    564     }
    565 
    566     /**
    567      * Returns the call types for the given number of items in the cursor.
    568      * <p/>
    569      * It uses the next {@code count} rows in the cursor to extract the types.
    570      * <p/>
    571      * Its position in the cursor is unchanged by this function.
    572      */
    573     public int[] getCallTypes(Cursor cursor, int count) {
    574         if (Log.isLoggable(TAG, Log.DEBUG)) {
    575             Log.d(TAG, "getCallTypes: cursor: " + cursor + ", count: " + count);
    576         }
    577 
    578         int position = cursor.getPosition();
    579         int[] callTypes = new int[count];
    580         String voicemailNumber = mTelephonyManager.getVoiceMailNumber();
    581         int column;
    582         for (int index = 0; index < count; ++index) {
    583             column = cursor.getColumnIndex(CallLog.Calls.NUMBER);
    584             String phoneNumber = cursor.getString(column);
    585             if (phoneNumber != null && phoneNumber.equals(voicemailNumber)) {
    586                 callTypes[index] = PhoneLoader.VOICEMAIL_TYPE;
    587             } else {
    588                 column = cursor.getColumnIndex(CallLog.Calls.TYPE);
    589                 callTypes[index] = cursor.getInt(column);
    590             }
    591             cursor.moveToNext();
    592         }
    593         cursor.moveToPosition(position);
    594         return callTypes;
    595     }
    596 
    597     private static Comparator<UiCall> getCallComparator() {
    598         return new Comparator<UiCall>() {
    599             @Override
    600             public int compare(UiCall call, UiCall otherCall) {
    601                 if (call.hasParent() && !otherCall.hasParent()) {
    602                     return 1;
    603                 } else if (!call.hasParent() && otherCall.hasParent()) {
    604                     return -1;
    605                 }
    606                 int carCallRank = sCallStateRank.indexOf(call.getState());
    607                 int otherCarCallRank = sCallStateRank.indexOf(otherCall.getState());
    608 
    609                 return otherCarCallRank - carCallRank;
    610             }
    611         };
    612     }
    613 
    614     private static class TelecomCallListener extends Call.Callback {
    615         private final WeakReference<UiCallManager> mCarTelecomMangerRef;
    616         private final WeakReference<UiCall> mCallContainerRef;
    617 
    618         TelecomCallListener(UiCallManager carTelecomManager, UiCall uiCall) {
    619             mCarTelecomMangerRef = new WeakReference<>(carTelecomManager);
    620             mCallContainerRef = new WeakReference<>(uiCall);
    621         }
    622 
    623         @Override
    624         public void onStateChanged(Call telecomCall, int state) {
    625             if (Log.isLoggable(TAG, Log.DEBUG)) {
    626                 Log.d(TAG, "onStateChanged: " + state);
    627             }
    628             UiCallManager manager = mCarTelecomMangerRef.get();
    629             UiCall call = mCallContainerRef.get();
    630             if (manager != null && call != null) {
    631                 call.setState(state);
    632                 manager.onStateChanged(call, state);
    633             }
    634         }
    635 
    636         @Override
    637         public void onParentChanged(Call telecomCall, Call parent) {
    638             doCallUpdated(telecomCall);
    639         }
    640 
    641         @Override
    642         public void onCallDestroyed(Call telecomCall) {
    643             if (Log.isLoggable(TAG, Log.DEBUG)) {
    644                 Log.d(TAG, "onCallDestroyed");
    645             }
    646         }
    647 
    648         @Override
    649         public void onDetailsChanged(Call telecomCall, Call.Details details) {
    650             doCallUpdated(telecomCall);
    651         }
    652 
    653         @Override
    654         public void onVideoCallChanged(Call telecomCall, InCallService.VideoCall videoCall) {
    655             doCallUpdated(telecomCall);
    656         }
    657 
    658         @Override
    659         public void onCannedTextResponsesLoaded(Call telecomCall,
    660                 List<String> cannedTextResponses) {
    661             doCallUpdated(telecomCall);
    662         }
    663 
    664         @Override
    665         public void onChildrenChanged(Call telecomCall, List<Call> children) {
    666             doCallUpdated(telecomCall);
    667         }
    668 
    669         private void doCallUpdated(Call telecomCall) {
    670             UiCallManager manager = mCarTelecomMangerRef.get();
    671             UiCall uiCall = mCallContainerRef.get();
    672             if (manager != null && uiCall != null) {
    673                 updateCallContainerFromTelecom(uiCall, telecomCall);
    674                 manager.onCallUpdated(uiCall);
    675             }
    676         }
    677     }
    678 }
    679