blob: 3d1bc7033e7786d00853dd3c8e128be8f45f7110 [file] [log] [blame]
/*
* Copyright (C) 2013 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.testapps;
import static android.media.AudioAttributes.CONTENT_TYPE_SPEECH;
import static android.media.AudioAttributes.USAGE_VOICE_COMMUNICATION;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.media.AudioAttributes;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.media.ToneGenerator;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import android.telecom.Conference;
import android.telecom.Connection;
import android.telecom.DisconnectCause;
import android.telecom.PhoneAccount;
import android.telecom.ConnectionRequest;
import android.telecom.ConnectionService;
import android.telecom.PhoneAccountHandle;
import android.telecom.TelecomManager;
import android.telecom.VideoProfile;
import android.telecom.Log;
import android.widget.Toast;
import java.lang.String;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import static com.android.server.telecom.testapps.CallServiceNotifier.SIM_SUBSCRIPTION_ID2;
/**
* Service which provides fake calls to test the ConnectionService interface.
*/
public class TestConnectionService extends ConnectionService {
/**
* Intent extra used to pass along the video state for a new test call.
*/
public static final String EXTRA_START_VIDEO_STATE = "extra_start_video_state";
public static final String EXTRA_HANDLE = "extra_handle";
/**
* If an outgoing call ends with 2879 (BUSY), the test CS will indicate the call is busy.
*/
public static final String BUSY_SUFFIX = "2879";
private static final String LOG_TAG = TestConnectionService.class.getSimpleName();
private static TestConnectionService INSTANCE;
/**
* Random number generator used to generate phone numbers.
*/
private Random mRandom = new Random();
private final class TestConference extends Conference {
public TestConference(Connection a, Connection b) {
super(null);
setConnectionCapabilities(
Connection.CAPABILITY_SUPPORT_HOLD |
Connection.CAPABILITY_HOLD |
Connection.CAPABILITY_MUTE |
Connection.CAPABILITY_MANAGE_CONFERENCE);
addConnection(a);
addConnection(b);
a.setConference(this);
b.setConference(this);
setActive();
}
@Override
public void onDisconnect() {
for (Connection c : getConnections()) {
c.setDisconnected(new DisconnectCause(DisconnectCause.REMOTE));
c.destroy();
}
}
@Override
public void onSeparate(Connection connection) {
if (getConnections().contains(connection)) {
connection.setConference(null);
removeConnection(connection);
}
}
@Override
public void onHold() {
for (Connection c : getConnections()) {
c.setOnHold();
}
setOnHold();
}
@Override
public void onUnhold() {
for (Connection c : getConnections()) {
c.setActive();
}
setActive();
}
}
final class TestConnection extends Connection {
private final boolean mIsIncoming;
/** Used to cleanup camera and media when done with connection. */
private TestVideoProvider mTestVideoCallProvider;
private ConnectionRequest mOriginalRequest;
private RttChatbot mRttChatbot;
private BroadcastReceiver mHangupReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
setDisconnected(new DisconnectCause(DisconnectCause.MISSED));
destroyCall(TestConnection.this);
destroy();
}
};
private BroadcastReceiver mUpgradeRequestReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
final int request = Integer.parseInt(intent.getData().getSchemeSpecificPart());
final VideoProfile videoProfile = new VideoProfile(request);
mTestVideoCallProvider.receiveSessionModifyRequest(videoProfile);
}
};
private BroadcastReceiver mRttUpgradeReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
sendRemoteRttRequest();
}
};
TestConnection(boolean isIncoming, ConnectionRequest request) {
mIsIncoming = isIncoming;
mOriginalRequest = request;
// Assume all calls are video capable.
int capabilities = getConnectionCapabilities();
capabilities |= CAPABILITY_SUPPORTS_VT_LOCAL_BIDIRECTIONAL;
capabilities |= CAPABILITY_SUPPORTS_VT_REMOTE_BIDIRECTIONAL;
capabilities |= CAPABILITY_CAN_UPGRADE_TO_VIDEO;
capabilities |= CAPABILITY_MUTE;
capabilities |= CAPABILITY_SUPPORT_HOLD;
capabilities |= CAPABILITY_HOLD;
capabilities |= CAPABILITY_RESPOND_VIA_TEXT;
setConnectionCapabilities(capabilities);
int properties = getConnectionProperties();
if (mOriginalRequest.isRequestingRtt()) {
properties |= PROPERTY_IS_RTT;
}
setConnectionProperties(properties);
if (isIncoming) {
Bundle newExtras = (getExtras() == null) ? new Bundle() : getExtras();
newExtras.putBoolean(Connection.EXTRA_ANSWERING_DROPS_FG_CALL, true);
putExtras(newExtras);
}
LocalBroadcastManager.getInstance(getApplicationContext()).registerReceiver(
mHangupReceiver, new IntentFilter(TestCallActivity.ACTION_HANGUP_CALLS));
final IntentFilter filter =
new IntentFilter(TestCallActivity.ACTION_SEND_UPGRADE_REQUEST);
filter.addDataScheme("int");
LocalBroadcastManager.getInstance(getApplicationContext()).registerReceiver(
mUpgradeRequestReceiver, filter);
LocalBroadcastManager.getInstance(getApplicationContext()).registerReceiver(
mRttUpgradeReceiver,
new IntentFilter(TestCallActivity.ACTION_REMOTE_RTT_UPGRADE));
}
void startOutgoing() {
setDialing();
mHandler.postDelayed(() -> {
if (getAddress().getSchemeSpecificPart().endsWith(BUSY_SUFFIX)) {
setDisconnected(new DisconnectCause(DisconnectCause.REMOTE, "Line busy",
"Line busy", "Line busy", ToneGenerator.TONE_SUP_BUSY));
destroyCall(this);
destroy();
} else {
setActive();
activateCall(TestConnection.this);
}
}, 4000);
if (mOriginalRequest.isRequestingRtt()) {
Log.i(LOG_TAG, "Is RTT call. Starting chatbot service.");
mRttChatbot = new RttChatbot(getApplicationContext(),
mOriginalRequest.getRttTextStream());
mRttChatbot.start();
}
}
/** ${inheritDoc} */
@Override
public void onAbort() {
destroyCall(this);
destroy();
}
/** ${inheritDoc} */
@Override
public void onAnswer(int videoState) {
setVideoState(videoState);
activateCall(this);
setActive();
updateConferenceable();
if (mOriginalRequest.isRequestingRtt()) {
Log.i(LOG_TAG, "Is RTT call. Starting chatbot service.");
mRttChatbot = new RttChatbot(getApplicationContext(),
mOriginalRequest.getRttTextStream());
mRttChatbot.start();
}
}
/** ${inheritDoc} */
@Override
public void onPlayDtmfTone(char c) {
if (c == '1') {
setDialing();
}
}
/** ${inheritDoc} */
@Override
public void onStopDtmfTone() { }
/** ${inheritDoc} */
@Override
public void onDisconnect() {
setDisconnected(new DisconnectCause(DisconnectCause.REMOTE));
destroyCall(this);
destroy();
}
/** ${inheritDoc} */
@Override
public void onHold() {
setOnHold();
}
/** ${inheritDoc} */
@Override
public void onReject() {
setDisconnected(new DisconnectCause(DisconnectCause.REJECTED));
destroyCall(this);
destroy();
}
/** ${inheritDoc} */
@Override
public void onUnhold() {
setActive();
}
@Override
public void onStopRtt() {
int newProperties = getConnectionProperties() & ~PROPERTY_IS_RTT;
setConnectionProperties(newProperties);
mRttChatbot.stop();
mRttChatbot = null;
}
@Override
public void handleRttUpgradeResponse(RttTextStream rttTextStream) {
Log.i(this, "RTT request response was %s", rttTextStream == null);
if (rttTextStream != null) {
mRttChatbot = new RttChatbot(getApplicationContext(), rttTextStream);
mRttChatbot.start();
sendRttInitiationSuccess();
}
}
@Override
public void onStartRtt(RttTextStream textStream) {
boolean doAccept = Math.random() < 0.5;
if (doAccept) {
Log.i(this, "Accepting RTT request.");
mRttChatbot = new RttChatbot(getApplicationContext(), textStream);
mRttChatbot.start();
sendRttInitiationSuccess();
} else {
sendRttInitiationFailure(RttModifyStatus.SESSION_MODIFY_REQUEST_FAIL);
}
}
public void setTestVideoCallProvider(TestVideoProvider testVideoCallProvider) {
mTestVideoCallProvider = testVideoCallProvider;
}
public void cleanup() {
LocalBroadcastManager.getInstance(getApplicationContext()).unregisterReceiver(
mHangupReceiver);
LocalBroadcastManager.getInstance(getApplicationContext()).unregisterReceiver(
mUpgradeRequestReceiver);
}
/**
* Stops playback of test videos.
*/
private void stopAndCleanupMedia() {
if (mTestVideoCallProvider != null) {
mTestVideoCallProvider.stopAndCleanupMedia();
mTestVideoCallProvider.stopCamera();
}
}
}
private final List<TestConnection> mCalls = new ArrayList<>();
private final Handler mHandler = new Handler();
/** Used to play an audio tone during a call. */
private MediaPlayer mMediaPlayer;
@Override
public void onCreate() {
INSTANCE = this;
}
@Override
public boolean onUnbind(Intent intent) {
log("onUnbind");
mMediaPlayer = null;
return super.onUnbind(intent);
}
@Override
public void onConference(Connection a, Connection b) {
addConference(new TestConference(a, b));
}
@Override
public Connection onCreateOutgoingConnection(
PhoneAccountHandle connectionManagerAccount,
final ConnectionRequest originalRequest) {
final Uri handle = originalRequest.getAddress();
String number = originalRequest.getAddress().getSchemeSpecificPart();
log("call, number: " + number);
// Crash on 555-DEAD to test call service crashing.
if ("5550340".equals(number)) {
throw new RuntimeException("Goodbye, cruel world.");
}
Bundle extras = originalRequest.getExtras();
String gatewayPackage = extras.getString(TelecomManager.GATEWAY_PROVIDER_PACKAGE);
Uri originalHandle = extras.getParcelable(TelecomManager.GATEWAY_ORIGINAL_ADDRESS);
if (extras.containsKey(TelecomManager.EXTRA_CALL_SUBJECT)) {
String callSubject = extras.getString(TelecomManager.EXTRA_CALL_SUBJECT);
log("Got subject: " + callSubject);
Toast.makeText(getApplicationContext(), "Got subject :" + callSubject,
Toast.LENGTH_SHORT).show();
}
log("gateway package [" + gatewayPackage + "], original handle [" +
originalHandle + "]");
final TestConnection connection =
new TestConnection(false /* isIncoming */, originalRequest);
setAddress(connection, handle);
// If the number starts with 555, then we handle it ourselves. If not, then we
// use a remote connection service.
// TODO: Have a special phone number to test the account-picker dialog flow.
if (number != null && number.startsWith("555")) {
// Normally we would use the original request as is, but for testing purposes, we are
// adding ".." to the end of the number to follow its path more easily through the logs.
final ConnectionRequest request = new ConnectionRequest(
originalRequest.getAccountHandle(),
Uri.fromParts(handle.getScheme(),
handle.getSchemeSpecificPart() + "..", ""),
originalRequest.getExtras(),
originalRequest.getVideoState());
connection.setVideoState(originalRequest.getVideoState());
addVideoProvider(connection);
addCall(connection);
connection.startOutgoing();
for (Connection c : getAllConnections()) {
c.setOnHold();
}
} else {
log("Not a test number");
}
return connection;
}
@Override
public Connection onCreateIncomingConnection(
PhoneAccountHandle connectionManagerAccount,
final ConnectionRequest request) {
PhoneAccountHandle accountHandle = request.getAccountHandle();
ComponentName componentName = new ComponentName(this, TestConnectionService.class);
if (accountHandle != null && componentName.equals(accountHandle.getComponentName())) {
final TestConnection connection = new TestConnection(true, request);
// Get the stashed intent extra that determines if this is a video call or audio call.
Bundle extras = request.getExtras();
int videoState = extras.getInt(EXTRA_START_VIDEO_STATE, VideoProfile.STATE_AUDIO_ONLY);
Uri providedHandle = extras.getParcelable(EXTRA_HANDLE);
// Use test number for testing incoming calls.
Uri address = providedHandle == null ?
Uri.fromParts(PhoneAccount.SCHEME_TEL, getRandomNumber(
VideoProfile.isVideo(videoState)), null)
: providedHandle;
connection.setVideoState(videoState);
Bundle connectionExtras = connection.getExtras();
if (connectionExtras == null) {
connectionExtras = new Bundle();
}
// Randomly choose a varying length call subject.
int subjectFormat = mRandom.nextInt(3);
if (subjectFormat == 0) {
connectionExtras.putString(Connection.EXTRA_CALL_SUBJECT,
"This is a test of call subject lines. Subjects for a call can be long " +
" and can go even longer.");
} else if (subjectFormat == 1) {
connectionExtras.putString(Connection.EXTRA_CALL_SUBJECT,
"This is a test of call subject lines.");
}
connection.putExtras(connectionExtras);
setAddress(connection, address);
addVideoProvider(connection);
addCall(connection);
connection.setVideoState(videoState);
return connection;
} else {
return Connection.createFailedConnection(new DisconnectCause(DisconnectCause.ERROR,
"Invalid inputs: " + accountHandle + " " + componentName));
}
}
@Override
public Connection onCreateUnknownConnection(PhoneAccountHandle connectionManagerPhoneAccount,
final ConnectionRequest request) {
PhoneAccountHandle accountHandle = request.getAccountHandle();
ComponentName componentName = new ComponentName(this, TestConnectionService.class);
if (accountHandle != null && componentName.equals(accountHandle.getComponentName())) {
final TestConnection connection = new TestConnection(false, request);
final Bundle extras = request.getExtras();
final Uri providedHandle = extras.getParcelable(EXTRA_HANDLE);
Uri handle = providedHandle == null ?
Uri.fromParts(PhoneAccount.SCHEME_TEL, getRandomNumber(false), null)
: providedHandle;
connection.setAddress(handle, TelecomManager.PRESENTATION_ALLOWED);
connection.setDialing();
addCall(connection);
return connection;
} else {
return Connection.createFailedConnection(new DisconnectCause(DisconnectCause.ERROR,
"Invalid inputs: " + accountHandle + " " + componentName));
}
}
public static TestConnectionService getInstance() {
return INSTANCE;
}
public void switchPhoneAccount() {
if (!mCalls.isEmpty()) {
TestConnection c = mCalls.get(0);
c.notifyPhoneAccountChanged(CallServiceNotifier.getInstance()
.getPhoneAccountHandle(SIM_SUBSCRIPTION_ID2));
} else {
Log.i(this, "Couldn't switch PhoneAccount, call is null!");
}
}
public void switchPhoneAccountWrong() {
PhoneAccountHandle pah = new PhoneAccountHandle(
new ComponentName("com.android.phone",
"com.android.services.telephony.TelephonyConnectionService"), "TEST");
if (!mCalls.isEmpty()) {
TestConnection c = mCalls.get(0);
try {
c.notifyPhoneAccountChanged(pah);
} catch (SecurityException e) {
Toast.makeText(getApplicationContext(), "SwitchPhoneAccount: Pass",
Toast.LENGTH_SHORT).show();
}
} else {
Log.i(this, "Couldn't switch PhoneAccount, call is null!");
}
}
private void addVideoProvider(TestConnection connection) {
TestVideoProvider testVideoCallProvider =
new TestVideoProvider(getApplicationContext(), connection);
connection.setVideoProvider(testVideoCallProvider);
// Keep reference to original so we can clean up the media players later.
connection.setTestVideoCallProvider(testVideoCallProvider);
}
private void activateCall(TestConnection connection) {
if (mMediaPlayer == null) {
mMediaPlayer = createMediaPlayer();
}
if (!mMediaPlayer.isPlaying()) {
mMediaPlayer.start();
}
}
private void destroyCall(TestConnection connection) {
connection.cleanup();
mCalls.remove(connection);
// Ensure any playing media and camera resources are released.
connection.stopAndCleanupMedia();
// Stops audio if there are no more calls.
if (mCalls.isEmpty() && mMediaPlayer != null && mMediaPlayer.isPlaying()) {
mMediaPlayer.stop();
mMediaPlayer.release();
mMediaPlayer = createMediaPlayer();
}
updateConferenceable();
}
private void addCall(TestConnection connection) {
mCalls.add(connection);
updateConferenceable();
}
private void updateConferenceable() {
List<Connection> freeConnections = new ArrayList<>();
freeConnections.addAll(mCalls);
for (int i = 0; i < freeConnections.size(); i++) {
if (freeConnections.get(i).getConference() != null) {
freeConnections.remove(i);
}
}
for (int i = 0; i < freeConnections.size(); i++) {
Connection c = freeConnections.remove(i);
c.setConferenceableConnections(freeConnections);
freeConnections.add(i, c);
}
}
private void setAddress(Connection connection, Uri address) {
connection.setAddress(address, TelecomManager.PRESENTATION_ALLOWED);
if ("5551234".equals(address.getSchemeSpecificPart())) {
connection.setCallerDisplayName("Hello World", TelecomManager.PRESENTATION_ALLOWED);
}
}
private MediaPlayer createMediaPlayer() {
AudioAttributes attributes = new AudioAttributes.Builder()
.setUsage(USAGE_VOICE_COMMUNICATION)
.setContentType(CONTENT_TYPE_SPEECH)
.build();
final int audioSessionId = ((AudioManager) getSystemService(
Context.AUDIO_SERVICE)).generateAudioSessionId();
// Prepare the media player to play a tone when there is a call.
MediaPlayer mediaPlayer = MediaPlayer.create(getApplicationContext(), R.raw.beep_boop, attributes,
audioSessionId);
mediaPlayer.setLooping(true);
return mediaPlayer;
}
private static void log(String msg) {
Log.w("telecomtestcs", "[TestConnectionService] " + msg);
}
/**
* Generates a random phone number of format 555YXXX. Where Y will be {@code 1} if the
* phone number is for a video call and {@code 0} for an audio call. XXX is a randomly
* generated phone number.
*
* @param isVideo {@code True} if the call is a video call.
* @return The phone number.
*/
private String getRandomNumber(boolean isVideo) {
int videoDigit = isVideo ? 1 : 0;
int number = mRandom.nextInt(999);
return String.format("555%s%03d", videoDigit, number);
}
}