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