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