blob: 7e60ebf3af170796510f17d29e33cfc4ea23055a [file] [log] [blame]
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.dialer.ui.activecall;
import android.app.Application;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.IBinder;
import android.telecom.Call;
import android.telecom.CallAudioState;
import androidx.annotation.NonNull;
import androidx.core.util.Pair;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MediatorLiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Transformations;
import com.android.car.arch.common.LiveDataFunctions;
import com.android.car.dialer.livedata.AudioRouteLiveData;
import com.android.car.dialer.livedata.CallDetailLiveData;
import com.android.car.dialer.livedata.CallStateLiveData;
import com.android.car.dialer.log.L;
import com.android.car.dialer.telecom.InCallServiceImpl;
import com.android.car.telephony.common.CallDetail;
import com.google.common.base.Predicate;
import com.google.common.collect.Lists;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
/**
* View model for {@link InCallActivity} and {@link OngoingCallFragment}. UI that doesn't belong to
* in call page should use a different ViewModel.
*/
public class InCallViewModel extends AndroidViewModel implements
InCallServiceImpl.ActiveCallListChangedCallback, InCallServiceImpl.CallAudioStateCallback {
private static final String TAG = "CD.InCallViewModel";
private final MutableLiveData<List<Call>> mCallListLiveData;
private final MutableLiveData<List<Call>> mOngoingCallListLiveData;
private final MutableLiveData<List<Call>> mConferenceCallListLiveData;
private final LiveData<List<CallDetail>> mConferenceCallDetailListLiveData;
private final Comparator<Call> mCallComparator;
private final MutableLiveData<Call> mIncomingCallLiveData;
private final CallDetailLiveData mCallDetailLiveData;
private final LiveData<Integer> mCallStateLiveData;
private final LiveData<Call> mPrimaryCallLiveData;
private final LiveData<Call> mSecondaryCallLiveData;
private final CallDetailLiveData mSecondaryCallDetailLiveData;
private final LiveData<Pair<Call, Call>> mOngoingCallPairLiveData;
private final LiveData<Integer> mAudioRouteLiveData;
private MutableLiveData<CallAudioState> mCallAudioStateLiveData;
private final MutableLiveData<Boolean> mDialpadIsOpen;
private final ShowOnholdCallLiveData mShowOnholdCall;
private LiveData<Long> mCallConnectTimeLiveData;
private LiveData<Long> mSecondaryCallConnectTimeLiveData;
private LiveData<Pair<Integer, Long>> mCallStateAndConnectTimeLiveData;
private final Context mContext;
private InCallServiceImpl mInCallService;
private final ServiceConnection mInCallServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder binder) {
L.d(TAG, "onServiceConnected: %s, service: %s", name, binder);
mInCallService = ((InCallServiceImpl.LocalBinder) binder).getService();
for (Call call : mInCallService.getCalls()) {
call.registerCallback(mCallStateChangedCallback);
}
updateCallList();
mInCallService.addActiveCallListChangedCallback(InCallViewModel.this);
mInCallService.addCallAudioStateChangedCallback(InCallViewModel.this);
}
@Override
public void onServiceDisconnected(ComponentName name) {
L.d(TAG, "onServiceDisconnected: %s", name);
mInCallService = null;
}
};
// Reuse the same instance so the callback won't be registered more than once.
private final Call.Callback mCallStateChangedCallback = new Call.Callback() {
@Override
public void onStateChanged(Call call, int state) {
// Don't show in call activity by declining a ringing call to avoid UI flashing.
if (call.equals(mIncomingCallLiveData.getValue()) && state == Call.STATE_DISCONNECTED) {
return;
}
// Sets value to trigger incoming call and active call list to update.
mCallListLiveData.setValue(mCallListLiveData.getValue());
}
@Override
public void onParentChanged(Call call, Call parent) {
L.d(TAG, "onParentChanged %s", call);
updateCallList();
}
@Override
public void onChildrenChanged(Call call, List<Call> children) {
L.d(TAG, "onChildrenChanged %s", call);
updateCallList();
}
};
public InCallViewModel(@NonNull Application application) {
super(application);
mContext = application.getApplicationContext();
mConferenceCallListLiveData = new MutableLiveData<>();
mIncomingCallLiveData = new MutableLiveData<>();
mOngoingCallListLiveData = new MutableLiveData<>();
mCallAudioStateLiveData = new MutableLiveData<>();
mCallComparator = new CallComparator();
mCallListLiveData = new MutableLiveData<List<Call>>() {
@Override
public void setValue(List<Call> callList) {
super.setValue(callList);
List<Call> activeCallList = filter(callList,
call -> call != null && call.getState() != Call.STATE_RINGING);
activeCallList.sort(mCallComparator);
List<Call> conferenceList = filter(activeCallList,
call -> call.getParent() != null);
List<Call> ongoingCallList = filter(activeCallList,
call -> call.getParent() == null);
mConferenceCallListLiveData.setValue(conferenceList);
mOngoingCallListLiveData.setValue(ongoingCallList);
if (mInCallService != null) {
mInCallService.maybeStartInCallActivity(ongoingCallList);
}
mIncomingCallLiveData.setValue(firstMatch(callList,
call -> call != null && call.getState() == Call.STATE_RINGING));
L.d(TAG, "size:" + activeCallList.size() + " activeList" + activeCallList);
L.d(TAG, "conf:%s" + conferenceList, conferenceList.size());
L.d(TAG, "ongoing:%s" + ongoingCallList, ongoingCallList.size());
}
};
mConferenceCallDetailListLiveData = Transformations.map(mConferenceCallListLiveData,
callList -> {
List<CallDetail> detailList = new ArrayList<>();
for (Call call : callList) {
detailList.add(CallDetail.fromTelecomCallDetail(call.getDetails()));
}
return detailList;
});
mCallDetailLiveData = new CallDetailLiveData();
mPrimaryCallLiveData = Transformations.map(mOngoingCallListLiveData, input -> {
Call call = input.isEmpty() ? null : input.get(0);
mCallDetailLiveData.setTelecomCall(call);
return call;
});
mCallStateLiveData = Transformations.switchMap(mPrimaryCallLiveData,
input -> input != null ? new CallStateLiveData(input) : null);
mCallConnectTimeLiveData = Transformations.map(mCallDetailLiveData, (details) -> {
if (details == null) {
return 0L;
}
return details.getConnectTimeMillis();
});
mCallStateAndConnectTimeLiveData =
LiveDataFunctions.pair(mCallStateLiveData, mCallConnectTimeLiveData);
mSecondaryCallDetailLiveData = new CallDetailLiveData();
mSecondaryCallLiveData = Transformations.map(mOngoingCallListLiveData, callList -> {
Call call = (callList != null && callList.size() > 1) ? callList.get(1) : null;
mSecondaryCallDetailLiveData.setTelecomCall(call);
return call;
});
mSecondaryCallConnectTimeLiveData = Transformations.map(mSecondaryCallDetailLiveData,
details -> {
if (details == null) {
return 0L;
}
return details.getConnectTimeMillis();
});
mOngoingCallPairLiveData = LiveDataFunctions.pair(mPrimaryCallLiveData,
mSecondaryCallLiveData);
mAudioRouteLiveData = new AudioRouteLiveData(mContext);
mDialpadIsOpen = new MutableLiveData<>();
// Set initial value to avoid NPE
mDialpadIsOpen.setValue(false);
mShowOnholdCall = new ShowOnholdCallLiveData(mSecondaryCallLiveData, mDialpadIsOpen);
Intent intent = new Intent(mContext, InCallServiceImpl.class);
intent.setAction(InCallServiceImpl.ACTION_LOCAL_BIND);
mContext.bindService(intent, mInCallServiceConnection, Context.BIND_AUTO_CREATE);
}
/** Merge primary and secondary calls into a conference */
public void mergeConference() {
Call call = mPrimaryCallLiveData.getValue();
Call otherCall = mSecondaryCallLiveData.getValue();
if (call == null || otherCall == null) {
return;
}
call.conference(otherCall);
}
/** Returns the live data which monitors conference calls */
public LiveData<List<CallDetail>> getConferenceCallDetailList() {
return mConferenceCallDetailListLiveData;
}
/** Returns the live data which monitors all the calls. */
public LiveData<List<Call>> getAllCallList() {
return mCallListLiveData;
}
/** Returns the live data which monitors the current incoming call. */
public LiveData<Call> getIncomingCall() {
return mIncomingCallLiveData;
}
/** Returns {@link LiveData} for the ongoing call list which excludes the ringing call. */
public LiveData<List<Call>> getOngoingCallList() {
return mOngoingCallListLiveData;
}
/**
* Returns the live data which monitors the primary call details.
*/
public LiveData<CallDetail> getPrimaryCallDetail() {
return mCallDetailLiveData;
}
/**
* Returns the live data which monitors the primary call state.
*/
public LiveData<Integer> getPrimaryCallState() {
return mCallStateLiveData;
}
/**
* Returns the live data which monitors the primary call state and the start time of the call.
*/
public LiveData<Pair<Integer, Long>> getCallStateAndConnectTime() {
return mCallStateAndConnectTimeLiveData;
}
/**
* Returns the live data which monitor the primary call.
* A primary call in the first call in the ongoing call list,
* which is sorted based on {@link CallComparator}.
*/
public LiveData<Call> getPrimaryCall() {
return mPrimaryCallLiveData;
}
/**
* Returns the live data which monitor the secondary call.
* A secondary call in the second call in the ongoing call list,
* which is sorted based on {@link CallComparator}.
* The value will be null if there is no second call in the call list.
*/
public LiveData<Call> getSecondaryCall() {
return mSecondaryCallLiveData;
}
/**
* Returns the live data which monitors the secondary call details.
*/
public LiveData<CallDetail> getSecondaryCallDetail() {
return mSecondaryCallDetailLiveData;
}
/**
* Returns the live data which monitors the secondary call connect time.
*/
public LiveData<Long> getSecondaryCallConnectTime() {
return mSecondaryCallConnectTimeLiveData;
}
/**
* Returns the live data that monitors the primary and secondary calls.
*/
public LiveData<Pair<Call, Call>> getOngoingCallPair() {
return mOngoingCallPairLiveData;
}
/**
* Returns current audio route.
*/
public LiveData<Integer> getAudioRoute() {
return mAudioRouteLiveData;
}
/**
* Returns current call audio state.
*/
public MutableLiveData<CallAudioState> getCallAudioState() {
return mCallAudioStateLiveData;
}
/** Return the {@link MutableLiveData} for dialpad open state. */
public MutableLiveData<Boolean> getDialpadOpenState() {
return mDialpadIsOpen;
}
/** Return the livedata monitors onhold call status. */
public LiveData<Boolean> shouldShowOnholdCall() {
return mShowOnholdCall;
}
@Override
public boolean onTelecomCallAdded(Call telecomCall) {
L.i(TAG, "onTelecomCallAdded %s %s", telecomCall, this);
telecomCall.registerCallback(mCallStateChangedCallback);
updateCallList();
return false;
}
@Override
public boolean onTelecomCallRemoved(Call telecomCall) {
L.i(TAG, "onTelecomCallRemoved %s %s", telecomCall, this);
telecomCall.unregisterCallback(mCallStateChangedCallback);
updateCallList();
return false;
}
@Override
public void onCallAudioStateChanged(CallAudioState callAudioState) {
L.i(TAG, "onCallAudioStateChanged %s %s", callAudioState, this);
mCallAudioStateLiveData.setValue(callAudioState);
}
private void updateCallList() {
List<Call> callList = new ArrayList<>();
callList.addAll(mInCallService.getCalls());
mCallListLiveData.setValue(callList);
}
@Override
protected void onCleared() {
mContext.unbindService(mInCallServiceConnection);
if (mInCallService != null) {
for (Call call : mInCallService.getCalls()) {
call.unregisterCallback(mCallStateChangedCallback);
}
mInCallService.removeActiveCallListChangedCallback(this);
mInCallService.removeCallAudioStateChangedCallback(this);
}
mInCallService = null;
}
private static class CallComparator implements Comparator<Call> {
/**
* The rank of call state. Used for sorting active calls. Rank is listed from lowest to
* highest.
*/
private static final List<Integer> CALL_STATE_RANK = Lists.newArrayList(
Call.STATE_RINGING,
Call.STATE_DISCONNECTED,
Call.STATE_DISCONNECTING,
Call.STATE_NEW,
Call.STATE_CONNECTING,
Call.STATE_SELECT_PHONE_ACCOUNT,
Call.STATE_HOLDING,
Call.STATE_ACTIVE,
Call.STATE_DIALING);
@Override
public int compare(Call call, Call otherCall) {
boolean callHasParent = call.getParent() != null;
boolean otherCallHasParent = otherCall.getParent() != null;
if (callHasParent && !otherCallHasParent) {
return 1;
} else if (!callHasParent && otherCallHasParent) {
return -1;
}
int carCallRank = CALL_STATE_RANK.indexOf(call.getState());
int otherCarCallRank = CALL_STATE_RANK.indexOf(otherCall.getState());
return otherCarCallRank - carCallRank;
}
}
private static Call firstMatch(List<Call> callList, Predicate<Call> predicate) {
List<Call> filteredResults = filter(callList, predicate);
return filteredResults.isEmpty() ? null : filteredResults.get(0);
}
private static List<Call> filter(List<Call> callList, Predicate<Call> predicate) {
if (callList == null || predicate == null) {
return Collections.emptyList();
}
List<Call> filteredResults = new ArrayList<>();
for (Call call : callList) {
if (predicate.apply(call)) {
filteredResults.add(call);
}
}
return filteredResults;
}
private static class ShowOnholdCallLiveData extends MediatorLiveData<Boolean> {
private final LiveData<Call> mSecondaryCallLiveData;
private final MutableLiveData<Boolean> mDialpadIsOpen;
private ShowOnholdCallLiveData(LiveData<Call> secondaryCallLiveData,
MutableLiveData<Boolean> dialpadState) {
mSecondaryCallLiveData = secondaryCallLiveData;
mDialpadIsOpen = dialpadState;
setValue(false);
addSource(mSecondaryCallLiveData, v -> update());
addSource(mDialpadIsOpen, v -> update());
}
private void update() {
Boolean shouldShowOnholdCall = !mDialpadIsOpen.getValue();
Call onholdCall = mSecondaryCallLiveData.getValue();
if (shouldShowOnholdCall && onholdCall != null
&& onholdCall.getState() == Call.STATE_HOLDING) {
setValue(true);
} else {
setValue(false);
}
}
@Override
public void setValue(Boolean newValue) {
// Only set value and notify observers when the value changes.
if (getValue() != newValue) {
super.setValue(newValue);
}
}
}
}