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 27 import androidx.annotation.NonNull; 28 import androidx.core.util.Pair; 29 import androidx.lifecycle.AndroidViewModel; 30 import androidx.lifecycle.LiveData; 31 import androidx.lifecycle.MutableLiveData; 32 import androidx.lifecycle.Transformations; 33 34 import com.android.car.arch.common.LiveDataFunctions; 35 import com.android.car.dialer.livedata.AudioRouteLiveData; 36 import com.android.car.dialer.livedata.CallDetailLiveData; 37 import com.android.car.dialer.livedata.CallStateLiveData; 38 import com.android.car.dialer.log.L; 39 import com.android.car.dialer.telecom.InCallServiceImpl; 40 import com.android.car.telephony.common.CallDetail; 41 42 import com.google.common.base.Predicate; 43 import com.google.common.collect.Lists; 44 45 import java.util.ArrayList; 46 import java.util.Collections; 47 import java.util.Comparator; 48 import java.util.List; 49 50 /** 51 * View model for {@link InCallActivity} and {@link OngoingCallFragment}. UI that doesn't belong to 52 * in call page should use a different ViewModel. 53 */ 54 public class InCallViewModel extends AndroidViewModel implements 55 InCallServiceImpl.ActiveCallListChangedCallback { 56 private static final String TAG = "CD.InCallViewModel"; 57 58 private final MutableLiveData<List<Call>> mCallListLiveData; 59 private final LiveData<List<Call>> mOngoingCallListLiveData; 60 private final Comparator<Call> mCallComparator; 61 62 private final LiveData<Call> mIncomingCallLiveData; 63 64 private final LiveData<CallDetail> mCallDetailLiveData; 65 private final LiveData<Integer> mCallStateLiveData; 66 private final LiveData<Call> mPrimaryCallLiveData; 67 private final LiveData<Call> mSecondaryCallLiveData; 68 private final LiveData<CallDetail> mSecondaryCallDetailLiveData; 69 private final LiveData<Integer> mAudioRouteLiveData; 70 private LiveData<Long> mCallConnectTimeLiveData; 71 private LiveData<Pair<Integer, Long>> mCallStateAndConnectTimeLiveData; 72 private final Context mContext; 73 74 private InCallServiceImpl mInCallService; 75 private final ServiceConnection mInCallServiceConnection = new ServiceConnection() { 76 77 @Override 78 public void onServiceConnected(ComponentName name, IBinder binder) { 79 L.d(TAG, "onServiceConnected: %s, service: %s", name, binder); 80 mInCallService = ((InCallServiceImpl.LocalBinder) binder).getService(); 81 updateCallList(); 82 mInCallService.addActiveCallListChangedCallback(InCallViewModel.this); 83 } 84 85 @Override 86 public void onServiceDisconnected(ComponentName name) { 87 L.d(TAG, "onServiceDisconnected: %s", name); 88 mInCallService = null; 89 } 90 }; 91 92 // Reuse the same instance so the callback won't be registered more than once. 93 private final Call.Callback mCallStateChangedCallback = new Call.Callback() { 94 @Override 95 public void onStateChanged(Call call, int state) { 96 // Sets value to trigger the live data for incoming call and active call list to update. 97 mCallListLiveData.setValue(mCallListLiveData.getValue()); 98 } 99 }; 100 InCallViewModel(@onNull Application application)101 public InCallViewModel(@NonNull Application application) { 102 super(application); 103 mContext = application.getApplicationContext(); 104 105 mCallListLiveData = new MutableLiveData<>(); 106 mCallComparator = new CallComparator(); 107 108 mIncomingCallLiveData = Transformations.map(mCallListLiveData, 109 callList -> firstMatch(callList, 110 call -> call != null && call.getState() == Call.STATE_RINGING)); 111 112 mOngoingCallListLiveData = Transformations.map(mCallListLiveData, 113 callList -> { 114 List<Call> activeCallList = filter(callList, 115 call -> call != null && call.getState() != Call.STATE_RINGING); 116 activeCallList.sort(mCallComparator); 117 return activeCallList; 118 }); 119 120 mPrimaryCallLiveData = Transformations.map(mOngoingCallListLiveData, 121 input -> input.isEmpty() ? null : input.get(0)); 122 mCallDetailLiveData = Transformations.switchMap(mPrimaryCallLiveData, 123 input -> input != null ? new CallDetailLiveData(input) : null); 124 mCallStateLiveData = Transformations.switchMap(mPrimaryCallLiveData, 125 input -> input != null ? new CallStateLiveData(input) : null); 126 mCallConnectTimeLiveData = Transformations.map(mCallDetailLiveData, (details) -> { 127 if (details == null) { 128 return 0L; 129 } 130 return details.getConnectTimeMillis(); 131 }); 132 mCallStateAndConnectTimeLiveData = 133 LiveDataFunctions.pair(mCallStateLiveData, mCallConnectTimeLiveData); 134 135 mSecondaryCallLiveData = Transformations.map(mOngoingCallListLiveData, 136 callList -> (callList != null && callList.size() > 1) ? callList.get(1) : null); 137 138 mSecondaryCallDetailLiveData = Transformations.switchMap(mSecondaryCallLiveData, 139 input -> input != null ? new CallDetailLiveData(input) : null); 140 141 mAudioRouteLiveData = new AudioRouteLiveData(mContext); 142 143 Intent intent = new Intent(mContext, InCallServiceImpl.class); 144 intent.setAction(InCallServiceImpl.ACTION_LOCAL_BIND); 145 mContext.bindService(intent, mInCallServiceConnection, Context.BIND_AUTO_CREATE); 146 } 147 148 /** Returns the live data which monitors the current incoming call. */ getIncomingCall()149 public LiveData<Call> getIncomingCall() { 150 return mIncomingCallLiveData; 151 } 152 153 /** Returns {@link LiveData} for the ongoing call list which excludes the ringing call. */ getOngoingCallList()154 public LiveData<List<Call>> getOngoingCallList() { 155 return mOngoingCallListLiveData; 156 } 157 158 /** 159 * Returns the live data which monitors the primary call details. 160 */ getPrimaryCallDetail()161 public LiveData<CallDetail> getPrimaryCallDetail() { 162 return mCallDetailLiveData; 163 } 164 165 /** 166 * Returns the live data which monitors the primary call state. 167 */ getPrimaryCallState()168 public LiveData<Integer> getPrimaryCallState() { 169 return mCallStateLiveData; 170 } 171 172 /** 173 * Returns the live data which monitors the primary call state and the start time of the call. 174 */ getCallStateAndConnectTime()175 public LiveData<Pair<Integer, Long>> getCallStateAndConnectTime() { 176 return mCallStateAndConnectTimeLiveData; 177 } 178 179 /** 180 * Returns the live data which monitor the primary call. 181 * A primary call in the first call in the ongoing call list, 182 * which is sorted based on {@link CallComparator}. 183 */ getPrimaryCall()184 public LiveData<Call> getPrimaryCall() { 185 return mPrimaryCallLiveData; 186 } 187 188 /** 189 * Returns the live data which monitor the secondary call. 190 * A secondary call in the second call in the ongoing call list, 191 * which is sorted based on {@link CallComparator}. 192 * The value will be null if there is no second call in the call list. 193 */ getSecondaryCall()194 public LiveData<Call> getSecondaryCall() { 195 return mSecondaryCallLiveData; 196 } 197 198 /** 199 * Returns the live data which monitors the secondary call details. 200 */ getSecondaryCallDetail()201 public LiveData<CallDetail> getSecondaryCallDetail() { 202 return mSecondaryCallDetailLiveData; 203 } 204 205 /** 206 * Returns current audio route. 207 */ getAudioRoute()208 public LiveData<Integer> getAudioRoute() { 209 return mAudioRouteLiveData; 210 } 211 212 @Override onTelecomCallAdded(Call telecomCall)213 public boolean onTelecomCallAdded(Call telecomCall) { 214 L.i(TAG, "onTelecomCallAdded %s %s", telecomCall, this); 215 telecomCall.registerCallback(mCallStateChangedCallback); 216 updateCallList(); 217 return false; 218 } 219 220 @Override onTelecomCallRemoved(Call telecomCall)221 public boolean onTelecomCallRemoved(Call telecomCall) { 222 L.i(TAG, "onTelecomCallRemoved %s %s", telecomCall, this); 223 telecomCall.unregisterCallback(mCallStateChangedCallback); 224 updateCallList(); 225 return false; 226 } 227 updateCallList()228 private void updateCallList() { 229 List<Call> callList = new ArrayList<>(); 230 callList.addAll(mInCallService.getCalls()); 231 mCallListLiveData.setValue(callList); 232 } 233 234 @Override onCleared()235 protected void onCleared() { 236 mContext.unbindService(mInCallServiceConnection); 237 if (mInCallService != null) { 238 mInCallService.removeActiveCallListChangedCallback(this); 239 } 240 mInCallService = null; 241 } 242 243 private static class CallComparator implements Comparator<Call> { 244 /** 245 * The rank of call state. Used for sorting active calls. Rank is listed from lowest to 246 * highest. 247 */ 248 private static final List<Integer> CALL_STATE_RANK = Lists.newArrayList( 249 Call.STATE_RINGING, 250 Call.STATE_DISCONNECTED, 251 Call.STATE_DISCONNECTING, 252 Call.STATE_NEW, 253 Call.STATE_CONNECTING, 254 Call.STATE_SELECT_PHONE_ACCOUNT, 255 Call.STATE_HOLDING, 256 Call.STATE_ACTIVE, 257 Call.STATE_DIALING); 258 259 @Override compare(Call call, Call otherCall)260 public int compare(Call call, Call otherCall) { 261 boolean callHasParent = call.getParent() != null; 262 boolean otherCallHasParent = otherCall.getParent() != null; 263 264 if (callHasParent && !otherCallHasParent) { 265 return 1; 266 } else if (!callHasParent && otherCallHasParent) { 267 return -1; 268 } 269 int carCallRank = CALL_STATE_RANK.indexOf(call.getState()); 270 int otherCarCallRank = CALL_STATE_RANK.indexOf(otherCall.getState()); 271 272 return otherCarCallRank - carCallRank; 273 } 274 } 275 firstMatch(List<Call> callList, Predicate<Call> predicate)276 private static Call firstMatch(List<Call> callList, Predicate<Call> predicate) { 277 List<Call> filteredResults = filter(callList, predicate); 278 return filteredResults.isEmpty() ? null : filteredResults.get(0); 279 } 280 filter(List<Call> callList, Predicate<Call> predicate)281 private static List<Call> filter(List<Call> callList, Predicate<Call> predicate) { 282 if (callList == null || predicate == null) { 283 return Collections.emptyList(); 284 } 285 286 List<Call> filteredResults = new ArrayList<>(); 287 for (Call call : callList) { 288 if (predicate.apply(call)) { 289 filteredResults.add(call); 290 } 291 } 292 return filteredResults; 293 } 294 } 295