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