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.embedded;
17 
18 import com.android.car.dialer.telecom.UiCall;
19 import com.android.car.dialer.telecom.UiCallManager;
20 
21 import android.content.ComponentName;
22 import android.content.Context;
23 import android.content.Intent;
24 import android.content.ServiceConnection;
25 import android.net.Uri;
26 import android.os.IBinder;
27 import android.telecom.Call;
28 import android.telecom.Call.Details;
29 import android.telecom.CallAudioState;
30 import android.telecom.InCallService.VideoCall;
31 import android.telecom.TelecomManager;
32 import android.util.Log;
33 
34 import java.lang.ref.WeakReference;
35 import java.util.List;
36 import java.util.concurrent.CopyOnWriteArrayList;
37 
38 /**
39  * An implementation of {@link UiCallManager} that uses {@code android.telecom.*} stack.
40  */
41 public class TelecomUiCallManager extends UiCallManager {
42 
43     private static final String TAG = "Em.TelecomMgrImpl";
44 
45     private TelecomManager mTelecomManager;
46     private InCallServiceImpl mInCallService;
47     private TelecomUiCallList mCallList = new TelecomUiCallList();
48 
49     private List<CallListener> mCallListeners = new CopyOnWriteArrayList<>();
50 
51     @Override
setUp(Context context)52     protected void setUp(Context context) {
53         super.setUp(context);
54         mTelecomManager = (TelecomManager) context.getSystemService(Context.TELECOM_SERVICE);
55         Intent intent = new Intent(context, InCallServiceImpl.class);
56         intent.setAction(InCallServiceImpl.ACTION_LOCAL_BIND);
57         context.bindService(intent, mInCallServiceConnection, Context.BIND_AUTO_CREATE);
58     }
59 
tearDown()60     public void tearDown() {
61         if (mInCallService != null) {
62             mContext.unbindService(mInCallServiceConnection);
63             mInCallService = null;
64         }
65         mCallList.clearCalls();
66     }
67 
68     @Override
addListener(CallListener listener)69     public void addListener(CallListener listener) {
70         if (Log.isLoggable(TAG, Log.DEBUG)) {
71             Log.d(TAG, "addListener: " + listener);
72         }
73         mCallListeners.add(listener);
74     }
75 
76     @Override
removeListener(CallListener listener)77     public void removeListener(CallListener listener) {
78         if (Log.isLoggable(TAG, Log.DEBUG)) {
79             Log.d(TAG, "removeListener: " + listener);
80         }
81         mCallListeners.remove(listener);
82     }
83 
84     @Override
placeCall(String number)85     public void placeCall(String number) {
86         if (Log.isLoggable(TAG, Log.DEBUG)) {
87             Log.d(TAG, "placeCall: " + number);
88         }
89         Uri uri = Uri.fromParts("tel", number, null);
90         Log.d(TAG, "android.telecom.TelecomManager#placeCall: " + uri);
91         mTelecomManager.placeCall(uri, null);
92     }
93 
94     @Override
answerCall(UiCall uiCall)95     public void answerCall(UiCall uiCall) {
96         if (Log.isLoggable(TAG, Log.DEBUG)) {
97             Log.d(TAG, "answerCall: " + uiCall);
98         }
99 
100         Call telecomCall = mCallList.getTelecomCall(uiCall);
101         if (telecomCall != null) {
102             telecomCall.answer(0);
103         }
104     }
105 
106     @Override
rejectCall(UiCall uiCall, boolean rejectWithMessage, String textMessage)107     public void rejectCall(UiCall uiCall, boolean rejectWithMessage, String textMessage) {
108         if (Log.isLoggable(TAG, Log.DEBUG)) {
109             Log.d(TAG, "rejectCall: " + uiCall + ", rejectWithMessage: " + rejectWithMessage
110                     + "textMessage: " + textMessage);
111         }
112 
113         Call telecomCall = mCallList.getTelecomCall(uiCall);
114         if (telecomCall != null) {
115             telecomCall.reject(rejectWithMessage, textMessage);
116         }
117     }
118 
119     @Override
disconnectCall(UiCall uiCall)120     public void disconnectCall(UiCall uiCall) {
121         if (Log.isLoggable(TAG, Log.DEBUG)) {
122             Log.d(TAG, "disconnectCall: " + uiCall);
123         }
124 
125         Call telecomCall = mCallList.getTelecomCall(uiCall);
126         if (telecomCall != null) {
127             telecomCall.disconnect();
128         }
129     }
130 
131     @Override
getCalls()132     public List<UiCall> getCalls() {
133         return mCallList.getCalls();
134     }
135 
136     @Override
getMuted()137     public boolean getMuted() {
138         if (Log.isLoggable(TAG, Log.DEBUG)) {
139             Log.d(TAG, "getMuted");
140         }
141         if (mInCallService == null) {
142             return false;
143         }
144         CallAudioState audioState = mInCallService.getCallAudioState();
145         return audioState != null && audioState.isMuted();
146     }
147 
148     @Override
setMuted(boolean muted)149     public void setMuted(boolean muted) {
150         if (Log.isLoggable(TAG, Log.DEBUG)) {
151             Log.d(TAG, "setMuted: " + muted);
152         }
153         if (mInCallService == null) {
154             return;
155         }
156         mInCallService.setMuted(muted);
157     }
158 
159     @Override
getSupportedAudioRouteMask()160     public int getSupportedAudioRouteMask() {
161         if (Log.isLoggable(TAG, Log.DEBUG)) {
162             Log.d(TAG, "getSupportedAudioRouteMask");
163         }
164 
165         CallAudioState audioState = getCallAudioStateOrNull();
166         return audioState != null ? audioState.getSupportedRouteMask() : 0;
167     }
168 
169     @Override
getAudioRoute()170     public int getAudioRoute() {
171         CallAudioState audioState = getCallAudioStateOrNull();
172         int audioRoute = audioState != null ? audioState.getRoute() : 0;
173         if (Log.isLoggable(TAG, Log.DEBUG)) {
174             Log.d(TAG, "getAudioRoute " + audioRoute);
175         }
176         return audioRoute;
177     }
178 
179     @Override
setAudioRoute(int audioRoute)180     public void setAudioRoute(int audioRoute) {
181         // In case of embedded where the CarKitt is always connected to one kind of speaker we
182         // should simply ignore any setAudioRoute requests.
183         Log.w(TAG, "setAudioRoute ignoring request " + audioRoute);
184     }
185 
186     @Override
holdCall(UiCall uiCall)187     public void holdCall(UiCall uiCall) {
188         if (Log.isLoggable(TAG, Log.DEBUG)) {
189             Log.d(TAG, "holdCall: " + uiCall);
190         }
191 
192         Call telecomCall = mCallList.getTelecomCall(uiCall);
193         if (telecomCall != null) {
194             telecomCall.hold();
195         }
196     }
197 
198     @Override
unholdCall(UiCall uiCall)199     public void unholdCall(UiCall uiCall) {
200         if (Log.isLoggable(TAG, Log.DEBUG)) {
201             Log.d(TAG, "unholdCall: " + uiCall);
202         }
203 
204         Call telecomCall = mCallList.getTelecomCall(uiCall);
205         if (telecomCall != null) {
206             telecomCall.unhold();
207         }
208     }
209 
210     @Override
playDtmfTone(UiCall uiCall, char digit)211     public void playDtmfTone(UiCall uiCall, char digit) {
212         if (Log.isLoggable(TAG, Log.DEBUG)) {
213             Log.d(TAG, "playDtmfTone: call: " + uiCall + ", digit: " + digit);
214         }
215 
216         Call telecomCall = mCallList.getTelecomCall(uiCall);
217         if (telecomCall != null) {
218             telecomCall.playDtmfTone(digit);
219         }
220     }
221 
222     @Override
stopDtmfTone(UiCall uiCall)223     public void stopDtmfTone(UiCall uiCall) {
224         if (Log.isLoggable(TAG, Log.DEBUG)) {
225             Log.d(TAG, "stopDtmfTone: call: " + uiCall);
226         }
227 
228         Call telecomCall = mCallList.getTelecomCall(uiCall);
229         if (telecomCall != null) {
230             telecomCall.stopDtmfTone();
231         }
232     }
233 
234     @Override
postDialContinue(UiCall uiCall, boolean proceed)235     public void postDialContinue(UiCall uiCall, boolean proceed) {
236         if (Log.isLoggable(TAG, Log.DEBUG)) {
237             Log.d(TAG, "postDialContinue: call: " + uiCall + ", proceed: " + proceed);
238         }
239 
240         Call telecomCall = mCallList.getTelecomCall(uiCall);
241         if (telecomCall != null) {
242             telecomCall.postDialContinue(proceed);
243         }
244     }
245 
246     @Override
conference(UiCall uiCall, UiCall otherUiCall)247     public void conference(UiCall uiCall, UiCall otherUiCall) {
248         if (Log.isLoggable(TAG, Log.DEBUG)) {
249             Log.d(TAG, "conference: call: " + uiCall + ", otherCall: " + otherUiCall);
250         }
251 
252         Call telecomCall = mCallList.getTelecomCall(uiCall);
253         Call otherTelecomCall = mCallList.getTelecomCall(otherUiCall);
254         if (telecomCall != null) {
255             telecomCall.conference(otherTelecomCall);
256         }
257     }
258 
259     @Override
splitFromConference(UiCall uiCall)260     public void splitFromConference(UiCall uiCall) {
261         if (Log.isLoggable(TAG, Log.DEBUG)) {
262             Log.d(TAG, "splitFromConference: call: " + uiCall);
263         }
264 
265         Call telecomCall = mCallList.getTelecomCall(uiCall);
266         if (telecomCall != null) {
267             telecomCall.splitFromConference();
268         }
269     }
270 
doTelecomCallAdded(final Call telecomCall)271     private UiCall doTelecomCallAdded(final Call telecomCall) {
272         Log.d(TAG, "doTelecomCallAdded: " + telecomCall);
273 
274         UiCall uiCall = getOrCreateCallContainer(telecomCall);
275         telecomCall.registerCallback(new TelecomCallListener(this, uiCall));
276         for (CallListener listener : mCallListeners) {
277             listener.onCallAdded(uiCall);
278         }
279         Log.d(TAG, "Call backs registered");
280 
281         if (telecomCall.getState() == Call.STATE_SELECT_PHONE_ACCOUNT) {
282             // TODO(b/26189994): need to show Phone Account picker to let user choose a phone
283             // account. It should be an account from TelecomManager#getCallCapablePhoneAccounts
284             // list.
285             Log.w(TAG, "Need to select phone account for the given call: " + telecomCall + ", "
286                     + "but this feature is not implemented yet.");
287             telecomCall.disconnect();
288         }
289         return uiCall;
290     }
291 
doTelecomCallRemoved(Call telecomCall)292     private void doTelecomCallRemoved(Call telecomCall) {
293         UiCall uiCall = getOrCreateCallContainer(telecomCall);
294 
295         mCallList.remove(uiCall);
296 
297         for (CallListener listener : mCallListeners) {
298             listener.onCallRemoved(uiCall);
299         }
300     }
301 
doCallAudioStateChanged(CallAudioState audioState)302     private void doCallAudioStateChanged(CallAudioState audioState) {
303         for (CallListener listener : mCallListeners) {
304             listener.onAudioStateChanged(audioState.isMuted(), audioState.getRoute(),
305                     audioState.getSupportedRouteMask());
306         }
307     }
308 
onStateChanged(UiCall uiCall, int state)309     private void onStateChanged(UiCall uiCall, int state) {
310         for (CallListener listener : mCallListeners) {
311             listener.onStateChanged(uiCall, state);
312         }
313     }
314 
onCallUpdated(UiCall uiCall)315     private void onCallUpdated(UiCall uiCall) {
316         for (CallListener listener : mCallListeners) {
317             listener.onCallUpdated(uiCall);
318         }
319     }
320 
321     private static class TelecomCallListener extends Call.Callback {
322         private final WeakReference<TelecomUiCallManager> mCarTelecomMangerRef;
323         private final WeakReference<UiCall> mCallContainerRef;
324 
TelecomCallListener(TelecomUiCallManager carTelecomManager, UiCall uiCall)325         TelecomCallListener(TelecomUiCallManager carTelecomManager, UiCall uiCall) {
326             mCarTelecomMangerRef = new WeakReference<>(carTelecomManager);
327             mCallContainerRef = new WeakReference<>(uiCall);
328         }
329 
330         @Override
onStateChanged(Call telecomCall, int state)331         public void onStateChanged(Call telecomCall, int state) {
332             if (Log.isLoggable(TAG, Log.DEBUG)) {
333                 Log.d(TAG, "onStateChanged: " + state);
334             }
335             TelecomUiCallManager manager = mCarTelecomMangerRef.get();
336             UiCall call = mCallContainerRef.get();
337             if (manager != null && call != null) {
338                 call.setState(state);
339                 manager.onStateChanged(call, state);
340             }
341         }
342 
343         @Override
onParentChanged(Call telecomCall, Call parent)344         public void onParentChanged(Call telecomCall, Call parent) {
345             doCallUpdated(telecomCall);
346         }
347 
348         @Override
onCallDestroyed(Call telecomCall)349         public void onCallDestroyed(Call telecomCall) {
350             if (Log.isLoggable(TAG, Log.DEBUG)) {
351                 Log.d(TAG, "onCallDestroyed");
352             }
353         }
354 
355         @Override
onDetailsChanged(Call telecomCall, Details details)356         public void onDetailsChanged(Call telecomCall, Details details) {
357             doCallUpdated(telecomCall);
358         }
359 
360         @Override
onVideoCallChanged(Call telecomCall, VideoCall videoCall)361         public void onVideoCallChanged(Call telecomCall, VideoCall videoCall) {
362             doCallUpdated(telecomCall);
363         }
364 
365         @Override
onCannedTextResponsesLoaded(Call telecomCall, List<String> cannedTextResponses)366         public void onCannedTextResponsesLoaded(Call telecomCall,
367                 List<String> cannedTextResponses) {
368             doCallUpdated(telecomCall);
369         }
370 
371         @Override
onChildrenChanged(Call telecomCall, List<Call> children)372         public void onChildrenChanged(Call telecomCall, List<Call> children) {
373             doCallUpdated(telecomCall);
374         }
375 
doCallUpdated(Call telecomCall)376         private void doCallUpdated(Call telecomCall) {
377             TelecomUiCallManager manager = mCarTelecomMangerRef.get();
378             UiCall uiCall = mCallContainerRef.get();
379             if (manager != null && uiCall != null) {
380                 TelecomUiCallList.updateCallContainerFromTelecom(uiCall, telecomCall);
381                 manager.onCallUpdated(uiCall);
382             }
383         }
384     }
385 
386     private ServiceConnection mInCallServiceConnection = new ServiceConnection() {
387 
388         @Override
389         public void onServiceConnected(ComponentName name, IBinder binder) {
390             if (Log.isLoggable(TAG, Log.DEBUG)) {
391                 Log.d(TAG, "onServiceConnected: " + name + ", service: " + binder);
392             }
393             mInCallService = ((InCallServiceImpl.LocalBinder) binder).getService();
394             mInCallService.registerCallback(mInCallServiceCallback);
395 
396             // The InCallServiceImpl could be bound when we already have some active calls, let's
397             // notify UI about these calls.
398             for (Call telecomCall : mInCallService.getCalls()) {
399                 UiCall uiCall = doTelecomCallAdded(telecomCall);
400                 onStateChanged(uiCall, uiCall.getState());
401             }
402         }
403 
404         @Override
405         public void onServiceDisconnected(ComponentName name) {
406             if (Log.isLoggable(TAG, Log.DEBUG)) {
407                 Log.d(TAG, "onServiceDisconnected: " + name);
408             }
409             mInCallService.unregisterCallback(mInCallServiceCallback);
410         }
411 
412         private InCallServiceImpl.Callback mInCallServiceCallback =
413                 new InCallServiceImpl.Callback() {
414 
415             @Override
416             public void onTelecomCallAdded(Call telecomCall) {
417                 doTelecomCallAdded(telecomCall);
418             }
419 
420             @Override
421             public void onTelecomCallRemoved(Call telecomCall) {
422                 doTelecomCallRemoved(telecomCall);
423             }
424 
425             @Override
426             public void onCallAudioStateChanged(CallAudioState audioState) {
427                 doCallAudioStateChanged(audioState);
428             }
429         };
430     };
431 
getOrCreateCallContainer(Call telecomCall)432     private UiCall getOrCreateCallContainer(Call telecomCall) {
433         synchronized (mCallList) {
434             return mCallList.getOrCreate(telecomCall);
435         }
436     }
437 
getCallAudioStateOrNull()438     private CallAudioState getCallAudioStateOrNull() {
439         return mInCallService != null ? mInCallService.getCallAudioState() : null;
440     }
441 }
442