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 */
16package com.android.car.dialer.telecom;
17
18import android.bluetooth.BluetoothAdapter;
19import android.bluetooth.BluetoothDevice;
20import android.bluetooth.BluetoothHeadsetClient;
21import android.bluetooth.BluetoothHeadsetClientCall;
22import android.bluetooth.BluetoothProfile;
23import android.content.ComponentName;
24import android.content.Context;
25import android.content.Intent;
26import android.content.ServiceConnection;
27import android.database.Cursor;
28import android.net.Uri;
29import android.os.IBinder;
30import android.provider.CallLog;
31import android.telecom.Call;
32import android.telecom.CallAudioState;
33import android.telecom.CallAudioState.CallAudioRoute;
34import android.telecom.DisconnectCause;
35import android.telecom.GatewayInfo;
36import android.telecom.InCallService;
37import android.telecom.PhoneAccountHandle;
38import android.telecom.TelecomManager;
39import android.telephony.TelephonyManager;
40import android.text.TextUtils;
41import android.util.Log;
42
43import com.android.car.dialer.CallListener;
44import com.android.car.dialer.R;
45
46import java.lang.ref.WeakReference;
47import java.util.ArrayList;
48import java.util.Calendar;
49import java.util.Collections;
50import java.util.Comparator;
51import java.util.HashMap;
52import java.util.List;
53import java.util.Map;
54import java.util.concurrent.CopyOnWriteArrayList;
55
56/**
57 * The entry point for all interactions between UI and telecom.
58 */
59public 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