• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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      */
init(Context applicationContext)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      */
get()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 
UiCallManager(Context context)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      */
tearDown()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 
addListener(CallListener listener)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 
removeListener(CallListener listener)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 
placeCall(String number)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 
answerCall(UiCall uiCall)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 
rejectCall(UiCall uiCall, boolean rejectWithMessage, String textMessage)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 
disconnectCall(UiCall uiCall)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 
getCalls()269     public List<UiCall> getCalls() {
270         return new ArrayList<>(mCallMapping.keySet());
271     }
272 
getMuted()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 
setMuted(boolean muted)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 
getSupportedAudioRouteMask()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 
getSupportedAudioRoute()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 
isBluetoothCall()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 
getAudioRoute()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      */
setAudioRoute(@allAudioRoute int audioRoute)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 
holdCall(UiCall uiCall)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 
unholdCall(UiCall uiCall)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 
playDtmfTone(UiCall uiCall, char digit)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 
stopDtmfTone(UiCall uiCall)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 
postDialContinue(UiCall uiCall, boolean proceed)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 
conference(UiCall uiCall, UiCall otherUiCall)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 
splitFromConference(UiCall uiCall)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 
doTelecomCallAdded(final Call telecomCall)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 
doTelecomCallRemoved(Call telecomCall)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 
doCallAudioStateChanged(CallAudioState audioState)481     private void doCallAudioStateChanged(CallAudioState audioState) {
482         for (CallListener listener : mCallListeners) {
483             listener.onAudioStateChanged(audioState.isMuted(), audioState.getRoute(),
484                     audioState.getSupportedRouteMask());
485         }
486     }
487 
onStateChanged(UiCall uiCall, int state)488     private void onStateChanged(UiCall uiCall, int state) {
489         for (CallListener listener : mCallListeners) {
490             listener.onCallStateChanged(uiCall, state);
491         }
492     }
493 
onCallUpdated(UiCall uiCall)494     private void onCallUpdated(UiCall uiCall) {
495         for (CallListener listener : mCallListeners) {
496             listener.onCallUpdated(uiCall);
497         }
498     }
499 
getOrCreateCallContainer(Call telecomCall)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 
updateCallContainerFromTelecom(UiCall uiCall, Call telecomCall)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 
getCallAudioStateOrNull()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 */
getCallWithState(int... callStates)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 
getPrimaryCall()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 
getSecondaryCall()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 
getCanPlaceCallStatus(String number, boolean bluetoothRequired)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 
getFailToPlaceCallMessage(int canPlaceCallResult)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 */
safePlaceCall(String number, boolean bluetoothRequired)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 
callVoicemail()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      */
getCallTypes(Cursor cursor, int count)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 
getCallComparator()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