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 Comparator<Call> mCallComparator;
63 
64     private final MutableLiveData<Call> mIncomingCallLiveData;
65 
66     private final LiveData<CallDetail> mCallDetailLiveData;
67     private final LiveData<Integer> mCallStateLiveData;
68     private final LiveData<Call> mPrimaryCallLiveData;
69     private final LiveData<Call> mSecondaryCallLiveData;
70     private final LiveData<CallDetail> mSecondaryCallDetailLiveData;
71     private final LiveData<Integer> mAudioRouteLiveData;
72     private MutableLiveData<CallAudioState> mCallAudioStateLiveData;
73     private final MutableLiveData<Boolean> mDialpadIsOpen;
74     private final ShowOnholdCallLiveData mShowOnholdCall;
75     private LiveData<Long> mCallConnectTimeLiveData;
76     private LiveData<Pair<Integer, Long>> mCallStateAndConnectTimeLiveData;
77     private final Context mContext;
78 
79     private InCallServiceImpl mInCallService;
80     private final ServiceConnection mInCallServiceConnection = new ServiceConnection() {
81 
82         @Override
83         public void onServiceConnected(ComponentName name, IBinder binder) {
84             L.d(TAG, "onServiceConnected: %s, service: %s", name, binder);
85             mInCallService = ((InCallServiceImpl.LocalBinder) binder).getService();
86             for (Call call : mInCallService.getCalls()) {
87                 call.registerCallback(mCallStateChangedCallback);
88             }
89             updateCallList();
90             mInCallService.addActiveCallListChangedCallback(InCallViewModel.this);
91             mInCallService.addCallAudioStateChangedCallback(InCallViewModel.this);
92         }
93 
94         @Override
95         public void onServiceDisconnected(ComponentName name) {
96             L.d(TAG, "onServiceDisconnected: %s", name);
97             mInCallService = null;
98         }
99     };
100 
101     // Reuse the same instance so the callback won't be registered more than once.
102     private final Call.Callback mCallStateChangedCallback = new Call.Callback() {
103         @Override
104         public void onStateChanged(Call call, int state) {
105             // Don't show in call activity by declining a ringing call to avoid UI flashing.
106             if (call.equals(mIncomingCallLiveData.getValue()) && state == Call.STATE_DISCONNECTED) {
107                 return;
108             }
109             // Sets value to trigger incoming call and active call list to update.
110             mCallListLiveData.setValue(mCallListLiveData.getValue());
111         }
112     };
113 
InCallViewModel(@onNull Application application)114     public InCallViewModel(@NonNull Application application) {
115         super(application);
116         mContext = application.getApplicationContext();
117 
118         mIncomingCallLiveData = new MutableLiveData<>();
119         mOngoingCallListLiveData = new MutableLiveData<>();
120         mCallAudioStateLiveData = new MutableLiveData<>();
121         mCallComparator = new CallComparator();
122         mCallListLiveData = new MutableLiveData<List<Call>>() {
123             @Override
124             public void setValue(List<Call> callList) {
125                 super.setValue(callList);
126                 List<Call> activeCallList = filter(callList,
127                         call -> call != null && call.getState() != Call.STATE_RINGING);
128                 activeCallList.sort(mCallComparator);
129                 mOngoingCallListLiveData.setValue(activeCallList);
130                 mIncomingCallLiveData.setValue(firstMatch(callList,
131                         call -> call != null && call.getState() == Call.STATE_RINGING));
132             }
133         };
134 
135         mPrimaryCallLiveData = Transformations.map(mOngoingCallListLiveData,
136                 input -> input.isEmpty() ? null : input.get(0));
137         mCallDetailLiveData = Transformations.switchMap(mPrimaryCallLiveData,
138                 input -> input != null ? new CallDetailLiveData(input) : null);
139         mCallStateLiveData = Transformations.switchMap(mPrimaryCallLiveData,
140                 input -> input != null ? new CallStateLiveData(input) : null);
141         mCallConnectTimeLiveData = Transformations.map(mCallDetailLiveData, (details) -> {
142             if (details == null) {
143                 return 0L;
144             }
145             return details.getConnectTimeMillis();
146         });
147         mCallStateAndConnectTimeLiveData =
148                 LiveDataFunctions.pair(mCallStateLiveData, mCallConnectTimeLiveData);
149 
150         mSecondaryCallLiveData = Transformations.map(mOngoingCallListLiveData,
151                 callList -> (callList != null && callList.size() > 1) ? callList.get(1) : null);
152 
153         mSecondaryCallDetailLiveData = Transformations.switchMap(mSecondaryCallLiveData,
154                 input -> input != null ? new CallDetailLiveData(input) : null);
155 
156         mAudioRouteLiveData = new AudioRouteLiveData(mContext);
157 
158         mDialpadIsOpen = new MutableLiveData<>();
159         // Set initial value to avoid NPE
160         mDialpadIsOpen.setValue(false);
161 
162         mShowOnholdCall = new ShowOnholdCallLiveData(mSecondaryCallLiveData, mDialpadIsOpen);
163 
164         Intent intent = new Intent(mContext, InCallServiceImpl.class);
165         intent.setAction(InCallServiceImpl.ACTION_LOCAL_BIND);
166         mContext.bindService(intent, mInCallServiceConnection, Context.BIND_AUTO_CREATE);
167     }
168 
169     /** Returns the live data which monitors all the calls. */
getAllCallList()170     public LiveData<List<Call>> getAllCallList() {
171         return mCallListLiveData;
172     }
173 
174     /** Returns the live data which monitors the current incoming call. */
getIncomingCall()175     public LiveData<Call> getIncomingCall() {
176         return mIncomingCallLiveData;
177     }
178 
179     /** Returns {@link LiveData} for the ongoing call list which excludes the ringing call. */
getOngoingCallList()180     public LiveData<List<Call>> getOngoingCallList() {
181         return mOngoingCallListLiveData;
182     }
183 
184     /**
185      * Returns the live data which monitors the primary call details.
186      */
getPrimaryCallDetail()187     public LiveData<CallDetail> getPrimaryCallDetail() {
188         return mCallDetailLiveData;
189     }
190 
191     /**
192      * Returns the live data which monitors the primary call state.
193      */
getPrimaryCallState()194     public LiveData<Integer> getPrimaryCallState() {
195         return mCallStateLiveData;
196     }
197 
198     /**
199      * Returns the live data which monitors the primary call state and the start time of the call.
200      */
getCallStateAndConnectTime()201     public LiveData<Pair<Integer, Long>> getCallStateAndConnectTime() {
202         return mCallStateAndConnectTimeLiveData;
203     }
204 
205     /**
206      * Returns the live data which monitor the primary call.
207      * A primary call in the first call in the ongoing call list,
208      * which is sorted based on {@link CallComparator}.
209      */
getPrimaryCall()210     public LiveData<Call> getPrimaryCall() {
211         return mPrimaryCallLiveData;
212     }
213 
214     /**
215      * Returns the live data which monitor the secondary call.
216      * A secondary call in the second call in the ongoing call list,
217      * which is sorted based on {@link CallComparator}.
218      * The value will be null if there is no second call in the call list.
219      */
getSecondaryCall()220     public LiveData<Call> getSecondaryCall() {
221         return mSecondaryCallLiveData;
222     }
223 
224     /**
225      * Returns the live data which monitors the secondary call details.
226      */
getSecondaryCallDetail()227     public LiveData<CallDetail> getSecondaryCallDetail() {
228         return mSecondaryCallDetailLiveData;
229     }
230 
231     /**
232      * Returns current audio route.
233      */
getAudioRoute()234     public LiveData<Integer> getAudioRoute() {
235         return mAudioRouteLiveData;
236     }
237 
238     /**
239      * Returns current call audio state.
240      */
getCallAudioState()241     public MutableLiveData<CallAudioState> getCallAudioState() {
242         return mCallAudioStateLiveData;
243     }
244 
245     /** Return the {@link MutableLiveData} for dialpad open state. */
getDialpadOpenState()246     public MutableLiveData<Boolean> getDialpadOpenState() {
247         return mDialpadIsOpen;
248     }
249 
250     /** Return the livedata monitors onhold call status. */
shouldShowOnholdCall()251     public LiveData<Boolean> shouldShowOnholdCall() {
252         return mShowOnholdCall;
253     }
254 
255     @Override
onTelecomCallAdded(Call telecomCall)256     public boolean onTelecomCallAdded(Call telecomCall) {
257         L.i(TAG, "onTelecomCallAdded %s %s", telecomCall, this);
258         telecomCall.registerCallback(mCallStateChangedCallback);
259         updateCallList();
260         return false;
261     }
262 
263     @Override
onTelecomCallRemoved(Call telecomCall)264     public boolean onTelecomCallRemoved(Call telecomCall) {
265         L.i(TAG, "onTelecomCallRemoved %s %s", telecomCall, this);
266         telecomCall.unregisterCallback(mCallStateChangedCallback);
267         updateCallList();
268         return false;
269     }
270 
271     @Override
onCallAudioStateChanged(CallAudioState callAudioState)272     public void onCallAudioStateChanged(CallAudioState callAudioState) {
273         L.i(TAG, "onCallAudioStateChanged %s %s", callAudioState, this);
274         mCallAudioStateLiveData.setValue(callAudioState);
275     }
276 
updateCallList()277     private void updateCallList() {
278         List<Call> callList = new ArrayList<>();
279         callList.addAll(mInCallService.getCalls());
280         mCallListLiveData.setValue(callList);
281     }
282 
283     @Override
onCleared()284     protected void onCleared() {
285         mContext.unbindService(mInCallServiceConnection);
286         if (mInCallService != null) {
287             for (Call call : mInCallService.getCalls()) {
288                 call.unregisterCallback(mCallStateChangedCallback);
289             }
290             mInCallService.removeActiveCallListChangedCallback(this);
291             mInCallService.removeCallAudioStateChangedCallback(this);
292         }
293         mInCallService = null;
294     }
295 
296     private static class CallComparator implements Comparator<Call> {
297         /**
298          * The rank of call state. Used for sorting active calls. Rank is listed from lowest to
299          * highest.
300          */
301         private static final List<Integer> CALL_STATE_RANK = Lists.newArrayList(
302                 Call.STATE_RINGING,
303                 Call.STATE_DISCONNECTED,
304                 Call.STATE_DISCONNECTING,
305                 Call.STATE_NEW,
306                 Call.STATE_CONNECTING,
307                 Call.STATE_SELECT_PHONE_ACCOUNT,
308                 Call.STATE_HOLDING,
309                 Call.STATE_ACTIVE,
310                 Call.STATE_DIALING);
311 
312         @Override
compare(Call call, Call otherCall)313         public int compare(Call call, Call otherCall) {
314             boolean callHasParent = call.getParent() != null;
315             boolean otherCallHasParent = otherCall.getParent() != null;
316 
317             if (callHasParent && !otherCallHasParent) {
318                 return 1;
319             } else if (!callHasParent && otherCallHasParent) {
320                 return -1;
321             }
322             int carCallRank = CALL_STATE_RANK.indexOf(call.getState());
323             int otherCarCallRank = CALL_STATE_RANK.indexOf(otherCall.getState());
324 
325             return otherCarCallRank - carCallRank;
326         }
327     }
328 
firstMatch(List<Call> callList, Predicate<Call> predicate)329     private static Call firstMatch(List<Call> callList, Predicate<Call> predicate) {
330         List<Call> filteredResults = filter(callList, predicate);
331         return filteredResults.isEmpty() ? null : filteredResults.get(0);
332     }
333 
filter(List<Call> callList, Predicate<Call> predicate)334     private static List<Call> filter(List<Call> callList, Predicate<Call> predicate) {
335         if (callList == null || predicate == null) {
336             return Collections.emptyList();
337         }
338 
339         List<Call> filteredResults = new ArrayList<>();
340         for (Call call : callList) {
341             if (predicate.apply(call)) {
342                 filteredResults.add(call);
343             }
344         }
345         return filteredResults;
346     }
347 
348     private static class ShowOnholdCallLiveData extends MediatorLiveData<Boolean> {
349 
350         private final LiveData<Call> mSecondaryCallLiveData;
351         private final MutableLiveData<Boolean> mDialpadIsOpen;
352 
ShowOnholdCallLiveData(LiveData<Call> secondaryCallLiveData, MutableLiveData<Boolean> dialpadState)353         private ShowOnholdCallLiveData(LiveData<Call> secondaryCallLiveData,
354                 MutableLiveData<Boolean> dialpadState) {
355             mSecondaryCallLiveData = secondaryCallLiveData;
356             mDialpadIsOpen = dialpadState;
357             setValue(false);
358 
359             addSource(mSecondaryCallLiveData, v -> update());
360             addSource(mDialpadIsOpen, v -> update());
361         }
362 
update()363         private void update() {
364             Boolean shouldShowOnholdCall = !mDialpadIsOpen.getValue();
365             Call onholdCall = mSecondaryCallLiveData.getValue();
366             if (shouldShowOnholdCall && onholdCall != null
367                     && onholdCall.getState() == Call.STATE_HOLDING) {
368                 setValue(true);
369             } else {
370                 setValue(false);
371             }
372         }
373 
374         @Override
setValue(Boolean newValue)375         public void setValue(Boolean newValue) {
376             // Only set value and notify observers when the value changes.
377             if (getValue() != newValue) {
378                 super.setValue(newValue);
379             }
380         }
381     }
382 }
383