Add notif. and foreground service to transactional test app
bug: 278098182
bug: 277930604
Test: test app / manual
Change-Id: I5e66f28f4c01319df2f4ab6f2e495302c3bb5ab5
diff --git a/testapps/transactionalVoipApp/AndroidManifest.xml b/testapps/transactionalVoipApp/AndroidManifest.xml
index d0aa50b..e4968db 100644
--- a/testapps/transactionalVoipApp/AndroidManifest.xml
+++ b/testapps/transactionalVoipApp/AndroidManifest.xml
@@ -15,13 +15,20 @@
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- coreApp="true"
- package="com.android.server.telecom.transactionalVoipApp">
+ coreApp="true"
+ package="com.android.server.telecom.transactionalVoipApp">
<uses-sdk android:minSdkVersion="28"
- android:targetSdkVersion="33"/>
+ android:targetSdkVersion="33"/>
- <uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />
+ <uses-permission android:name="android.permission.MANAGE_OWN_CALLS"/>
+ <!-- Needed to test media/audio -->
+ <uses-permission android:name="android.permission.RECORD_AUDIO"/>
+ <!-- Needed for foreground services -->
+ <uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT"/>
+ <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE_PHONE_CALL"/>
<application android:label="Transactional Voip">
<uses-library android:name="android.test.runner"/>
@@ -30,10 +37,26 @@
android:exported="true"
android:label="Transactional Voip">
<intent-filter>
- <action android:name="android.intent.action.MAIN" />
- <category android:name="android.intent.category.LAUNCHER" />
+ <action android:name="android.intent.action.MAIN"/>
+ <category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
+ <activity android:name="com.android.server.telecom.transactionalVoipApp.InCallActivity"
+ android:exported="true"
+ android:launchMode="singleInstance"
+ android:label="InCall VoIP Activity">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN"/>
+ <category android:name="android.intent.category.LAUNCHER"/>
+ </intent-filter>
+ </activity>
+
+ <service
+ android:name=".BackgroundIncomingCallService"
+ android:foregroundServiceType="phoneCall"
+ android:exported="false"
+ />
+
</application>
</manifest>
diff --git a/testapps/transactionalVoipApp/res/layout/in_call_activity.xml b/testapps/transactionalVoipApp/res/layout/in_call_activity.xml
new file mode 100644
index 0000000..54d467e
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/layout/in_call_activity.xml
@@ -0,0 +1,97 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2022 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.
+ -->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@+id/getCallIdTextView"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/get_call_id"
+ />
+
+ <Button
+ android:id="@+id/answer_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/answer"/>
+
+ <Button
+ android:id="@+id/set_call_active_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/set_call_active"/>
+
+ <Button
+ android:id="@+id/set_call_inactive_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/set_call_inactive"/>
+
+ <Button
+ android:id="@+id/disconnect_call_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/disconnect_call"/>
+
+ <Button
+ android:id="@+id/start_stream_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/start_stream"/>
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@+id/current_endpoint"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ />
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <Button
+ android:id="@+id/request_earpiece"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/request_earpiece_endpoint"/>
+
+ <Button
+ android:id="@+id/request_speaker"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/request_speaker_endpoint"/>
+
+ <Button
+ android:id="@+id/request_bluetooth"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/request_bluetooth_endpoint"/>
+ </LinearLayout>
+
+ </LinearLayout>
+</LinearLayout>
diff --git a/testapps/transactionalVoipApp/res/layout/main_activity.xml b/testapps/transactionalVoipApp/res/layout/main_activity.xml
index 86d8e20..28f0744 100644
--- a/testapps/transactionalVoipApp/res/layout/main_activity.xml
+++ b/testapps/transactionalVoipApp/res/layout/main_activity.xml
@@ -38,53 +38,31 @@
android:layout_height="wrap_content"
android:text="@string/register_phone_account"/>
- <ToggleButton
- android:id="@+id/callDirectionButton"
+ <Button
+ android:id="@+id/startForegroundService"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:textOff="@string/direction_outgoing"
- android:textOn="@string/direction_incoming"
+ android:text="@string/start_foreground_service"
/>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
- <Button
- android:id="@+id/add_call_1_button"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="@string/add_call_1"/>
<Button
- android:id="@+id/disconnect_call_1_button"
+ android:id="@+id/startOutgoingCall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:text="@string/disconnect_call_1"/>
- </LinearLayout>
-
- <LinearLayout
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:orientation="horizontal">
+ android:text="@string/start_outgoing"
+ />
<Button
- android:id="@+id/add_call_2_button"
+ android:id="@+id/startIncomingCall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:text="@string/add_call_2"/>
-
- <Button
- android:id="@+id/set_call_2_active_button"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="@string/set_call_active"/>
-
- <Button
- android:id="@+id/disconnect_call_2_button"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="@string/disconnect_call_2"/>
+ android:text="@string/start_incoming"
+ />
</LinearLayout>
</LinearLayout>
</LinearLayout>
diff --git a/testapps/transactionalVoipApp/res/raw/sample_audio.ogg b/testapps/transactionalVoipApp/res/raw/sample_audio.ogg
new file mode 100644
index 0000000..0129b46
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/raw/sample_audio.ogg
Binary files differ
diff --git a/testapps/transactionalVoipApp/res/raw/sample_audio2.ogg b/testapps/transactionalVoipApp/res/raw/sample_audio2.ogg
new file mode 100644
index 0000000..a0b39b4
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/raw/sample_audio2.ogg
Binary files differ
diff --git a/testapps/transactionalVoipApp/res/values/strings.xml b/testapps/transactionalVoipApp/res/values/strings.xml
index 038adc1..23a5118 100644
--- a/testapps/transactionalVoipApp/res/values/strings.xml
+++ b/testapps/transactionalVoipApp/res/values/strings.xml
@@ -17,13 +17,26 @@
<resources>
<string name="app_name">Transactional API test Activity</string>
+ <string name="in_call_activity_name">Transactional In Call Activity</string>
+
+ <!-- Main Activity -->
<string name="register_phone_account">Register Phone Account</string>
- <string name="direction_outgoing">outgoing</string>
- <string name="direction_incoming">incoming</string>
- <string name="add_call_1">add call 1</string>
- <string name="disconnect_call_1">disconnect call 1</string>
- <string name="add_call_2">add call 2</string>
- <string name="set_call_active">set call 2 active</string>
- <string name="disconnect_call_2">disconnect call 2</string>
+ <string name="start_foreground_service">Start FGS (simulate MT + app in background)</string>
+ <string name="start_outgoing">Start Outgoing Call</string>
+ <string name="start_incoming">Start Incoming Call</string>
+
+ <!-- InCall Activity -->
+ <string name="get_call_id">call id not set</string>
+ <!-- control the call state -->
+ <string name="set_call_active">setActive</string>
+ <string name="answer">answer</string>
+ <string name="set_call_inactive">setInactive</string>
+ <string name="disconnect_call">disconnect</string>
+ <!-- control the call audio -->
+ <string name="request_earpiece_endpoint">Earpiece</string>
+ <string name="request_speaker_endpoint">Speaker</string>
+ <string name="request_bluetooth_endpoint">Bluetooth</string>
+ <!-- extra functionality -->
+ <string name="start_stream">start streaming</string>
</resources>
\ No newline at end of file
diff --git a/testapps/transactionalVoipApp/src/com/android/server/telecom/transactionalVoipApp/BackgroundIncomingCallService.java b/testapps/transactionalVoipApp/src/com/android/server/telecom/transactionalVoipApp/BackgroundIncomingCallService.java
new file mode 100644
index 0000000..b503e94
--- /dev/null
+++ b/testapps/transactionalVoipApp/src/com/android/server/telecom/transactionalVoipApp/BackgroundIncomingCallService.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2023 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.transactionalVoipApp;
+
+import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL;
+
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.Service;
+import android.content.Intent;
+import android.os.Binder;
+import android.os.IBinder;
+import android.util.Log;
+
+public class BackgroundIncomingCallService extends Service {
+ // finals
+ private static final String TAG = "BackgroundIncomingCallService";
+ // instance vars
+ private NotificationManager mNotificationManager;
+ private final IBinder mBinder = new LocalBinder();
+
+ @Override
+ public void onCreate() {
+ Log.i(TAG, "onCreate");
+ mNotificationManager = getSystemService(NotificationManager.class);
+ }
+
+ @Override
+ @StartResult
+ public int onStartCommand(Intent intent, @StartArgFlags int flags, int startId) {
+ Log.i(TAG, String.format("onStartCommand: intent=[%s]", intent));
+
+ // create the notification channel
+ if (mNotificationManager != null) {
+ mNotificationManager.createNotificationChannel(new NotificationChannel(
+ Utils.CHANNEL_ID, "incoming calls", NotificationManager.IMPORTANCE_DEFAULT));
+ }
+
+ // start the foreground service and post a notification
+ startForeground(98765, Utils.createCallStyleNotification(this),
+ FOREGROUND_SERVICE_TYPE_PHONE_CALL);
+
+ return Service.START_STICKY_COMPATIBILITY;
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ Log.i(TAG, String.format("onBind: intent=[%s]", intent));
+ return mBinder;
+ }
+
+ /**
+ * Class used for the client Binder. Because we know this service always
+ * runs in the same process as its clients, we don't need to deal with IPC.
+ */
+ public class LocalBinder extends Binder {
+ BackgroundIncomingCallService getService() {
+ // Return this instance of LocalService so clients can call public methods
+ return BackgroundIncomingCallService.this;
+ }
+ }
+}
diff --git a/testapps/transactionalVoipApp/src/com/android/server/telecom/transactionalVoipApp/InCallActivity.java b/testapps/transactionalVoipApp/src/com/android/server/telecom/transactionalVoipApp/InCallActivity.java
new file mode 100644
index 0000000..4c9f52d
--- /dev/null
+++ b/testapps/transactionalVoipApp/src/com/android/server/telecom/transactionalVoipApp/InCallActivity.java
@@ -0,0 +1,268 @@
+/*
+ * Copyright (C) 2023 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.transactionalVoipApp;
+
+import static android.telecom.CallAttributes.AUDIO_CALL;
+import static android.telecom.CallAttributes.DIRECTION_INCOMING;
+import static android.telecom.CallAttributes.DIRECTION_OUTGOING;
+
+import android.app.Activity;
+import android.graphics.Color;
+import android.media.AudioManager;
+import android.media.AudioRecord;
+import android.media.MediaPlayer;
+import android.net.StringNetworkSpecifier;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.OutcomeReceiver;
+import android.telecom.CallAttributes;
+import android.telecom.CallControl;
+import android.telecom.CallEndpoint;
+import android.telecom.CallException;
+import android.telecom.DisconnectCause;
+import android.telecom.TelecomManager;
+import android.util.Log;
+import android.view.View;
+import android.widget.TextView;
+
+public class InCallActivity extends Activity {
+ private static final String TAG = "InCallActivity";
+ private final AudioManager.AudioRecordingCallback mAudioRecordingCallback =
+ Utils.getAudioRecordingCallback();
+ private static TelecomManager mTelecomManager;
+ private MyVoipCall mVoipCall;
+ private MediaPlayer mMediaPlayer;
+ private AudioRecord mAudioRecord;
+ private int mCallDirection = DIRECTION_INCOMING;
+ private TextView mCurrentEndpointTextView;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ Log.i(TAG, "#onCreate: in function");
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.in_call_activity);
+
+ Bundle extras = getIntent().getExtras();
+ if (extras != null) {
+ mCallDirection = extras.getInt(Utils.sCALL_DIRECTION_KEY, DIRECTION_INCOMING);
+ }
+ mCurrentEndpointTextView = findViewById(R.id.current_endpoint);
+ mCurrentEndpointTextView.setText("Endpoint/Audio Route NOT ESTABLISHED");
+ updateCallId();
+ mTelecomManager = getSystemService(TelecomManager.class);
+ mMediaPlayer = Utils.createMediaPlayer(getApplicationContext());
+ mAudioRecord = Utils.createAudioRecord();
+ mAudioRecord.registerAudioRecordingCallback(Runnable::run, mAudioRecordingCallback);
+
+ if (mVoipCall == null) {
+ addCall();
+ }
+
+ findViewById(R.id.set_call_active_button).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ updateCurrentEndpoint();
+ if (canUseCallControl()) {
+ mVoipCall.mCallControl.setActive(Runnable::run,
+ Utils.getLoggableOutcomeReceiver("setActive"));
+ }
+ mAudioRecord.startRecording();
+ mMediaPlayer.start();
+ }
+ });
+
+
+ findViewById(R.id.answer_button).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ updateCurrentEndpoint();
+ if (canUseCallControl() && mCallDirection != DIRECTION_OUTGOING) {
+ mVoipCall.mCallControl.answer(AUDIO_CALL, Runnable::run,
+ Utils.getLoggableOutcomeReceiver("answer"));
+ mAudioRecord.startRecording();
+ mMediaPlayer.start();
+ }
+ }
+ });
+
+
+ findViewById(R.id.set_call_inactive_button).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (canUseCallControl()) {
+ mVoipCall.mCallControl.setInactive(Runnable::run,
+ Utils.getLoggableOutcomeReceiver("setInactive"));
+ }
+ mAudioRecord.stop();
+ mMediaPlayer.pause();
+ }
+ });
+
+ findViewById(R.id.disconnect_call_button).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ disconnectAndStopAudio();
+ finish();
+ }
+ });
+
+ findViewById(R.id.start_stream_button).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (canUseCallControl()) {
+ mVoipCall.mCallControl.startCallStreaming(Runnable::run,
+ Utils.getLoggableOutcomeReceiver("startCallStream"));
+ }
+ }
+ });
+
+ findViewById(R.id.request_earpiece).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (canUseCallControl() && mVoipCall.mEarpieceEndpoint != null) {
+ requestEndpointChange(mVoipCall.mEarpieceEndpoint,
+ "Request EARPIECE Endpoint:");
+ }
+ }
+ });
+
+ findViewById(R.id.request_speaker).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (canUseCallControl() && mVoipCall.mSpeakerEndpoint != null) {
+ requestEndpointChange(mVoipCall.mSpeakerEndpoint,
+ "Request SPEAKER Endpoint:");
+ }
+ }
+ });
+
+ findViewById(R.id.request_bluetooth).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (canUseCallControl() && mVoipCall.mBluetoothEndpoint != null) {
+ requestEndpointChange(mVoipCall.mBluetoothEndpoint,
+ "Request BLUETOOTH Endpoint:");
+ }
+ }
+ });
+ }
+
+ @Override
+ protected void onDestroy() {
+ disconnectAndStopAudio();
+ super.onDestroy();
+ }
+
+ private boolean canUseCallControl() {
+ return mVoipCall != null && mVoipCall.mCallControl != null;
+ }
+
+ private void updateCurrentEndpoint() {
+ if (mCurrentEndpointTextView != null) {
+ if (mVoipCall != null && mVoipCall.mCurrentEndpoint != null) {
+ mCurrentEndpointTextView.setText("CallEndpoint=[" +
+ mVoipCall.mCurrentEndpoint.getEndpointName() + "]");
+ }
+ }
+ }
+
+ private void updateCurrentEndpointWithOnResult(CallEndpoint endpoint) {
+ if (mCurrentEndpointTextView != null) {
+ if (mVoipCall != null && mVoipCall.mCurrentEndpoint != null) {
+ mCurrentEndpointTextView.setText("CallEndpoint=[" +
+ endpoint.getEndpointName() + "]");
+ }
+ }
+ }
+
+ private void updateCallId() {
+ TextView view = findViewById(R.id.getCallIdTextView);
+ StringBuilder sb = new StringBuilder();
+ sb.append("[");
+ if (canUseCallControl()) {
+ String id = mVoipCall.mCallControl.getCallId().toString();
+ sb.append(id);
+ } else {
+ sb.append("Error Getting Id");
+ }
+ sb.append("]");
+ view.setText(sb.toString());
+ }
+
+ private void addCall() {
+ mVoipCall = new MyVoipCall("123");
+
+ CallAttributes callAttributes =
+ new CallAttributes.Builder(
+ Utils.PHONE_ACCOUNT_HANDLE,
+ mCallDirection,
+ "Alan Turing",
+ Uri.parse("tel:6506959001")).build();
+
+ mTelecomManager.addCall(callAttributes, Runnable::run,
+ new OutcomeReceiver<CallControl, CallException>() {
+ @Override
+ public void onResult(CallControl callControl) {
+ Log.i(TAG, "addCall: onResult: callback fired");
+ mVoipCall.onAddCallControl(callControl);
+ updateCallId();
+ updateCurrentEndpoint();
+ }
+
+ @Override
+ public void onError(CallException exception) {
+
+ }
+ },
+ mVoipCall, mVoipCall);
+ }
+
+ private void disconnectAndStopAudio() {
+ if (mVoipCall != null) {
+ mVoipCall.mCallControl.disconnect(
+ new DisconnectCause(DisconnectCause.LOCAL),
+ Runnable::run,
+ Utils.getLoggableOutcomeReceiver("disconnect"));
+ }
+ mMediaPlayer.stop();
+ mAudioRecord.stop();
+ try {
+ mAudioRecord.unregisterAudioRecordingCallback(mAudioRecordingCallback);
+ } catch (IllegalArgumentException e) {
+ // pass through
+ }
+ }
+
+ private void requestEndpointChange(CallEndpoint endpoint, String tag) {
+ mVoipCall.mCallControl.requestCallEndpointChange(
+ endpoint,
+ Runnable::run,
+ new OutcomeReceiver<Void, CallException>() {
+ @Override
+ public void onResult(Void result) {
+ Log.i(TAG, String.format("success w/ %s", tag));
+ updateCurrentEndpointWithOnResult(endpoint);
+ }
+
+ @Override
+ public void onError(CallException e) {
+ Log.i(TAG, String.format("%s :failed to switch to endpoint=[%s],"
+ + " due to exception=[%s]", tag, endpoint, e.toString()));
+ }
+ });
+ }
+}
diff --git a/testapps/transactionalVoipApp/src/com/android/server/telecom/transactionalVoipApp/MyVoipCall.java b/testapps/transactionalVoipApp/src/com/android/server/telecom/transactionalVoipApp/MyVoipCall.java
index 690311e..e2b5b14 100644
--- a/testapps/transactionalVoipApp/src/com/android/server/telecom/transactionalVoipApp/MyVoipCall.java
+++ b/testapps/transactionalVoipApp/src/com/android/server/telecom/transactionalVoipApp/MyVoipCall.java
@@ -24,6 +24,7 @@
import android.telecom.DisconnectCause;
import android.util.Log;
+import java.util.ArrayList;
import java.util.List;
import androidx.annotation.NonNull;
@@ -34,7 +35,12 @@
private static final String TAG = "MyVoipCall";
private final String mCallId;
- CallControl mCallControl;
+ public CallControl mCallControl;
+ public CallEndpoint mCurrentEndpoint;
+ public CallEndpoint mEarpieceEndpoint;
+ public CallEndpoint mSpeakerEndpoint;
+ public CallEndpoint mBluetoothEndpoint;
+ List<CallEndpoint> mAvailableEndpoint = new ArrayList<>();
MyVoipCall(String id) {
mCallId = id;
@@ -89,6 +95,7 @@
@Override
public void onCallEndpointChanged(@NonNull CallEndpoint newCallEndpoint) {
Log.i(TAG, String.format("onCallEndpointChanged: endpoint=[%s]", newCallEndpoint));
+ mCurrentEndpoint = newCallEndpoint;
}
@Override
@@ -97,7 +104,17 @@
Log.i(TAG, String.format("onAvailableCallEndpointsChanged: callId=[%s]", mCallId));
for (CallEndpoint endpoint : availableEndpoints) {
Log.i(TAG, String.format("endpoint=[%s]", endpoint));
+ if (endpoint != null && endpoint.getEndpointType() == CallEndpoint.TYPE_EARPIECE) {
+ mEarpieceEndpoint = endpoint;
+ }
+ if (endpoint != null && endpoint.getEndpointType() == CallEndpoint.TYPE_SPEAKER) {
+ mSpeakerEndpoint = endpoint;
+ }
+ if (endpoint != null && endpoint.getEndpointType() == CallEndpoint.TYPE_BLUETOOTH) {
+ mBluetoothEndpoint = endpoint;
+ }
}
+ mAvailableEndpoint = availableEndpoints;
}
@Override
diff --git a/testapps/transactionalVoipApp/src/com/android/server/telecom/transactionalVoipApp/Utils.java b/testapps/transactionalVoipApp/src/com/android/server/telecom/transactionalVoipApp/Utils.java
new file mode 100644
index 0000000..98de790
--- /dev/null
+++ b/testapps/transactionalVoipApp/src/com/android/server/telecom/transactionalVoipApp/Utils.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2023 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.transactionalVoipApp;
+
+import android.app.Notification;
+import android.app.PendingIntent;
+import android.app.Person;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.media.AudioFormat;
+import android.media.AudioManager;
+import android.media.AudioRecord;
+import android.media.AudioRecordingConfiguration;
+import android.media.MediaPlayer;
+import android.media.MediaRecorder;
+import android.os.OutcomeReceiver;
+import android.telecom.CallException;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.util.Log;
+
+import java.util.List;
+
+public class Utils {
+ public static final String TAG = "TransactionalAppUtils";
+ public static final String sEXTRAS_KEY = "ExtrasKey";
+ public static final String sCALL_DIRECTION_KEY = "CallDirectionKey";
+ public static final String CHANNEL_ID = "TelecomVoipAppChannelId";
+ private static final int SAMPLING_RATE_HZ = 44100;
+
+ public static final PhoneAccountHandle PHONE_ACCOUNT_HANDLE = new PhoneAccountHandle(
+ new ComponentName("com.android.server.telecom.transactionalVoipApp",
+ "com.android.server.telecom.transactionalVoipApp.VoipAppMainActivity"), "123");
+
+ public static final PhoneAccount PHONE_ACCOUNT =
+ PhoneAccount.builder(PHONE_ACCOUNT_HANDLE, "test label")
+ .setCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED |
+ PhoneAccount.CAPABILITY_SUPPORTS_TRANSACTIONAL_OPERATIONS).build();
+
+
+ public static Notification createCallStyleNotification(Context context) {
+ Intent answerIntent = new Intent(context, InCallActivity.class);
+ Intent rejectIntent = new Intent(context, InCallActivity.class);
+
+ // Creating a pending intent and wrapping our intent
+ PendingIntent pendingAnswer = PendingIntent.getActivity(context, 0,
+ answerIntent, PendingIntent.FLAG_IMMUTABLE);
+ PendingIntent pendingReject = PendingIntent.getActivity(context, 0,
+ rejectIntent, PendingIntent.FLAG_IMMUTABLE);
+
+
+ Notification callStyleNotification = new Notification.Builder(context,
+ CHANNEL_ID)
+ .setContentText("Answer/Reject call")
+ .setContentTitle("Incoming call")
+ .setSmallIcon(R.drawable.ic_android_black_24dp)
+ .setStyle(Notification.CallStyle.forIncomingCall(
+ new Person.Builder().setName("Tom Stu").setImportant(true).build(),
+ pendingAnswer, pendingReject)
+ )
+ .setFullScreenIntent(pendingAnswer, true)
+ .build();
+
+ return callStyleNotification;
+ }
+
+ public static MediaPlayer createMediaPlayer(Context context) {
+ int audioToPlay = (Math.random() > 0.5f) ?
+ com.android.server.telecom.transactionalVoipApp.R.raw.sample_audio :
+ com.android.server.telecom.transactionalVoipApp.R.raw.sample_audio2;
+ MediaPlayer mediaPlayer = MediaPlayer.create(context, audioToPlay);
+ mediaPlayer.setLooping(true);
+ return mediaPlayer;
+ }
+
+ public static AudioRecord createAudioRecord() {
+ return new AudioRecord.Builder()
+ .setAudioFormat(new AudioFormat.Builder()
+ .setSampleRate(SAMPLING_RATE_HZ)
+ .setEncoding(AudioFormat.ENCODING_PCM_16BIT)
+ .setChannelMask(AudioFormat.CHANNEL_IN_MONO).build())
+ .setAudioSource(MediaRecorder.AudioSource.DEFAULT)
+ .setBufferSizeInBytes(
+ AudioRecord.getMinBufferSize(SAMPLING_RATE_HZ,
+ AudioFormat.CHANNEL_IN_MONO,
+ AudioFormat.ENCODING_PCM_16BIT) * 10)
+ .build();
+ }
+
+
+ public static AudioManager.AudioRecordingCallback getAudioRecordingCallback() {
+ return new AudioManager.AudioRecordingCallback() {
+ @Override
+ public void onRecordingConfigChanged(List<AudioRecordingConfiguration> configs) {
+ super.onRecordingConfigChanged(configs);
+
+ for (AudioRecordingConfiguration config : configs) {
+ if (config != null) {
+ Log.i(TAG, String.format("onRecordingConfigChanged: random: "
+ + "isClientSilenced=[%b], config=[%s]",
+ config.isClientSilenced(), config));
+ }
+ }
+ }
+ };
+ }
+
+ public static OutcomeReceiver<Void, CallException> getLoggableOutcomeReceiver(String tag) {
+ return new OutcomeReceiver<Void, CallException>() {
+ @Override
+ public void onResult(Void result) {
+ Log.i(TAG, tag + " : onResult");
+ }
+
+ @Override
+ public void onError(CallException exception) {
+ Log.i(TAG, tag + " : onError");
+ }
+ };
+ }
+}
\ No newline at end of file
diff --git a/testapps/transactionalVoipApp/src/com/android/server/telecom/transactionalVoipApp/VoipAppMainActivity.java b/testapps/transactionalVoipApp/src/com/android/server/telecom/transactionalVoipApp/VoipAppMainActivity.java
index 4e1ec4c..ae7d9d0 100644
--- a/testapps/transactionalVoipApp/src/com/android/server/telecom/transactionalVoipApp/VoipAppMainActivity.java
+++ b/testapps/transactionalVoipApp/src/com/android/server/telecom/transactionalVoipApp/VoipAppMainActivity.java
@@ -20,7 +20,12 @@
import static android.telecom.CallAttributes.DIRECTION_OUTGOING;
import android.app.Activity;
-import android.content.ComponentName;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.content.Intent;
+import android.media.AudioManager;
+import android.media.AudioRecord;
+import android.media.MediaPlayer;
import android.net.Uri;
import android.os.Bundle;
import android.os.OutcomeReceiver;
@@ -28,142 +33,115 @@
import android.telecom.CallControl;
import android.telecom.CallException;
import android.telecom.DisconnectCause;
-import android.telecom.PhoneAccount;
-import android.telecom.PhoneAccountHandle;
import android.telecom.TelecomManager;
import android.util.Log;
import android.view.View;
import android.widget.ToggleButton;
public class VoipAppMainActivity extends Activity {
-
private static final String TAG = "VoipAppMainActivity";
+ private static final String ACT_STATE_TAG = "VoipActivityState";
private static TelecomManager mTelecomManager;
- private MyVoipCall mCall1;
- private MyVoipCall mCall2;
- private ToggleButton mCallDirectionButton;
-
- PhoneAccountHandle handle = new PhoneAccountHandle(
- new ComponentName("com.android.server.telecom.transactionalVoipApp",
- "com.android.server.telecom.transactionalVoipApp.VoipAppMainActivity"), "123");
-
- PhoneAccount mPhoneAccount = PhoneAccount.builder(handle, "test label")
- .setCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED |
- PhoneAccount.CAPABILITY_SUPPORTS_TRANSACTIONAL_OPERATIONS).build();
+ NotificationManager mNotificationManager;
@Override
protected void onCreate(Bundle savedInstanceState) {
+ Log.i(TAG, ACT_STATE_TAG + "onCreate");
super.onCreate(savedInstanceState);
setContentView(R.layout.main_activity);
mTelecomManager = getSystemService(TelecomManager.class);
- mCallDirectionButton = findViewById(R.id.callDirectionButton);
+ mNotificationManager = getSystemService(NotificationManager.class);
+ // create a notification channel
+ if (mNotificationManager != null) {
+ mNotificationManager.createNotificationChannel(new NotificationChannel(
+ Utils.CHANNEL_ID, "new call channel",
+ NotificationManager.IMPORTANCE_DEFAULT));
+ }
// register account
findViewById(R.id.registerButton).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
- mTelecomManager.registerPhoneAccount(mPhoneAccount);
+ mTelecomManager.registerPhoneAccount(Utils.PHONE_ACCOUNT);
}
});
- // call 1 buttons
- findViewById(R.id.add_call_1_button).setOnClickListener(new View.OnClickListener() {
+ // Start a foreground service that will post a notification within 10 seconds.
+ // This is helpful for debugging scenarios where the app is in the background and posting
+ // an incoming call notification.
+ findViewById(R.id.startForegroundService).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
- Bundle extras = new Bundle();
- extras.putString("testKey", "testValue");
- mCall1 = new MyVoipCall("1");
- addCall(mCall1, true);
+ Intent startForegroundService = new Intent(getApplicationContext(),
+ BackgroundIncomingCallService.class);
+ getApplicationContext().startForegroundService(startForegroundService);
}
});
- findViewById(R.id.disconnect_call_1_button).setOnClickListener(new View.OnClickListener() {
+
+ // post a new call notification and start an InCall activity
+ findViewById(R.id.startOutgoingCall).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
- disconnectCall(mCall1);
+ startInCallActivity(DIRECTION_OUTGOING);
}
});
-
- //call 2 buttons
- findViewById(R.id.add_call_2_button).setOnClickListener(new View.OnClickListener() {
+ // post a new call notification and start an InCall activity
+ findViewById(R.id.startIncomingCall).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
- Bundle extras = new Bundle();
- extras.putString("call2extraKey", "call2Value");
- mCall2 = new MyVoipCall("2");
- addCall(mCall2, false);
+ startInCallActivity(DIRECTION_INCOMING);
}
});
- findViewById(R.id.set_call_2_active_button).setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- setCallActive(mCall2);
- }
- });
-
- findViewById(R.id.disconnect_call_2_button).setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- disconnectCall(mCall2);
- }
- });
}
- private void addCall(MyVoipCall call, boolean setActive) {
- int direction = (mCallDirectionButton.isChecked() ? DIRECTION_INCOMING
- : DIRECTION_OUTGOING);
-
- CallAttributes callAttributes = new CallAttributes.Builder(handle, direction, "Alan Turing",
- Uri.fromParts("tel", "abc", "123")).build();
-
- mTelecomManager.addCall(callAttributes, Runnable::run,
- new OutcomeReceiver<CallControl, CallException>() {
- @Override
- public void onResult(CallControl callControl) {
- Log.i(TAG, "addCall: onResult: callback fired");
- call.onAddCallControl(callControl);
- if (setActive) {
- setCallActive(call);
- }
- }
-
- @Override
- public void onError(CallException exception) {
-
- }
- },
- call, call);
+ private void startInCallActivity(int direction) {
+ mNotificationManager.notify(123456,
+ Utils.createCallStyleNotification(getApplicationContext()));
+ Bundle extras = new Bundle();
+ extras.putInt(Utils.sCALL_DIRECTION_KEY, direction);
+ Intent intent = new Intent(getApplicationContext(), InCallActivity.class);
+ intent.putExtra(Utils.sEXTRAS_KEY, extras);
+ startActivity(intent);
}
- private void setCallActive(MyVoipCall call) {
- call.mCallControl.setActive(Runnable::run, new OutcomeReceiver<Void, CallException>() {
- @Override
- public void onResult(Void result) {
- Log.i(TAG, "setCallActive: onResult");
- }
-
- @Override
- public void onError(CallException exception) {
- Log.i(TAG, "setCallActive: onError");
- }
- });
+ @Override
+ protected void onResume() {
+ Log.i(TAG, ACT_STATE_TAG + " onResume: When the activity enters the Resumed state,"
+ + " it comes to the foreground");
+ super.onResume();
}
- private void disconnectCall(MyVoipCall call) {
- call.mCallControl.disconnect(new DisconnectCause(DisconnectCause.LOCAL), Runnable::run,
- new OutcomeReceiver<Void, CallException>() {
- @Override
- public void onResult(Void result) {
- Log.i(TAG, "disconnectCall: onResult");
- }
+ @Override
+ protected void onPause() {
+ Log.i(TAG, ACT_STATE_TAG + " onPause: The system calls this method as the first"
+ + " indication that the user is leaving your activity. It indicates that the"
+ + " activity is no longer in the foreground, but it is still visible if the user"
+ + " is in multi-window mode");
+ super.onPause();
+ }
- @Override
- public void onError(CallException exception) {
- Log.i(TAG, "disconnectCall: onError");
- }
- });
+ @Override
+ protected void onStop() {
+ Log.i(TAG, ACT_STATE_TAG + "onStop: When your activity is no longer visible to"
+ + " the user, it enters the Stopped state,");
+ super.onStop();
+ }
+
+ @Override
+ protected void onRestart() {
+ Log.i(TAG, ACT_STATE_TAG + " onRestart: onStop has called onRestart and the "
+ + "activity comes back to interact with the user");
+ super.onRestart();
+ }
+
+ @Override
+ protected void onDestroy() {
+ Log.i(TAG, ACT_STATE_TAG + " onDestroy: is called before the activity is"
+ + " destroyed. ");
+ super.onDestroy();
}
}