1 /*
2  * Copyright (C) 2018 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 
17 package com.android.car.dialer.ui.activecall;
18 
19 import android.app.Application;
20 import android.content.ComponentName;
21 import android.content.Context;
22 import android.content.Intent;
23 import android.content.ServiceConnection;
24 import android.os.IBinder;
25 import android.telecom.Call;
26 import android.telecom.CallAudioState;
27 
28 import androidx.annotation.NonNull;
29 import androidx.core.util.Pair;
30 import androidx.lifecycle.AndroidViewModel;
31 import androidx.lifecycle.LiveData;
32 import androidx.lifecycle.MediatorLiveData;
33 import androidx.lifecycle.MutableLiveData;
34 import androidx.lifecycle.Transformations;
35 
36 import com.android.car.arch.common.LiveDataFunctions;
37 import com.android.car.dialer.livedata.AudioRouteLiveData;
38 import com.android.car.dialer.livedata.CallDetailLiveData;
39 import com.android.car.dialer.livedata.CallStateLiveData;
40 import com.android.car.dialer.log.L;
41 import com.android.car.dialer.telecom.InCallServiceImpl;
42 import com.android.car.telephony.common.CallDetail;
43 
44 import com.google.common.base.Predicate;
45 import com.google.common.collect.Lists;
46 
47 import java.util.ArrayList;
48 import java.util.Collections;
49 import java.util.Comparator;
50 import java.util.List;
51 
52 /**
53  * View model for {@link InCallActivity} and {@link OngoingCallFragment}. UI that doesn't belong to
54  * in call page should use a different ViewModel.
55  */
56 public class InCallViewModel extends AndroidViewModel implements
57         InCallServiceImpl.ActiveCallListChangedCallback, InCallServiceImpl.CallAudioStateCallback {
58     private static final String TAG = "CD.InCallViewModel";
59 
60     private final MutableLiveData<List<Call>> mCallListLiveData;
61     private final MutableLiveData<List<Call>> mOngoingCallListLiveData;
62     private final MutableLiveData<List<Call>> mConferenceCallListLiveData;
63     private final LiveData<List<CallDetail>> mConferenceCallDetailListLiveData;
64     private final Comparator<Call> mCallComparator;
65 
66     private final MutableLiveData<Call> mIncomingCallLiveData;
67 
68     private final CallDetailLiveData mCallDetailLiveData;
69     private final LiveData<Integer> mCallStateLiveData;
70     private final LiveData<Call> mPrimaryCallLiveData;
71     private final LiveData<Call> mSecondaryCallLiveData;
72     private final CallDetailLiveData mSecondaryCallDetailLiveData;
73     private final LiveData<Pair<Call, Call>> mOngoingCallPairLiveData;
74     private final LiveData<Integer> mAudioRouteLiveData;
75     private MutableLiveData<CallAudioState> mCallAudioStateLiveData;
76     private final MutableLiveData<Boolean> mDialpadIsOpen;
77     private final ShowOnholdCallLiveData mShowOnholdCall;
78     private LiveData<Long> mCallConnectTimeLiveData;
79     private LiveData<Long> mSecondaryCallConnectTimeLiveData;
80     private LiveData<Pair<Integer, Long>> mCallStateAndConnectTimeLiveData;
81     private final Context mContext;
82 
83     private InCallServiceImpl mInCallService;
84     private final ServiceConnection mInCallServiceConnection = new ServiceConnection() {
85 
86         @Override
87         public void onServiceConnected(ComponentName name, IBinder binder) {
88             L.d(TAG, "onServiceConnected: %s, service: %s", name, binder);
89             mInCallService = ((InCallServiceImpl.LocalBinder) binder).getService();
90             for (Call call : mInCallService.getCalls()) {
91                 call.registerCallback(mCallStateChangedCallback);
92             }
93             updateCallList();
94             mInCallService.addActiveCallListChangedCallback(InCallViewModel.this);
95             mInCallService.addCallAudioStateChangedCallback(InCallViewModel.this);
96         }
97 
98         @Override
99         public void onServiceDisconnected(ComponentName name) {
100             L.d(TAG, "onServiceDisconnected: %s", name);
101             mInCallService = null;
102         }
103     };
104 
105     // Reuse the same instance so the callback won't be registered more than once.
106     private final Call.Callback mCallStateChangedCallback = new Call.Callback() {
107         @Override
108         public void onStateChanged(Call call, int state) {
109             // Don't show in call activity by declining a ringing call to avoid UI flashing.
110             if (call.equals(mIncomingCallLiveData.getValue()) && state == Call.STATE_DISCONNECTED) {
111                 return;
112             }
113             // Sets value to trigger incoming call and active call list to update.
114             mCallListLiveData.setValue(mCallListLiveData.getValue());
115         }
116 
117         @Override
118         public void onParentChanged(Call call, Call parent) {
119             L.d(TAG, "onParentChanged %s", call);
120             updateCallList();
121         }
122 
123         @Override
124         public void onChildrenChanged(Call call, List<Call> children) {
125             L.d(TAG, "onChildrenChanged %s", call);
126             updateCallList();
127         }
128     };
129 
InCallViewModel(@onNull Application application)130     public InCallViewModel(@NonNull Application application) {
131         super(application);
132         mContext = application.getApplicationContext();
133 
134         mConferenceCallListLiveData = new MutableLiveData<>();
135         mIncomingCallLiveData = new MutableLiveData<>();
136         mOngoingCallListLiveData = new MutableLiveData<>();
137         mCallAudioStateLiveData = new MutableLiveData<>();
138         mCallComparator = new CallComparator();
139         mCallListLiveData = new MutableLiveData<List<Call>>() {
140             @Override
141             public void setValue(List<Call> callList) {
142                 super.setValue(callList);
143                 List<Call> activeCallList = filter(callList,
144                         call -> call != null && call.getState() != Call.STATE_RINGING);
145                 activeCallList.sort(mCallComparator);
146                 List<Call> conferenceList = filter(activeCallList,
147                         call -> call.getParent() != null);
148                 List<Call> ongoingCallList = filter(activeCallList,
149                         call -> call.getParent() == null);
150                 mConferenceCallListLiveData.setValue(conferenceList);
151                 mOngoingCallListLiveData.setValue(ongoingCallList);
152                 mIncomingCallLiveData.setValue(firstMatch(callList,
153                         call -> call != null && call.getState() == Call.STATE_RINGING));
154 
155                 L.d(TAG, "size:" + activeCallList.size() + " activeList" + activeCallList);
156                 L.d(TAG, "conf:%s" + conferenceList, conferenceList.size());
157                 L.d(TAG, "ongoing:%s" + ongoingCallList, ongoingCallList.size());
158             }
159         };
160 
161         mConferenceCallDetailListLiveData = Transformations.map(mConferenceCallListLiveData,
162                 callList -> {
163                     List<CallDetail> detailList = new ArrayList<>();
164                     for (Call call : callList) {
165                         detailList.add(CallDetail.fromTelecomCallDetail(call.getDetails()));
166                     }
167                     return detailList;
168                 });
169 
170         mCallDetailLiveData = new CallDetailLiveData();
171         mPrimaryCallLiveData = Transformations.map(mOngoingCallListLiveData, input -> {
172             Call call = input.isEmpty() ? null : input.get(0);
173             mCallDetailLiveData.setTelecomCall(call);
174             return call;
175         });
176 
177         mCallStateLiveData = Transformations.switchMap(mPrimaryCallLiveData,
178                 input -> input != null ? new CallStateLiveData(input) : null);
179         mCallConnectTimeLiveData = Transformations.map(mCallDetailLiveData, (details) -> {
180             if (details == null) {
181                 return 0L;
182             }
183             return details.getConnectTimeMillis();
184         });
185         mCallStateAndConnectTimeLiveData =
186                 LiveDataFunctions.pair(mCallStateLiveData, mCallConnectTimeLiveData);
187 
188         mSecondaryCallDetailLiveData = new CallDetailLiveData();
189         mSecondaryCallLiveData = Transformations.map(mOngoingCallListLiveData, callList -> {
190             Call call = (callList != null && callList.size() > 1) ? callList.get(1) : null;
191             mSecondaryCallDetailLiveData.setTelecomCall(call);
192             return call;
193         });
194 
195         mSecondaryCallConnectTimeLiveData = Transformations.map(mSecondaryCallDetailLiveData,
196                 details -> {
197                     if (details == null) {
198                         return 0L;
199                     }
200                     return details.getConnectTimeMillis();
201                 });
202 
203         mOngoingCallPairLiveData = LiveDataFunctions.pair(mPrimaryCallLiveData,
204                 mSecondaryCallLiveData);
205 
206         mAudioRouteLiveData = new AudioRouteLiveData(mContext);
207 
208         mDialpadIsOpen = new MutableLiveData<>();
209         // Set initial value to avoid NPE
210         mDialpadIsOpen.setValue(false);
211 
212         mShowOnholdCall = new ShowOnholdCallLiveData(mSecondaryCallLiveData, mDialpadIsOpen);
213 
214         Intent intent = new Intent(mContext, InCallServiceImpl.class);
215         intent.setAction(InCallServiceImpl.ACTION_LOCAL_BIND);
216         mContext.bindService(intent, mInCallServiceConnection, Context.BIND_AUTO_CREATE);
217     }
218 
219     /** Merge primary and secondary calls into a conference */
mergeConference()220     public void mergeConference() {
221         Call call = mPrimaryCallLiveData.getValue();
222         Call otherCall = mSecondaryCallLiveData.getValue();
223 
224         if (call == null || otherCall == null) {
225             return;
226         }
227         call.conference(otherCall);
228     }
229 
230     /** Returns the live data which monitors conference calls */
getConferenceCallDetailList()231     public LiveData<List<CallDetail>> getConferenceCallDetailList() {
232         return mConferenceCallDetailListLiveData;
233     }
234 
235     /** Returns the live data which monitors all the calls. */
getAllCallList()236     public LiveData<List<Call>> getAllCallList() {
237         return mCallListLiveData;
238     }
239 
240     /** Returns the live data which monitors the current incoming call. */
getIncomingCall()241     public LiveData<Call> getIncomingCall() {
242         return mIncomingCallLiveData;
243     }
244 
245     /** Returns {@link LiveData} for the ongoing call list which excludes the ringing call. */
getOngoingCallList()246     public LiveData<List<Call>> getOngoingCallList() {
247         return mOngoingCallListLiveData;
248     }
249 
250     /**
251      * Returns the live data which monitors the primary call details.
252      */
getPrimaryCallDetail()253     public LiveData<CallDetail> getPrimaryCallDetail() {
254         return mCallDetailLiveData;
255     }
256 
257     /**
258      * Returns the live data which monitors the primary call state.
259      */
getPrimaryCallState()260     public LiveData<Integer> getPrimaryCallState() {
261         return mCallStateLiveData;
262     }
263 
264     /**
265      * Returns the live data which monitors the primary call state and the start time of the call.
266      */
getCallStateAndConnectTime()267     public LiveData<Pair<Integer, Long>> getCallStateAndConnectTime() {
268         return mCallStateAndConnectTimeLiveData;
269     }
270 
271     /**
272      * Returns the live data which monitor the primary call.
273      * A primary call in the first call in the ongoing call list,
274      * which is sorted based on {@link CallComparator}.
275      */
getPrimaryCall()276     public LiveData<Call> getPrimaryCall() {
277         return mPrimaryCallLiveData;
278     }
279 
280     /**
281      * Returns the live data which monitor the secondary call.
282      * A secondary call in the second call in the ongoing call list,
283      * which is sorted based on {@link CallComparator}.
284      * The value will be null if there is no second call in the call list.
285      */
getSecondaryCall()286     public LiveData<Call> getSecondaryCall() {
287         return mSecondaryCallLiveData;
288     }
289 
290     /**
291      * Returns the live data which monitors the secondary call details.
292      */
getSecondaryCallDetail()293     public LiveData<CallDetail> getSecondaryCallDetail() {
294         return mSecondaryCallDetailLiveData;
295     }
296 
297     /**
298      * Returns the live data which monitors the secondary call connect time.
299      */
getSecondaryCallConnectTime()300     public LiveData<Long> getSecondaryCallConnectTime() {
301         return mSecondaryCallConnectTimeLiveData;
302     }
303 
304     /**
305      * Returns the live data that monitors the primary and secondary calls.
306      */
getOngoingCallPair()307     public LiveData<Pair<Call, Call>> getOngoingCallPair() {
308         return mOngoingCallPairLiveData;
309     }
310 
311     /**
312      * Returns current audio route.
313      */
getAudioRoute()314     public LiveData<Integer> getAudioRoute() {
315         return mAudioRouteLiveData;
316     }
317 
318     /**
319      * Returns current call audio state.
320      */
getCallAudioState()321     public MutableLiveData<CallAudioState> getCallAudioState() {
322         return mCallAudioStateLiveData;
323     }
324 
325     /** Return the {@link MutableLiveData} for dialpad open state. */
getDialpadOpenState()326     public MutableLiveData<Boolean> getDialpadOpenState() {
327         return mDialpadIsOpen;
328     }
329 
330     /** Return the livedata monitors onhold call status. */
shouldShowOnholdCall()331     public LiveData<Boolean> shouldShowOnholdCall() {
332         return mShowOnholdCall;
333     }
334 
335     @Override
onTelecomCallAdded(Call telecomCall)336     public boolean onTelecomCallAdded(Call telecomCall) {
337         L.i(TAG, "onTelecomCallAdded %s %s", telecomCall, this);
338         telecomCall.registerCallback(mCallStateChangedCallback);
339         updateCallList();
340         return false;
341     }
342 
343     @Override
onTelecomCallRemoved(Call telecomCall)344     public boolean onTelecomCallRemoved(Call telecomCall) {
345         L.i(TAG, "onTelecomCallRemoved %s %s", telecomCall, this);
346         telecomCall.unregisterCallback(mCallStateChangedCallback);
347         updateCallList();
348         return false;
349     }
350 
351     @Override
onCallAudioStateChanged(CallAudioState callAudioState)352     public void onCallAudioStateChanged(CallAudioState callAudioState) {
353         L.i(TAG, "onCallAudioStateChanged %s %s", callAudioState, this);
354         mCallAudioStateLiveData.setValue(callAudioState);
355     }
356 
updateCallList()357     private void updateCallList() {
358         List<Call> callList = new ArrayList<>();
359         callList.addAll(mInCallService.getCalls());
360         mCallListLiveData.setValue(callList);
361     }
362 
363     @Override
onCleared()364     protected void onCleared() {
365         mContext.unbindService(mInCallServiceConnection);
366         if (mInCallService != null) {
367             for (Call call : mInCallService.getCalls()) {
368                 call.unregisterCallback(mCallStateChangedCallback);
369             }
370             mInCallService.removeActiveCallListChangedCallback(this);
371             mInCallService.removeCallAudioStateChangedCallback(this);
372         }
373         mInCallService = null;
374     }
375 
376     private static class CallComparator implements Comparator<Call> {
377         /**
378          * The rank of call state. Used for sorting active calls. Rank is listed from lowest to
379          * highest.
380          */
381         private static final List<Integer> CALL_STATE_RANK = Lists.newArrayList(
382                 Call.STATE_RINGING,
383                 Call.STATE_DISCONNECTED,
384                 Call.STATE_DISCONNECTING,
385                 Call.STATE_NEW,
386                 Call.STATE_CONNECTING,
387                 Call.STATE_SELECT_PHONE_ACCOUNT,
388                 Call.STATE_HOLDING,
389                 Call.STATE_ACTIVE,
390                 Call.STATE_DIALING);
391 
392         @Override
compare(Call call, Call otherCall)393         public int compare(Call call, Call otherCall) {
394             boolean callHasParent = call.getParent() != null;
395             boolean otherCallHasParent = otherCall.getParent() != null;
396 
397             if (callHasParent && !otherCallHasParent) {
398                 return 1;
399             } else if (!callHasParent && otherCallHasParent) {
400                 return -1;
401             }
402             int carCallRank = CALL_STATE_RANK.indexOf(call.getState());
403             int otherCarCallRank = CALL_STATE_RANK.indexOf(otherCall.getState());
404 
405             return otherCarCallRank - carCallRank;
406         }
407     }
408 
firstMatch(List<Call> callList, Predicate<Call> predicate)409     private static Call firstMatch(List<Call> callList, Predicate<Call> predicate) {
410         List<Call> filteredResults = filter(callList, predicate);
411         return filteredResults.isEmpty() ? null : filteredResults.get(0);
412     }
413 
filter(List<Call> callList, Predicate<Call> predicate)414     private static List<Call> filter(List<Call> callList, Predicate<Call> predicate) {
415         if (callList == null || predicate == null) {
416             return Collections.emptyList();
417         }
418 
419         List<Call> filteredResults = new ArrayList<>();
420         for (Call call : callList) {
421             if (predicate.apply(call)) {
422                 filteredResults.add(call);
423             }
424         }
425         return filteredResults;
426     }
427 
428     private static class ShowOnholdCallLiveData extends MediatorLiveData<Boolean> {
429 
430         private final LiveData<Call> mSecondaryCallLiveData;
431         private final MutableLiveData<Boolean> mDialpadIsOpen;
432 
ShowOnholdCallLiveData(LiveData<Call> secondaryCallLiveData, MutableLiveData<Boolean> dialpadState)433         private ShowOnholdCallLiveData(LiveData<Call> secondaryCallLiveData,
434                 MutableLiveData<Boolean> dialpadState) {
435             mSecondaryCallLiveData = secondaryCallLiveData;
436             mDialpadIsOpen = dialpadState;
437             setValue(false);
438 
439             addSource(mSecondaryCallLiveData, v -> update());
440             addSource(mDialpadIsOpen, v -> update());
441         }
442 
update()443         private void update() {
444             Boolean shouldShowOnholdCall = !mDialpadIsOpen.getValue();
445             Call onholdCall = mSecondaryCallLiveData.getValue();
446             if (shouldShowOnholdCall && onholdCall != null
447                     && onholdCall.getState() == Call.STATE_HOLDING) {
448                 setValue(true);
449             } else {
450                 setValue(false);
451             }
452         }
453 
454         @Override
setValue(Boolean newValue)455         public void setValue(Boolean newValue) {
456             // Only set value and notify observers when the value changes.
457             if (getValue() != newValue) {
458                 super.setValue(newValue);
459             }
460         }
461     }
462 }
463