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();
     }
 }