blob: e63ed96b56a21342a768bd2d3a48428c5f275c07 [file] [log] [blame]
/*
* Copyright (C) 2021 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.server.telecom;
import static android.telecom.Call.Callback.ANSWER_FAILED_ENDPOINT_REJECTED;
import static android.telecom.Call.Callback.ANSWER_FAILED_ENDPOINT_TIMEOUT;
import static android.telecom.Call.Callback.ANSWER_FAILED_ENDPOINT_UNAVAILABLE;
import static android.telecom.Call.Callback.ANSWER_FAILED_UNKNOWN_REASON;
import static android.telecom.Call.Callback.PUSH_FAILED_ENDPOINT_REJECTED;
import static android.telecom.Call.Callback.PUSH_FAILED_ENDPOINT_TIMEOUT;
import static android.telecom.Call.Callback.PUSH_FAILED_ENDPOINT_UNAVAILABLE;
import static android.telecom.Call.Callback.PUSH_FAILED_UNKNOWN_REASON;
import static android.telecom.Call.STATE_ACTIVE;
import android.content.Context;
import android.os.RemoteException;
import android.telecom.CallEndpoint;
import android.telecom.CallEndpointSession;
import android.telecom.Connection;
import android.telecom.Log;
import android.telecom.Logging.Runnable;
import android.telecom.VideoProfile;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.telecom.bluetooth.BluetoothRouteManager;
import com.android.server.telecom.bluetooth.BluetoothStateReceiver;
import com.android.server.telecom.ui.AudioProcessingNotification;
import com.android.server.telecom.ui.DisconnectedCallNotifier;
import com.android.server.telecom.ui.ToastFactory;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class CallEndpointController extends CallsManagerListenerBase {
private static final long ENDPOINT_TIMEOUT = 5000L;
private final TelecomSystem.SyncRoot mTelecomLock;
private final CallsManager mCallsManager;
private final Set<CallEndpoint> mCallEndpoints;
private final InCallController mInCallController;
private final HashMap<Call, CallEndpointSessionTracker> mSessionsByCall = new HashMap<>();
private final CallsManagerAdapter mCallsManagerAdapter;
private final Object mLock;
private final class CallsManagerAdapter {
private final CallsManager mCallsManager;
/**
* Initializes the required Telecom components.
*/
public CallsManagerAdapter(CallsManager callsManager) {
mCallsManager = callsManager;
}
public void updateAvailableCallEndpoints(Set<CallEndpoint> availableCallEndpoints) {
mCallsManager.updateAvailableCallEndpoints(availableCallEndpoints);
}
}
private final SystemStateHelper.SystemStateListener mSystemStateListener =
new SystemStateHelper.SystemStateListener() {
@Override
public void onCarModeChanged(int priority, String packageName, boolean isCarMode) {
// ignore
}
@Override
public void onAutomotiveProjectionStateSet(String automotiveProjectionPackage) {
// ignore
}
@Override
public void onAutomotiveProjectionStateReleased() {
// ignore
}
@Override
public void onPackageUninstalled(String packageName) {
// Handle uninstalled packages
Set<CallEndpoint> removed = new HashSet<>();
synchronized (mLock) {
for (CallEndpoint callEndpoint : mCallEndpoints) {
if (callEndpoint.getComponentName() != null
&& callEndpoint.getComponentName().getPackageName().equals(
packageName)) {
mSessionsByCall.forEach((call, session) -> {
if (session.getCallEndpoint().equals(callEndpoint)) {
notifySessionDeactivated(call);
mCallsManager.disconnectCall(call);
}
});
removed.add(callEndpoint);
}
}
}
if (!removed.isEmpty()) {
unregisterCallEndpoints(new ArrayList<>(removed));
}
}
};
public CallEndpointController(TelecomSystem.SyncRoot lock, CallsManager callsManager) {
mTelecomLock = lock;
mCallsManager = callsManager;
mCallsManager.getSystemStateHelper().addListener(mSystemStateListener);
mInCallController = callsManager.getInCallController();
mCallEndpoints = new HashSet<>();
mLock = new Object();
mCallsManager.addListener(this);
mCallsManagerAdapter = new CallsManagerAdapter(callsManager);
}
public void registerCallEndpoints(List<CallEndpoint> endpoints) {
boolean updated = false;
synchronized (mLock) {
for (CallEndpoint e : endpoints) {
updated ^= mCallEndpoints.add(e);
}
}
if (updated) {
mCallsManagerAdapter.updateAvailableCallEndpoints(Set.copyOf(mCallEndpoints));
}
}
public void unregisterCallEndpoints(List<CallEndpoint> endpoints) {
boolean updated = false;
synchronized (mLock) {
for (CallEndpoint e : endpoints) {
updated ^= mCallEndpoints.remove(e);
}
}
if (updated) {
mCallsManagerAdapter.updateAvailableCallEndpoints(Set.copyOf(mCallEndpoints));
}
}
public void requestPlaceCall(Call call, CallEndpoint callEndpoint) {
synchronized (mLock) {
if (!mCallEndpoints.contains(callEndpoint)) {
// unavailable call endpoint: disconnect the call or fallback to normal call?
// Current implementation: disconnect the call.
Log.i(this, "requestPlaceCall failed due to unavailable callEndpoint: "
+ callEndpoint);
mCallsManager.disconnectCall(call);
return;
}
InternalCallEndpointSession session = new InternalCallEndpointSession(call,
callEndpoint, this);
CallEndpointSessionTracker tracker = new CallEndpointSessionTracker(session,
CallEndpointSession.PLACE_REQUEST);
call.setActiveCallEndpoint(callEndpoint);
mInCallController.requestStreamingCall(tracker);
mSessionsByCall.put(call, tracker);
tracker.getHandler().postDelayed(new Runnable("CEC.rPlaceC", mTelecomLock) {
@Override
public void loggedRun() {
// Check if the request have been handled. If not, notify timeout to the
// endpoint and dialer call.
if (!tracker.isRequestHandled()) {
onCallEndpointSessionActivationTimedout(call);
mCallsManager.disconnectCall(call);
}
}
}.prepare(), ENDPOINT_TIMEOUT);
}
}
public void requestAnswerCall(Call call, CallEndpoint callEndpoint,
@VideoProfile.VideoState int videoState) {
synchronized (mLock) {
if (!mCallEndpoints.contains(callEndpoint)) {
// unavailable call endpoint
Log.i(this, "requestAnswerCall failed due to unavailable callEndpoint: "
+ callEndpoint);
call.onAnswerFailed(callEndpoint, ANSWER_FAILED_ENDPOINT_UNAVAILABLE);
mCallsManager.disconnectCall(call);
return;
}
InternalCallEndpointSession session = new InternalCallEndpointSession(call,
callEndpoint, this);
CallEndpointSessionTracker tracker = new CallEndpointSessionTracker(session,
CallEndpointSession.ANSWER_REQUEST, videoState);
// Set this before bind to streaming app to guarantee audio route changed in time.
call.setActiveCallEndpoint(callEndpoint);
mCallsManager.answerCall(call, videoState);
mInCallController.requestStreamingCall(tracker);
mSessionsByCall.put(call, tracker);
tracker.getHandler().postDelayed(new Runnable("CEC.rAC", mTelecomLock) {
@Override
public void loggedRun() {
// Check if the request have been handled. If not, notify timeout to the
// endpoint and dialer call.
if (!tracker.isRequestHandled()) {
onCallEndpointSessionActivationTimedout(call);
call.onAnswerFailed(callEndpoint, ANSWER_FAILED_ENDPOINT_TIMEOUT);
mCallsManager.disconnectCall(call);
}
}
}.prepare(), ENDPOINT_TIMEOUT);
}
}
public void requestPushCall(Call call, CallEndpoint callEndpoint) {
synchronized (mLock) {
if (call.getState() != STATE_ACTIVE) {
// ignore
return;
}
if (!mCallEndpoints.contains(callEndpoint)) {
// unavailable call endpoint
Log.i(this, "requestPushCall failed due to unavailable callEndpoint: "
+ callEndpoint);
call.onPushFailed(callEndpoint, PUSH_FAILED_ENDPOINT_UNAVAILABLE);
mCallsManager.disconnectCall(call);
return;
}
InternalCallEndpointSession session = new InternalCallEndpointSession(call,
callEndpoint, this);
CallEndpointSessionTracker tracker = new CallEndpointSessionTracker(session,
CallEndpointSession.PUSH_REQUEST);
call.setActiveCallEndpoint(callEndpoint);
mSessionsByCall.put(call, tracker);
mInCallController.requestStreamingCall(tracker);
tracker.getHandler().postDelayed(new Runnable("CEC.rPushC", mTelecomLock) {
@Override
public void loggedRun() {
// Check if the request have been handled. If not, notify timeout to the
// endpoint and dialer call.
if (!tracker.isRequestHandled()) {
onCallEndpointSessionActivationTimedout(call);
call.onPushFailed(callEndpoint, PUSH_FAILED_ENDPOINT_TIMEOUT);
mCallsManager.disconnectCall(call);
}
}
}.prepare(), ENDPOINT_TIMEOUT);
}
}
public Set<CallEndpoint> getCallEndpoints() {
synchronized (mLock) {
return Collections.unmodifiableSet(mCallEndpoints);
}
}
public CallsManager getCallsManager() {
return mCallsManager;
}
@VisibleForTesting
public SystemStateHelper.SystemStateListener getSystemStateListener() {
return mSystemStateListener;
}
@Override
public void onCallStateChanged(Call call, int oldState, int newState) {
if (oldState != newState && newState == CallState.DISCONNECTED) {
synchronized (mLock) {
if (mSessionsByCall.remove(call) != null) {
Log.i(this, "Stop track call endpoint session of disconnected call %s",
call);
}
}
}
}
@Override
public void onCallRemoved(Call call) {
synchronized (mLock) {
if (mSessionsByCall.remove(call) != null) {
Log.i(this, "Stop track call endpoint session of removed call %s",
call);
}
}
}
public void onCallEndpointSessionActivationFailed(Call call, int reason) {
synchronized (mLock) {
CallEndpointSessionTracker tracker = mSessionsByCall.get(call);
if (tracker != null) {
CallEndpoint callEndpoint = tracker.getCallEndpoint();
tracker.setRequestHandled(true);
call.setActiveCallEndpoint(null);
if (reason == android.telecom.CallEndpointSession.ACTIVATION_FAILURE_UNAVAILABLE) {
Log.i(this, "Call endpoint session activation failed due to " +
"unavailable call endpoint: %s", callEndpoint);
if (tracker.getRequestType() == CallEndpointSession.ANSWER_REQUEST) {
call.onAnswerFailed(callEndpoint, ANSWER_FAILED_ENDPOINT_UNAVAILABLE);
} else if (tracker.getRequestType() == CallEndpointSession.PUSH_REQUEST) {
call.onPushFailed(callEndpoint, PUSH_FAILED_ENDPOINT_UNAVAILABLE);
}
mCallsManager.disconnectCall(call);
} else if (reason
== android.telecom.CallEndpointSession.ACTIVATION_FAILURE_REJECTED) {
Log.i(this, "Call endpoint session activation failed due to " +
"call endpoint rejection: %s", callEndpoint);
if (tracker.getRequestType() == CallEndpointSession.ANSWER_REQUEST) {
call.onAnswerFailed(callEndpoint, ANSWER_FAILED_ENDPOINT_REJECTED);
} else if (tracker.getRequestType() == CallEndpointSession.PUSH_REQUEST) {
call.onPushFailed(callEndpoint, PUSH_FAILED_ENDPOINT_REJECTED);
}
mCallsManager.disconnectCall(call);
} else {
// Unexpected
Log.i(this, "Call endpoint session activation failed due to unknown reason %s",
reason);
if (tracker.getRequestType() == CallEndpointSession.ANSWER_REQUEST) {
call.onAnswerFailed(callEndpoint, ANSWER_FAILED_UNKNOWN_REASON);
} else if (tracker.getRequestType() == CallEndpointSession.PUSH_REQUEST) {
call.onPushFailed(callEndpoint,
PUSH_FAILED_UNKNOWN_REASON);
}
mCallsManager.disconnectCall(call);
}
} else {
Log.w(this, "onCallEndpointSessionActivationFailed, unknown call %s", call);
}
}
}
public void onCallEndpointSessionActivated(Call call) {
synchronized (mLock) {
CallEndpointSessionTracker tracker = mSessionsByCall.get(call);
if (tracker != null) {
tracker.setRequestHandled(true);
mCallsManager.getInCallController().unbindFromDialer();
call.setTethered(true);
call.setConnectionCapabilities(call.getConnectionCapabilities()
| Connection.CAPABILITY_CAN_PULL_CALL);
} else {
Log.w(this, "onCallEndpointSessionActivated, unknown call %s", call);
}
}
}
public void onCallEndpointSessionDeactivated(Call call) {
synchronized (mLock) {
CallEndpointSessionTracker tracker = mSessionsByCall.get(call);
if (tracker != null) {
tracker.setRequestHandled(true);
call.setActiveCallEndpoint(null);
notifySessionDeactivated(call);
mCallsManager.disconnectCall(call);
} else {
Log.w(this, "onCallEndpointSessionDeactivated, unknown call %s", call);
}
}
}
public void onCallEndpointSessionActivationTimedout(Call call) {
synchronized (mLock) {
CallEndpointSessionTracker tracker = mSessionsByCall.get(call);
if (tracker != null) {
CallEndpoint callEndpoint = tracker.getCallEndpoint();
Log.i(this, "Call endpoint session activation request %s failed due to timeout",
tracker.getRequestType());
if (tracker.getRequestType() == CallEndpointSession.ANSWER_REQUEST) {
call.onAnswerFailed(callEndpoint, ANSWER_FAILED_ENDPOINT_TIMEOUT);
} else if (tracker.getRequestType() == CallEndpointSession.PUSH_REQUEST) {
call.onPushFailed(callEndpoint, PUSH_FAILED_ENDPOINT_TIMEOUT);
}
if (tracker.getCallEndpointCallback() != null) {
try {
tracker.getCallEndpointCallback().onCallEndpointSessionActivationTimeout();
} catch (RemoteException e) {
Log.e(this, e, "Failed to notify call endpoint session activation timeout");
}
}
mCallsManager.disconnectCall(call);
} else {
Log.w(this, "onCallEndpointSessionActivationTimedout, unknown call %s", call);
}
}
}
public void notifySessionDeactivated(Call call) {
synchronized (mLock) {
CallEndpointSessionTracker tracker = mSessionsByCall.get(call);
if (tracker != null) {
try {
tracker.getCallEndpointCallback().onCallEndpointSessionDeactivated();
} catch (RemoteException e) {
Log.e(this, e, "Failed to notify call endpoint session deactivation");
}
} else {
Log.w(this, "notifySessionDeactivated, unknown call %s", call);
}
}
}
@VisibleForTesting
public CallEndpointSessionTracker getSessionTrackerByCall(Call call) {
return mSessionsByCall.get(call);
}
}