Merge "Tests for 3rd call reception in DSDA." into udc-dev
diff --git a/Android.bp b/Android.bp
index c5141ca..501b438 100644
--- a/Android.bp
+++ b/Android.bp
@@ -27,6 +27,7 @@
     ],
     static_libs: [
         "androidx.annotation_annotation",
+        "androidx.core_core",
     ],
     libs: [
         "services",
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index d42dcff..ab067d9 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -63,6 +63,8 @@
     <uses-permission android:name="android.permission.WRITE_BLOCKED_NUMBERS"/>
     <uses-permission android:name="android.permission.SUBSTITUTE_NOTIFICATION_APP_NAME"/>
     <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
+    <uses-permission android:name="android.permission.USE_COLORIZED_NOTIFICATIONS"/>
+    <uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT"/>
     <uses-permission android:name="com.android.phone.permission.ACCESS_LAST_KNOWN_CELL_ID"/>
     <uses-permission android:name="android.permission.STATUS_BAR_SERVICE" />
 
diff --git a/res/drawable/gm_phonelink.xml b/res/drawable/gm_phonelink.xml
new file mode 100644
index 0000000..2ffba0e
--- /dev/null
+++ b/res/drawable/gm_phonelink.xml
@@ -0,0 +1,11 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="20dp"
+    android:height="20dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?android:attr/colorControlNormal"
+    android:autoMirrored="true">
+  <path
+      android:fillColor="#FF000000"
+      android:pathData="M5,6h16L21,4L5,4c-1.1,0 -2,0.9 -2,2v11L1,17v3h11v-3L5,17L5,6zM21,8h-6c-0.55,0 -1,0.45 -1,1v10c0,0.55 0.45,1 1,1h6c0.55,0 1,-0.45 1,-1L22,9c0,-0.55 -0.45,-1 -1,-1zM20,17h-4v-7h4v7z"/>
+</vector>
diff --git a/res/values-gu/strings.xml b/res/values-gu/strings.xml
index 4af6351..883ce52 100644
--- a/res/values-gu/strings.xml
+++ b/res/values-gu/strings.xml
@@ -25,7 +25,7 @@
     <string name="notification_missedCallsMsg" msgid="5055782736170916682">"<xliff:g id="NUM_MISSED_CALLS">%s</xliff:g> ચૂકી ગયેલા કૉલ"</string>
     <string name="notification_missedCallTicker" msgid="6731461957487087769">"<xliff:g id="MISSED_CALL_FROM">%s</xliff:g> નો કૉલ ચૂકી ગયાં"</string>
     <string name="notification_missedCall_call_back" msgid="7900333283939789732">"કૉલ બેક"</string>
-    <string name="notification_missedCall_message" msgid="4054698824390076431">"સંદેશ"</string>
+    <string name="notification_missedCall_message" msgid="4054698824390076431">"મેસેજ"</string>
     <string name="notification_disconnectedCall_title" msgid="1790131923692416928">"ડિસ્કનેક્ટ કરેલો કૉલ"</string>
     <string name="notification_disconnectedCall_body" msgid="600491714584417536">"ઇમર્જન્સી કૉલને કારણે <xliff:g id="CALLER">%s</xliff:g>નો કૉલ ડિસ્કનેક્ટ કરવામાં આવ્યો છે."</string>
     <string name="notification_disconnectedCall_generic_body" msgid="5282765206349184853">"ઇમર્જન્સી કૉલને કારણે તમારો કૉલ ડિસ્કનેક્ટ કરવામાં આવ્યો છે."</string>
diff --git a/res/values-kn/strings.xml b/res/values-kn/strings.xml
index 8109de2..68d8078 100644
--- a/res/values-kn/strings.xml
+++ b/res/values-kn/strings.xml
@@ -53,7 +53,7 @@
     <string name="no_vm_number" msgid="2179959110602180844">"ಧ್ವನಿಮೇಲ್‌ ಸಂಖ್ಯೆಯು ಕಾಣೆಯಾಗಿದೆ"</string>
     <string name="no_vm_number_msg" msgid="1339245731058529388">"ಸಿಮ್‌ ಕಾರ್ಡ್‌ನಲ್ಲಿ ಯಾವುದೇ ಧ್ವನಿಮೇಲ್‌ ಸಂಖ್ಯೆಯನ್ನು ಸಂಗ್ರಹಿಸಿಲ್ಲ."</string>
     <string name="add_vm_number_str" msgid="5179510133063168998">"ಸಂಖ್ಯೆಯನ್ನು ಸೇರಿಸಿ"</string>
-    <string name="change_default_dialer_dialog_title" msgid="5861469279421508060">"<xliff:g id="NEW_APP">%s</xliff:g> ಅನ್ನು ನಿಮ್ಮ ಡಿಫಾಲ್ಟ್ ಫೋನ್ ಅಪ್ಲಿಕೇಶನ್ ಆಗಿ ಮಾಡುವುದೇ?"</string>
+    <string name="change_default_dialer_dialog_title" msgid="5861469279421508060">"<xliff:g id="NEW_APP">%s</xliff:g> ಅನ್ನು ನಿಮ್ಮ ಡಿಫಾಲ್ಟ್ ಫೋನ್ ಆ್ಯಪ್ ಆಗಿ ಮಾಡಬೇಕೆ?"</string>
     <string name="change_default_dialer_dialog_affirmative" msgid="8604665314757739550">"ಡಿಫಾಲ್ಟ್ ಹೊಂದಿಸಿ"</string>
     <string name="change_default_dialer_dialog_negative" msgid="8648669840052697821">"ರದ್ದುಮಾಡಿ"</string>
     <string name="change_default_dialer_warning_message" msgid="8461963987376916114">"<xliff:g id="NEW_APP">%s</xliff:g> ಗೆ ನಿಮ್ಮ ಕರೆಗಳ ಎಲ್ಲಾ ಅಂಶಗಳನ್ನು ನಿಯಂತ್ರಿಸಲು ಮತ್ತು ಕರೆಗಳನ್ನು ಮಾಡಲು ಸಾಧ್ಯವಾಗುತ್ತದೆ. ನೀವು ವಿಶ್ವಾಸವಿರಿಸಿರುವಂತಹ ಅಪ್ಲಿಕೇಶನ್‌ಗಳನ್ನು ಮಾತ್ರ ನಿಮ್ಮ ಡಿಫಾಲ್ಟ್ ಅಪ್ಲಿಕೇಶನ್ ಆಗಿ ಹೊಂದಿಸಬೇಕು."</string>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index d67df4b..ec278f0 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -321,6 +321,10 @@
     <string name="notification_channel_disconnected_calls">Disconnected calls</string>
     <!-- Notification channel name for a channel containing crashed phone apps service notifications. -->
     <string name="notification_channel_in_call_service_crash">Crashed phone apps</string>
+    <!-- Notification channel name for a channel containing notifications related to call streaming.
+         Call streaming is a feature where an app can use another device like a tablet to see and
+         control a call taking place on their phone. -->
+    <string name="notification_channel_call_streaming">Call streaming</string>
 
     <!-- Alert dialog content used to inform the user that placing a new outgoing call will end the
          ongoing call in the app "other_app". -->
@@ -395,4 +399,20 @@
     <string name="callendpoint_name_streaming">External</string>
     <!-- The user-visible name of the unknown new type CallEndpoint -->
     <string name="callendpoint_name_unknown">Unknown</string>
+
+    <!-- The content of a notification shown when a call is being streamed to another device.
+         Call streaming is a feature where a user can see and interact with a call from another
+         device like a tablet while the call takes place on their phone. -->
+    <string name="call_streaming_notification_body">Streaming audio to other device</string>
+    <!-- A notification action which is shown when a call is being streamed to another device.
+         Tapping the action will hang up the call.
+         Call streaming is a feature where a user can see and interact with a call from another
+         device like a tablet while the call takes place on their phone. -->
+    <string name="call_streaming_notification_action_hang_up">Hang up</string>
+    <!-- A notification action which is shown when a call is being streamed to another device.
+         Tapping the action will move the call back to the phone from the device it is being
+         streamed to.
+         Call streaming is a feature where a user can see and interact with a call from another
+         device like a tablet while the call takes place on their phone. -->
+    <string name="call_streaming_notification_action_switch_here">Switch here</string>
 </resources>
diff --git a/src/com/android/server/telecom/Call.java b/src/com/android/server/telecom/Call.java
index 743156e..42f02fb 100644
--- a/src/com/android/server/telecom/Call.java
+++ b/src/com/android/server/telecom/Call.java
@@ -414,6 +414,16 @@
 
     private boolean mIsEmergencyCall;
 
+    /**
+     * Flag indicating if ECBM is active for the target phone account. This only applies to MT calls
+     * in the scenario of work profiles (when the profile is paused and the user has only registered
+     * a work sim). Normally, MT calls made to the work sim should be rejected when the work apps
+     * are paused. However, when the admin makes a MO ecall, ECBM should be enabled for that sim to
+     * allow non-emergency MT calls. MO calls don't apply because the phone account would be
+     * rejected from selection if the owner is not placing the call.
+     */
+    private boolean mIsInECBM;
+
     // The Call is considered an emergency call for testing, but will not actually connect to
     // emergency services.
     private boolean mIsTestEmergencyCall;
@@ -989,6 +999,9 @@
         s.append(SimpleDateFormat.getDateTimeInstance().format(new Date(getCreationTimeMillis())));
         s.append("]");
         s.append(isIncoming() ? "(MT - incoming)" : "(MO - outgoing)");
+        s.append("(User=");
+        s.append(getInitiatingUser());
+        s.append(")");
         s.append("\n\t");
 
         PhoneAccountHandle targetPhoneAccountHandle = getTargetPhoneAccount();
@@ -1592,6 +1605,21 @@
     }
 
     /**
+     * @return {@code true} if the target phone account is in ECBM.
+     */
+    public boolean isInECBM() {
+        return mIsInECBM;
+    }
+
+    /**
+     * Set if the target phone account is in ECBM.
+     * @param isInEcbm {@code true} if target phone account is in ECBM, {@code false} otherwise.
+     */
+    public void setIsInECBM(boolean isInECBM) {
+        mIsInECBM = isInECBM;
+    }
+
+    /**
      * @return {@code true} if the network has identified this call as an emergency call.
      */
     public boolean isNetworkIdentifiedEmergencyCall() {
@@ -1682,6 +1710,11 @@
     public void setTargetPhoneAccount(PhoneAccountHandle accountHandle) {
         if (!Objects.equals(mTargetPhoneAccountHandle, accountHandle)) {
             mTargetPhoneAccountHandle = accountHandle;
+            // Update the last MO emergency call in the helper, if applicable.
+            if (isEmergencyCall() && !isIncoming()) {
+                mCallsManager.getEmergencyCallHelper().setLastOutgoingEmergencyCallPAH(
+                        accountHandle);
+            }
             for (Listener l : mListeners) {
                 l.onTargetPhoneAccountChanged(this);
             }
@@ -4086,6 +4119,15 @@
      * @param extras The extras.
      */
     public void onConnectionEvent(String event, Bundle extras) {
+        if (mIsTransactionalCall) {
+            // send the Event directly to the ICS via the InCallController listener
+            for (Listener l : mListeners) {
+                l.onConnectionEvent(this, event, extras);
+            }
+            // Don't run the below block since it applies to Calls that are attached to a
+            // ConnectionService
+            return;
+        }
         // Don't log call quality reports; they're quite frequent and will clog the log.
         if (!Connection.EVENT_CALL_QUALITY_REPORT.equals(event)) {
             Log.addEvent(this, LogUtils.Events.CONNECTION_EVENT, event);
diff --git a/src/com/android/server/telecom/CallStreamingController.java b/src/com/android/server/telecom/CallStreamingController.java
index 6276a7d..d90524d 100644
--- a/src/com/android/server/telecom/CallStreamingController.java
+++ b/src/com/android/server/telecom/CallStreamingController.java
@@ -87,6 +87,14 @@
             mStreamingCall = null;
             mTransactionalServiceWrapper = null;
             if (mConnection != null) {
+                // Notify service streaming stopped and then unbind.
+                try {
+                    mService.onCallStreamingStopped();
+                } catch (RemoteException e) {
+                    // Could not notify stop streaming; we're about to just unbind so this is
+                    // unfortunate but not the end of the world.
+                    Log.e(this, e, "resetController: failed to notify stop streaming.");
+                }
                 mContext.unbindService(mConnection);
                 mConnection = null;
             }
@@ -140,7 +148,7 @@
 
         @Override
         public CompletableFuture<VoipCallTransactionResult> processTransaction(Void v) {
-            Log.d(this, "processTransaction");
+            Log.i(this, "processTransaction");
             CompletableFuture<VoipCallTransactionResult> future = new CompletableFuture<>();
 
             if (mEnterInterception) {
@@ -178,9 +186,8 @@
         @SuppressLint("LongLogTag")
         @Override
         public CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
-            Log.d(this, "processTransaction");
+            Log.i(this, "processTransaction");
             CompletableFuture<VoipCallTransactionResult> future = new CompletableFuture<>();
-
             RoleManager roleManager = mContext.getSystemService(RoleManager.class);
             PackageManager packageManager = mContext.getPackageManager();
             if (roleManager == null || packageManager == null) {
@@ -198,7 +205,7 @@
                         VoipCallTransactionResult.RESULT_FAILED, MESSAGE));
                 return future;
             }
-
+            Log.i(this, "processTransaction: servicePackage=%s", holders.get(0));
             Intent serviceIntent = new Intent(CallStreamingService.SERVICE_INTERFACE);
             serviceIntent.setPackage(holders.get(0));
             List<ResolveInfo> infos = packageManager.queryIntentServicesAsUser(serviceIntent,
@@ -223,7 +230,7 @@
             Intent intent = new Intent(CallStreamingService.SERVICE_INTERFACE);
             intent.setComponent(serviceInfo.getComponentName());
 
-            mConnection =  new CallStreamingServiceConnection(mCall, mWrapper, future);
+            mConnection = new CallStreamingServiceConnection(mCall, mWrapper, future);
             if (!mContext.bindServiceAsUser(intent, mConnection, Context.BIND_AUTO_CREATE
                     | Context.BIND_FOREGROUND_SERVICE
                     | Context.BIND_SCHEDULE_LIKE_TOP_APP, mUserHandle)) {
@@ -232,7 +239,6 @@
                         VoipCallTransactionResult.RESULT_FAILED,
                         "STREAMING_FAILED_SENDER_BINDING_ERROR"));
             }
-
             return future;
         }
     }
@@ -249,7 +255,7 @@
         @SuppressLint("LongLogTag")
         @Override
         public CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
-            Log.d(this, "processTransaction");
+            Log.i(this, "processTransaction (unbindStreaming");
             CompletableFuture<VoipCallTransactionResult> future = new CompletableFuture<>();
 
             resetController();
@@ -280,11 +286,13 @@
                 case CallState.ON_HOLD:
                     transaction = new CallStreamingStateChangeTransaction(
                             StreamingCall.STATE_HOLDING);
+                    break;
                 case CallState.DISCONNECTING:
                 case CallState.DISCONNECTED:
                     Log.addEvent(call, LogUtils.Events.STOP_STREAMING);
                     transaction = new CallStreamingStateChangeTransaction(
                             StreamingCall.STATE_DISCONNECTED);
+                    break;
                 default:
                     // ignore
             }
@@ -374,13 +382,6 @@
         }
 
         private void clearBinding() {
-            try {
-                if (mService != null) {
-                    mService.onCallStreamingStopped();
-                }
-            } catch (RemoteException e) {
-                Log.e(this, e, "Exception when stop call streaming");
-            }
             resetController();
             if (!mFuture.isDone()) {
                 mFuture.complete(new VoipCallTransactionResult(
diff --git a/src/com/android/server/telecom/CallsManager.java b/src/com/android/server/telecom/CallsManager.java
index ccc8e59..13a965c 100644
--- a/src/com/android/server/telecom/CallsManager.java
+++ b/src/com/android/server/telecom/CallsManager.java
@@ -24,6 +24,7 @@
 import static android.provider.CallLog.Calls.USER_MISSED_CALL_FILTERS_TIMEOUT;
 import static android.provider.CallLog.Calls.USER_MISSED_CALL_SCREENING_SERVICE_SILENCED;
 import static android.provider.CallLog.Calls.USER_MISSED_NEVER_RANG;
+import static android.provider.CallLog.Calls.USER_MISSED_NOT_RUNNING;
 import static android.provider.CallLog.Calls.USER_MISSED_NO_ANSWER;
 import static android.provider.CallLog.Calls.USER_MISSED_SHORT_RING;
 import static android.telecom.TelecomManager.ACTION_POST_CALL;
@@ -40,6 +41,7 @@
 
 import android.Manifest;
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.app.ActivityManager;
 import android.app.AlertDialog;
 import android.app.KeyguardManager;
@@ -131,6 +133,7 @@
 import com.android.server.telecom.stats.CallFailureCause;
 import com.android.server.telecom.ui.AudioProcessingNotification;
 import com.android.server.telecom.ui.CallRedirectionTimeoutDialogActivity;
+import com.android.server.telecom.ui.CallStreamingNotification;
 import com.android.server.telecom.ui.ConfirmCallDialogActivity;
 import com.android.server.telecom.ui.DisconnectedCallNotifier;
 import com.android.server.telecom.ui.IncomingCallNotifier;
@@ -446,6 +449,8 @@
     private final CallStreamingController mCallStreamingController;
     private final BlockedNumbersAdapter mBlockedNumbersAdapter;
     private final TransactionManager mTransactionManager;
+    private final UserManager mUserManager;
+    private final CallStreamingNotification mCallStreamingNotification;
 
     private final ConnectionServiceFocusManager.CallsManagerRequester mRequester =
             new ConnectionServiceFocusManager.CallsManagerRequester() {
@@ -557,7 +562,8 @@
             Executor asyncTaskExecutor,
             BlockedNumbersAdapter blockedNumbersAdapter,
             TransactionManager transactionManager,
-            EmergencyCallDiagnosticLogger emergencyCallDiagnosticLogger) {
+            EmergencyCallDiagnosticLogger emergencyCallDiagnosticLogger,
+            CallStreamingNotification callStreamingNotification) {
 
         mContext = context;
         mLock = lock;
@@ -646,6 +652,7 @@
         mBlockedNumbersAdapter = blockedNumbersAdapter;
         mCallStreamingController = new CallStreamingController(mContext, mLock);
         mVoipCallMonitor = new VoipCallMonitor(mContext, mLock);
+        mCallStreamingNotification = callStreamingNotification;
 
         mListeners.add(mInCallWakeLockController);
         mListeners.add(statusBarNotifier);
@@ -667,6 +674,7 @@
         // this needs to be after the mCallAudioManager
         mListeners.add(mPhoneStateBroadcaster);
         mListeners.add(mVoipCallMonitor);
+        mListeners.add(mCallStreamingNotification);
 
         mVoipCallMonitor.startMonitor();
 
@@ -685,6 +693,7 @@
 
         mCallAnomalyWatchdog = callAnomalyWatchdog;
         mAsyncTaskExecutor = asyncTaskExecutor;
+        mUserManager = mContext.getSystemService(UserManager.class);
     }
 
     public void setIncomingCallNotifier(IncomingCallNotifier incomingCallNotifier) {
@@ -1411,6 +1420,15 @@
                     extras.getInt(CallAttributes.CALL_CAPABILITIES_KEY,
                             CallAttributes.SUPPORTS_SET_INACTIVE), true);
             call.setTargetPhoneAccount(phoneAccountHandle);
+            if (extras.containsKey(CallAttributes.DISPLAY_NAME_KEY)) {
+                CharSequence displayName = extras.getCharSequence(CallAttributes.DISPLAY_NAME_KEY);
+                if (!TextUtils.isEmpty(displayName)) {
+                    call.setCallerDisplayName(displayName.toString(),
+                            TelecomManager.PRESENTATION_ALLOWED);
+                }
+            }
+            // Incoming address was set via EXTRA_INCOMING_CALL_ADDRESS above.
+            call.setInitiatingUser(phoneAccountHandle.getUserHandle());
         }
 
         // Ensure new calls related to self-managed calls/connections are set as such. This will
@@ -1534,7 +1552,22 @@
 
         CallFailureCause startFailCause =
                 checkIncomingCallPermitted(call, call.getTargetPhoneAccount());
-        if (!isHandoverAllowed ||
+        // Check if the target phone account is possibly in ECBM.
+        call.setIsInECBM(getEmergencyCallHelper()
+                .isLastOutgoingEmergencyCallPAH(call.getTargetPhoneAccount()));
+        if (mUserManager.isQuietModeEnabled(call.getUserHandleFromTargetPhoneAccount())
+                && !call.isEmergencyCall() && !call.isInECBM()) {
+            Log.d(TAG, "Rejecting non-emergency call because the owner %s is not running.",
+                    phoneAccountHandle.getUserHandle());
+            call.setMissedReason(USER_MISSED_NOT_RUNNING);
+            call.setStartFailCause(CallFailureCause.INVALID_USE);
+            if (isConference) {
+                notifyCreateConferenceFailed(phoneAccountHandle, call);
+            } else {
+                notifyCreateConnectionFailed(phoneAccountHandle, call);
+            }
+        }
+        else if (!isHandoverAllowed ||
                 (call.isSelfManaged() && !startFailCause.isSuccess())) {
             if (isConference) {
                 notifyCreateConferenceFailed(phoneAccountHandle, call);
@@ -1680,7 +1713,6 @@
         boolean isReusedCall;
         Uri handle = isConference ? Uri.parse("tel:conf-factory") : participants.get(0);
         Call call = reuseOutgoingCall(handle);
-
         PhoneAccount account =
                 mPhoneAccountRegistrar.getPhoneAccount(requestedAccountHandle, initiatingUser);
         Bundle phoneAccountExtra = account != null ? account.getExtras() : null;
@@ -1721,6 +1753,14 @@
                 call.setConnectionCapabilities(
                         extras.getInt(CallAttributes.CALL_CAPABILITIES_KEY,
                                 CallAttributes.SUPPORTS_SET_INACTIVE), true);
+                if (extras.containsKey(CallAttributes.DISPLAY_NAME_KEY)) {
+                    CharSequence displayName = extras.getCharSequence(
+                            CallAttributes.DISPLAY_NAME_KEY);
+                    if (!TextUtils.isEmpty(displayName)) {
+                        call.setCallerDisplayName(displayName.toString(),
+                                TelecomManager.PRESENTATION_ALLOWED);
+                    }
+                }
                 call.setTargetPhoneAccount(requestedAccountHandle);
             }
 
@@ -3412,10 +3452,11 @@
      */
     boolean holdActiveCallForNewCall(Call call) {
         Call activeCall = (Call) mConnectionSvrFocusMgr.getCurrentFocusCall();
-        Log.i(this, "holdActiveCallForNewCall, newCall: %s, activeCall: %s", call, activeCall);
+        Log.i(this, "holdActiveCallForNewCall, newCall: %s, activeCall: %s", call.getId(),
+                (activeCall == null ? "<none>" : activeCall.getId()));
         if (activeCall != null && activeCall != call) {
             if (canHold(activeCall)) {
-                activeCall.hold();
+                activeCall.hold("swap to " + call.getId());
                 return true;
             } else if (supportsHold(activeCall)
                     && areFromSameSource(activeCall, call)) {
@@ -4900,12 +4941,25 @@
                     liveCallPhoneAccount);
         }
 
-        // First thing, if we are trying to make a call with the same phone account as the live
-        // call, then allow it so that the connection service can make its own decision about
-        // how to handle the new call relative to the current one.
+        // First thing, for managed calls, if we are trying to make a call with the same phone
+        // account as the live call, then allow it so that the connection service can make its own
+        // decision about how to handle the new call relative to the current one.
+        // Note: This behavior is primarily in place because Telephony historically manages the
+        // state of the calls it tracks by itself, holding and unholding as needed.  Self-managed
+        // calls, even though from the same package are normally held/unheld automatically by
+        // Telecom.  Calls within a single ConnectionService get held/unheld automatically during
+        // "swap" operations by CallsManager#holdActiveCallForNewCall.  There is, however, a quirk
+        // in that if an app declares TWO different ConnectionServices, holdActiveCallForNewCall
+        // would not work correctly because focus switches between ConnectionServices, yet we
+        // tended to assume that if the calls are from the same package that the hold/unhold should
+        // be done by the app.  That was a bad assumption as it meant that we could have two active
+        // calls.
+        // TODO(b/280826075): We need to come back and revisit all this logic in a holistic manner.
         if (PhoneAccountHandle.areFromSamePackage(liveCallPhoneAccount,
-                call.getTargetPhoneAccount())) {
-            Log.i(this, "makeRoomForOutgoingCall: phoneAccount matches.");
+                call.getTargetPhoneAccount())
+                && !call.isSelfManaged()
+                && !liveCall.isSelfManaged()) {
+            Log.i(this, "makeRoomForOutgoingCall: managed phoneAccount matches");
             call.getAnalytics().setCallIsAdditional(true);
             liveCall.getAnalytics().setCallIsInterrupted(true);
             return true;
@@ -5485,6 +5539,13 @@
             impl.dump(pw);
             pw.decreaseIndent();
         }
+
+        if (mConnectionSvrFocusMgr != null) {
+            pw.println("mConnectionSvrFocusMgr:");
+            pw.increaseIndent();
+            mConnectionSvrFocusMgr.dump(pw);
+            pw.decreaseIndent();
+        }
     }
 
     /**
@@ -6326,4 +6387,30 @@
     public CallStreamingController getCallStreamingController() {
         return mCallStreamingController;
     }
+
+    /**
+     * Given a call identified by call id, get the instance from the list of calls.
+     * @param callId the call id.
+     * @return the call, or null if not found.
+     */
+    public @Nullable Call getCall(@NonNull String callId) {
+        Optional<Call> foundCall = mCalls.stream().filter(
+                c -> c.getId().equals(callId)).findFirst();
+        if (foundCall.isPresent()) {
+            return foundCall.get();
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Triggers stopping of call streaming for a call by launching a stop streaming transaction.
+     * @param call the call.
+     */
+    public void stopCallStreaming(@NonNull Call call) {
+        if (call.getTransactionServiceWrapper() == null) {
+            return;
+        }
+        call.getTransactionServiceWrapper().stopCallStreaming(call);
+    }
 }
diff --git a/src/com/android/server/telecom/ConnectionServiceFocusManager.java b/src/com/android/server/telecom/ConnectionServiceFocusManager.java
index 6fbc494..3694727 100644
--- a/src/com/android/server/telecom/ConnectionServiceFocusManager.java
+++ b/src/com/android/server/telecom/ConnectionServiceFocusManager.java
@@ -25,8 +25,10 @@
 import android.telecom.Log;
 import android.telecom.Logging.Session;
 import android.text.TextUtils;
+import android.util.LocalLog;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.IndentingPrintWriter;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -41,6 +43,7 @@
 public class ConnectionServiceFocusManager {
     private static final String TAG = "ConnectionSvrFocusMgr";
     private static final int GET_CURRENT_FOCUS_TIMEOUT_MILLIS = 1000;
+    private final LocalLog mLocalLog = new LocalLog(20);
 
     /** Factory interface used to create the {@link ConnectionServiceFocusManager} instance. */
     public interface ConnectionServiceFocusManagerFactory {
@@ -124,6 +127,11 @@
          * @return {@code True} if this call can receive focus, {@code false} otherwise.
          */
         boolean isFocusable();
+
+        /**
+         * @return the ID of the focusable for debug purposes.
+         */
+        String getId();
     }
 
     /** Interface define a call back for focus request event. */
@@ -361,10 +369,11 @@
     }
 
     private void updateCurrentFocusCall() {
+        CallFocus previousFocus = mCurrentFocusCall;
         mCurrentFocusCall = null;
 
         if (mCurrentFocus == null) {
-            Log.d(this, "updateCurrentFocusCall: mCurrentFocus is null");
+            Log.i(this, "updateCurrentFocusCall: mCurrentFocus is null");
             return;
         }
 
@@ -377,11 +386,16 @@
         for (CallFocus call : calls) {
             if (PRIORITY_FOCUS_CALL_STATE.contains(call.getState())) {
                 mCurrentFocusCall = call;
+                if (previousFocus != call) {
+                    mLocalLog.log(call.getId());
+                }
                 Log.i(this, "updateCurrentFocusCall %s", mCurrentFocusCall);
                 return;
             }
         }
-
+        if (previousFocus != null) {
+            mLocalLog.log("<none>");
+        }
         Log.i(this, "updateCurrentFocusCall = null");
     }
 
@@ -477,6 +491,11 @@
         }
     }
 
+    public void dump(IndentingPrintWriter pw) {
+        pw.println("Call Focus History:");
+        mLocalLog.dump(pw);
+    }
+
     private final class FocusManagerHandler extends Handler {
         FocusManagerHandler(Looper looper) {
             super(looper);
diff --git a/src/com/android/server/telecom/EmergencyCallHelper.java b/src/com/android/server/telecom/EmergencyCallHelper.java
index a213e26..fbb666d 100644
--- a/src/com/android/server/telecom/EmergencyCallHelper.java
+++ b/src/com/android/server/telecom/EmergencyCallHelper.java
@@ -21,6 +21,8 @@
 import android.content.pm.PackageManager;
 import android.os.UserHandle;
 import android.telecom.Log;
+import android.telecom.PhoneAccountHandle;
+
 import com.android.internal.annotations.VisibleForTesting;
 
 /**
@@ -34,6 +36,7 @@
     private final DefaultDialerCache mDefaultDialerCache;
     private final Timeouts.Adapter mTimeoutsAdapter;
     private UserHandle mLocationPermissionGrantedToUser;
+    private PhoneAccountHandle mLastOutgoingEmergencyCallPAH;
 
     //stores the original state of permissions that dialer had
     private boolean mHadFineLocation = false;
@@ -46,6 +49,7 @@
     private boolean mBackgroundLocationGranted = false;
 
     private long mLastEmergencyCallTimestampMillis;
+    private long mLastOutgoingEmergencyCallTimestampMillis;
 
     @VisibleForTesting
     public EmergencyCallHelper(
@@ -63,7 +67,7 @@
             grantLocationPermission(userHandle);
         }
         if (call != null && call.isEmergencyCall()) {
-            recordEmergencyCallTime();
+            recordEmergencyCall(call);
         }
     }
 
@@ -78,15 +82,37 @@
         return mLastEmergencyCallTimestampMillis;
     }
 
-    private void recordEmergencyCallTime() {
-        mLastEmergencyCallTimestampMillis = System.currentTimeMillis();
+    void setLastOutgoingEmergencyCallPAH(PhoneAccountHandle accountHandle) {
+        mLastOutgoingEmergencyCallPAH = accountHandle;
     }
 
-    private boolean isInEmergencyCallbackWindow() {
-        return System.currentTimeMillis() - getLastEmergencyCallTimeMillis()
+    public boolean isLastOutgoingEmergencyCallPAH(PhoneAccountHandle currentCallHandle) {
+        boolean ecbmActive = mLastOutgoingEmergencyCallPAH != null
+                && isInEmergencyCallbackWindow(mLastOutgoingEmergencyCallTimestampMillis)
+                && currentCallHandle != null
+                && currentCallHandle.equals(mLastOutgoingEmergencyCallPAH);
+        if (ecbmActive) {
+            Log.i(this, "ECBM is enabled for %s. The last recorded call timestamp was at %s",
+                    currentCallHandle, mLastOutgoingEmergencyCallTimestampMillis);
+        }
+
+        return ecbmActive;
+    }
+
+    boolean isInEmergencyCallbackWindow(long lastEmergencyCallTimestampMillis) {
+        return System.currentTimeMillis() - lastEmergencyCallTimestampMillis
                 < mTimeoutsAdapter.getEmergencyCallbackWindowMillis(mContext.getContentResolver());
     }
 
+    private void recordEmergencyCall(Call call) {
+        mLastEmergencyCallTimestampMillis = System.currentTimeMillis();
+        if (!call.isIncoming()) {
+            // ECBM is applicable to MO emergency calls
+            mLastOutgoingEmergencyCallTimestampMillis = mLastEmergencyCallTimestampMillis;
+            mLastOutgoingEmergencyCallPAH = call.getTargetPhoneAccount();
+        }
+    }
+
     private boolean shouldGrantTemporaryLocationPermission(Call call) {
         if (!mContext.getResources().getBoolean(R.bool.grant_location_permission_enabled)) {
             Log.i(this, "ShouldGrantTemporaryLocationPermission, disabled by config");
@@ -96,7 +122,8 @@
             Log.i(this, "ShouldGrantTemporaryLocationPermission, no call");
             return false;
         }
-        if (!call.isEmergencyCall() && !isInEmergencyCallbackWindow()) {
+        if (!call.isEmergencyCall() && !isInEmergencyCallbackWindow(
+                getLastEmergencyCallTimeMillis())) {
             Log.i(this, "ShouldGrantTemporaryLocationPermission, not emergency");
             return false;
         }
diff --git a/src/com/android/server/telecom/InCallController.java b/src/com/android/server/telecom/InCallController.java
index be27dd1..3d3e3b4 100644
--- a/src/com/android/server/telecom/InCallController.java
+++ b/src/com/android/server/telecom/InCallController.java
@@ -338,7 +338,10 @@
             UserHandle userToBind = getUserFromCall(call);
             boolean isManagedProfile = UserUtil.isManagedProfile(mContext, userToBind);
             // Note that UserHandle.CURRENT fails to capture the work profile, so we need to handle
-            // it separately to ensure that the ICS is bound to the appropriate user.
+            // it separately to ensure that the ICS is bound to the appropriate user. If ECBM is
+            // active, we know that a work sim was previously used to place a MO emergency call. We
+            // need to ensure that we bind to the CURRENT_USER in this case, as the work user would
+            // not be running (handled in getUserFromCall).
             userToBind = isManagedProfile ? userToBind : UserHandle.CURRENT;
             if (!mContext.bindServiceAsUser(intent, mServiceConnection,
                     Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE
@@ -2601,7 +2604,8 @@
             UserManager userManager = mContext.getSystemService(UserManager.class);
             // Emergency call should never be blocked, so if the user associated with call is in
             // quite mode, use the primary user for the emergency call.
-            if (call.isEmergencyCall() && userManager.isQuietModeEnabled(userFromCall)) {
+            if ((call.isEmergencyCall() || call.isInECBM())
+                    && userManager.isQuietModeEnabled(userFromCall)) {
                 return mCallsManager.getCurrentUserHandle();
             }
             return userFromCall;
diff --git a/src/com/android/server/telecom/Ringer.java b/src/com/android/server/telecom/Ringer.java
index cdacab0..45fb2af 100644
--- a/src/com/android/server/telecom/Ringer.java
+++ b/src/com/android/server/telecom/Ringer.java
@@ -626,10 +626,9 @@
     public boolean shouldRingForContact(Call call) {
         // avoid re-computing manager.matcherCallFilter(Bundle)
         if (call.wasDndCheckComputedForCall()) {
-            Log.v(this, "shouldRingForContact: returning computation from DndCallFilter.");
+            Log.i(this, "shouldRingForContact: returning computation from DndCallFilter.");
             return !call.isCallSuppressedByDoNotDisturb();
         }
-
         final Uri contactUri = call.getHandle();
         final Bundle peopleExtras = new Bundle();
         if (contactUri != null) {
@@ -637,13 +636,7 @@
             personList.add(new Person.Builder().setUri(contactUri.toString()).build());
             peopleExtras.putParcelableArrayList(Notification.EXTRA_PEOPLE_LIST, personList);
         }
-
-        // query NotificationManager
-        boolean shouldRing = mNotificationManager.matchesCallFilter(peopleExtras);
-        // store the suppressed status in the call object
-        call.setCallIsSuppressedByDoNotDisturb(!shouldRing);
-
-        return shouldRing;
+        return mNotificationManager.matchesCallFilter(peopleExtras);
     }
 
     private boolean hasExternalRinger(Call foregroundCall) {
diff --git a/src/com/android/server/telecom/TelecomBroadcastIntentProcessor.java b/src/com/android/server/telecom/TelecomBroadcastIntentProcessor.java
index 0be90e0..523b841 100644
--- a/src/com/android/server/telecom/TelecomBroadcastIntentProcessor.java
+++ b/src/com/android/server/telecom/TelecomBroadcastIntentProcessor.java
@@ -101,6 +101,10 @@
     public static final String ACTION_CANCEL_REDIRECTED_CALL =
             "com.android.server.telecom.CANCEL_REDIRECTED_CALL";
 
+    public static final String ACTION_HANGUP_CALL = "com.android.server.telecom.HANGUP_CALL";
+    public static final String ACTION_STOP_STREAMING =
+            "com.android.server.telecom.ACTION_STOP_STREAMING";
+
     public static final String EXTRA_USERHANDLE = "userhandle";
     public static final String EXTRA_REDIRECTION_OUTGOING_CALL_ID =
             "android.telecom.extra.REDIRECTION_OUTGOING_CALL_ID";
@@ -242,6 +246,26 @@
             } finally {
                 Log.endSession();
             }
+        } else if (ACTION_HANGUP_CALL.equals(action)) {
+            Log.startSession("TBIP.aHC", "streamingDialog");
+            try {
+                Call call = mCallsManager.getCall(intent.getData().getSchemeSpecificPart());
+                if (call != null) {
+                    mCallsManager.disconnectCall(call);
+                }
+            } finally {
+                Log.endSession();
+            }
+        } else if (ACTION_STOP_STREAMING.equals(action)) {
+            Log.startSession("TBIP.aSS", "streamingDialog");
+            try {
+                Call call = mCallsManager.getCall(intent.getData().getSchemeSpecificPart());
+                if (call != null) {
+                    mCallsManager.stopCallStreaming(call);
+                }
+            } finally {
+                Log.endSession();
+            }
         }
     }
 
diff --git a/src/com/android/server/telecom/TelecomSystem.java b/src/com/android/server/telecom/TelecomSystem.java
index 8477d49..d3ca0b7 100644
--- a/src/com/android/server/telecom/TelecomSystem.java
+++ b/src/com/android/server/telecom/TelecomSystem.java
@@ -48,6 +48,7 @@
 import com.android.server.telecom.components.UserCallIntentProcessor;
 import com.android.server.telecom.components.UserCallIntentProcessorFactory;
 import com.android.server.telecom.ui.AudioProcessingNotification;
+import com.android.server.telecom.ui.CallStreamingNotification;
 import com.android.server.telecom.ui.DisconnectedCallNotifier;
 import com.android.server.telecom.ui.IncomingCallNotifier;
 import com.android.server.telecom.ui.MissedCallNotifierImpl.MissedCallNotifierImplFactory;
@@ -350,6 +351,12 @@
                     mLock, timeoutsAdapter, clockProxy, emergencyCallDiagnosticLogger);
 
             TransactionManager transactionManager = TransactionManager.getInstance();
+
+            CallStreamingNotification callStreamingNotification =
+                    new CallStreamingNotification(mContext,
+                            packageName -> AppLabelProxy.Util.getAppLabel(
+                                    mContext.getPackageManager(), packageName), asyncTaskExecutor);
+
             mCallsManager = new CallsManager(
                     mContext,
                     mLock,
@@ -386,7 +393,8 @@
                     asyncTaskExecutor,
                     blockedNumbersAdapter,
                     transactionManager,
-                    emergencyCallDiagnosticLogger);
+                    emergencyCallDiagnosticLogger,
+                    callStreamingNotification);
 
             mIncomingCallNotifier = incomingCallNotifier;
             incomingCallNotifier.setCallsManagerProxy(new IncomingCallNotifier.CallsManagerProxy() {
diff --git a/src/com/android/server/telecom/TransactionalServiceWrapper.java b/src/com/android/server/telecom/TransactionalServiceWrapper.java
index d83e551..ec95f39 100644
--- a/src/com/android/server/telecom/TransactionalServiceWrapper.java
+++ b/src/com/android/server/telecom/TransactionalServiceWrapper.java
@@ -682,6 +682,7 @@
 
 
     public void stopCallStreaming(Call call) {
+        Log.i(this, "stopCallStreaming; callid=%s", call.getId());
         if (call != null && call.isStreaming()) {
             VoipCallTransaction stopStreamingTransaction = createStopStreamingTransaction(call);
             addTransactionsToManager(stopStreamingTransaction, new ResultReceiver(null));
diff --git a/src/com/android/server/telecom/ui/CallStreamingNotification.java b/src/com/android/server/telecom/ui/CallStreamingNotification.java
new file mode 100644
index 0000000..752d8c8
--- /dev/null
+++ b/src/com/android/server/telecom/ui/CallStreamingNotification.java
@@ -0,0 +1,345 @@
+/*
+ * 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.ui;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Person;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
+import android.net.Uri;
+import android.os.UserHandle;
+import android.telecom.Log;
+import android.telecom.PhoneAccount;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.TextUtils;
+import android.text.style.ForegroundColorSpan;
+import androidx.core.graphics.drawable.RoundedBitmapDrawable;
+import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.server.telecom.AppLabelProxy;
+import com.android.server.telecom.Call;
+import com.android.server.telecom.CallsManagerListenerBase;
+import com.android.server.telecom.R;
+import com.android.server.telecom.TelecomBroadcastIntentProcessor;
+import com.android.server.telecom.components.TelecomBroadcastReceiver;
+
+import java.util.concurrent.Executor;
+
+/**
+ * Class responsible for tracking if there is a call which is being streamed and posting a
+ * notification which informs the user that a call is streaming.  The user has two possible actions:
+ * disconnect the call, bring the call back to the current device (stop streaming).
+ */
+public class CallStreamingNotification extends CallsManagerListenerBase implements Call.Listener {
+    // URI scheme used for data related to the notification actions.
+    public static final String CALL_ID_SCHEME = "callid";
+    // The default streaming notification ID.
+    private static final int STREAMING_NOTIFICATION_ID = 90210;
+    // Tag for streaming notification.
+    private static final String NOTIFICATION_TAG =
+            CallStreamingNotification.class.getSimpleName();
+
+    private final Context mContext;
+    private final NotificationManager mNotificationManager;
+    // Used to get the app name for the notification.
+    private final AppLabelProxy mAppLabelProxy;
+    // An executor that can be used to fire off async tasks that do not block Telecom in any manner.
+    private final Executor mAsyncTaskExecutor;
+    // The call which is treaming.
+    private Call mStreamingCall;
+    // Lock for notification post/remove -- these happen outside the Telecom sync lock.
+    private final Object mNotificationLock = new Object();
+
+    // Whether the notification is showing.
+    @GuardedBy("mNotificationLock")
+    private boolean mIsNotificationShowing = false;
+    @GuardedBy("mNotificationLock")
+    private UserHandle mNotificationUserHandle;
+
+    public CallStreamingNotification(@NonNull Context context,
+            @NonNull AppLabelProxy appLabelProxy,
+            @NonNull Executor asyncTaskExecutor) {
+        mContext = context;
+        mNotificationManager = context.getSystemService(NotificationManager.class);
+        mAppLabelProxy = appLabelProxy;
+        mAsyncTaskExecutor = asyncTaskExecutor;
+    }
+
+    @Override
+    public void onCallAdded(Call call) {
+        if (call.isStreaming()) {
+            trackStreamingCall(call);
+            enqueueStreamingNotification(call);
+        }
+    }
+
+    @Override
+    public void onCallRemoved(Call call) {
+        if (call == mStreamingCall) {
+            trackStreamingCall(null);
+            dequeueStreamingNotification();
+        }
+    }
+
+    /**
+     * Handles streaming state changes for a call.
+     * @param call the call
+     * @param isStreaming whether it is streaming or not
+     */
+    @Override
+    public void onCallStreamingStateChanged(Call call, boolean isStreaming) {
+        Log.i(this, "onCallStreamingStateChanged: call=%s, isStreaming=%b", call.getId(),
+                isStreaming);
+
+        if (isStreaming) {
+            trackStreamingCall(call);
+            enqueueStreamingNotification(call);
+        } else {
+            trackStreamingCall(null);
+            dequeueStreamingNotification();
+        }
+    }
+
+    /**
+     * Handles changes to the caller info for a call.  Used to ensure we can update the photo uri
+     * if one was found.
+     * @param call the call which the caller info changed on.
+     */
+    @Override
+    public void onCallerInfoChanged(Call call) {
+        if (call == mStreamingCall) {
+            Log.i(this, "onCallerInfoChanged: call=%s, photoUri=%b", call.getId(),
+                    call.getContactPhotoUri());
+            enqueueStreamingNotification(call);
+        }
+    }
+
+    /**
+     * Change the streaming call we are tracking.
+     * @param call the call.
+     */
+    private void trackStreamingCall(Call call) {
+        if (mStreamingCall != null) {
+            mStreamingCall.removeListener(this);
+        }
+        mStreamingCall = call;
+        if (mStreamingCall != null) {
+            mStreamingCall.addListener(this);
+        }
+    }
+
+    /**
+     * Enqueue an async task to post/repost the streaming notification.
+     * Note: This happens INSIDE the telecom lock.
+     * @param call the call to post notification for.
+     */
+    private void enqueueStreamingNotification(Call call) {
+        final Bitmap contactPhotoBitmap = call.getPhotoIcon();
+        mAsyncTaskExecutor.execute(() -> {
+            Icon contactPhotoIcon = null;
+            try {
+                if (contactPhotoBitmap != null) {
+                    // Make the icon rounded... because there has to be hoops to jump through.
+                    RoundedBitmapDrawable roundedDrawable = RoundedBitmapDrawableFactory.create(
+                            mContext.getResources(), contactPhotoBitmap);
+                    roundedDrawable.setCornerRadius(Math.max(contactPhotoBitmap.getWidth(),
+                            contactPhotoBitmap.getHeight()) / 2.0f);
+                    contactPhotoIcon = Icon.createWithBitmap(drawableToBitmap(roundedDrawable,
+                            contactPhotoBitmap.getWidth(), contactPhotoBitmap.getHeight()));
+                }
+            } catch (Exception e) {
+                // All loads of things can do wrong when working with bitmaps and images, so to
+                // ensure Telecom doesn't crash, lets try/catch to be sure.
+                Log.e(this, e, "enqueueStreamingNotification: Couldn't build rounded icon");
+            }
+            showStreamingNotification(call.getId(),
+                    call.getUserHandleFromTargetPhoneAccount(), call.getCallerDisplayName(),
+                    call.getHandle(), contactPhotoIcon,
+                    call.getTargetPhoneAccount().getComponentName().getPackageName(),
+                    call.getConnectTimeMillis());
+        });
+    }
+
+    /**
+     * Dequeues the call streaming notification.
+     * Note: This is yo be called within the Telecom sync lock to launch the task to remove the call
+     * streaming notification.
+     */
+    private void dequeueStreamingNotification() {
+        mAsyncTaskExecutor.execute(() -> hideStreamingNotification());
+    }
+
+    /**
+     * Show the call streaming notification.  This is intended to run outside the Telecom sync lock.
+     *
+     * @param callId the call ID we're streaming.
+     * @param userHandle the userhandle for the call.
+     * @param callerName the name of the caller/callee associated with the call
+     * @param callerAddress the address associated with the caller/callee
+     * @param photoIcon the contact photo icon if available
+     * @param appPackageName the package name for the app to post the notification for
+     * @param connectTimeMillis when the call connected (for chronometer in the notification)
+     */
+    private void showStreamingNotification(final String callId, final UserHandle userHandle,
+            String callerName, Uri callerAddress, Icon photoIcon, String appPackageName,
+            long connectTimeMillis) {
+        Log.i(this, "showStreamingNotification; callid=%s, hasPhoto=%b", callId, photoIcon != null);
+
+        // Use the caller name for the label if available, default to app name if none.
+        if (TextUtils.isEmpty(callerName)) {
+            // App did not provide a caller name, so default to app's name.
+            callerName = mAppLabelProxy.getAppLabel(appPackageName).toString();
+        }
+
+        // Action to hangup; this can use the default hangup action from the call style
+        // notification.
+        Intent hangupIntent = new Intent(TelecomBroadcastIntentProcessor.ACTION_HANGUP_CALL,
+                Uri.fromParts(CALL_ID_SCHEME, callId, null),
+                mContext, TelecomBroadcastReceiver.class);
+        PendingIntent hangupPendingIntent = PendingIntent.getBroadcast(mContext, 0, hangupIntent,
+                PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
+
+        // Action to switch here.
+        Intent switchHereIntent = new Intent(TelecomBroadcastIntentProcessor.ACTION_STOP_STREAMING,
+                Uri.fromParts(CALL_ID_SCHEME, callId, null),
+                mContext, TelecomBroadcastReceiver.class);
+        PendingIntent switchHerePendingIntent = PendingIntent.getBroadcast(mContext, 0,
+                switchHereIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
+
+        // Apply a span to the string to colorize it using the "answer" color.
+        Spannable spannable = new SpannableString(
+                mContext.getString(R.string.call_streaming_notification_action_switch_here));
+        spannable.setSpan(new ForegroundColorSpan(
+                com.android.internal.R.color.call_notification_answer_color), 0, spannable.length(),
+                Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
+
+        // Use the "phone link" icon per mock.
+        Icon switchHereIcon = Icon.createWithResource(mContext, R.drawable.gm_phonelink);
+        Notification.Action.Builder switchHereBuilder = new Notification.Action.Builder(
+                switchHereIcon,
+                spannable,
+                switchHerePendingIntent);
+        Notification.Action switchHereAction = switchHereBuilder.build();
+
+        // Notifications use a "person" entity to identify caller/callee.
+        Person.Builder personBuilder = new Person.Builder()
+                .setName(callerName);
+
+        // Some apps use phone numbers to identify; these are something the notification framework
+        // can lookup in contacts to provide more data
+        if (callerAddress != null && PhoneAccount.SCHEME_TEL.equals(callerAddress)) {
+            personBuilder.setUri(callerAddress.toString());
+        }
+        if (photoIcon != null) {
+            personBuilder.setIcon(photoIcon);
+        }
+        Person person = personBuilder.build();
+
+        // Call Style notification requires a full screen intent, so we'll just link in a null
+        // pending intent
+        Intent nullIntent = new Intent();
+        PendingIntent nullPendingIntent = PendingIntent.getBroadcast(mContext, 0, nullIntent,
+                PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
+
+        Notification.Builder builder = new Notification.Builder(mContext,
+                NotificationChannelManager.CHANNEL_ID_CALL_STREAMING)
+                // Use call style to get the general look and feel for the notification; it provides
+                // a hangup action with the right action already so we can leverage that.  The
+                // "switch here" action will be a custom action defined later.
+                .setStyle(Notification.CallStyle.forOngoingCall(person, hangupPendingIntent))
+                .setSmallIcon(R.drawable.ic_phone)
+                .setContentText(mContext.getString(
+                        R.string.call_streaming_notification_body))
+                // Report call time
+                .setWhen(connectTimeMillis)
+                .setShowWhen(true)
+                .setUsesChronometer(true)
+                // Set the full screen intent; this is just tricking notification manager into
+                // letting us use this style.  Sssh.
+                .setFullScreenIntent(nullPendingIntent, true)
+                .setColorized(true)
+                .addAction(switchHereAction);
+        Notification notification = builder.build();
+
+        synchronized(mNotificationLock) {
+            mIsNotificationShowing = true;
+            mNotificationUserHandle = userHandle;
+            try {
+                mNotificationManager.notifyAsUser(NOTIFICATION_TAG, STREAMING_NOTIFICATION_ID,
+                        notification, userHandle);
+            } catch (Exception e) {
+                // We don't want to crash Telecom if something changes with the requirements for the
+                // notification.
+                Log.e(this, e, "Notification post failed.");
+            }
+        }
+    }
+
+    /**
+     * Removes the posted streaming notification.  Intended to run outside the telecom lock.
+     */
+    private void hideStreamingNotification() {
+        Log.i(this, "hideStreamingNotification");
+        synchronized(mNotificationLock) {
+            if (mIsNotificationShowing) {
+                mIsNotificationShowing = false;
+                mNotificationManager.cancelAsUser(NOTIFICATION_TAG,
+                        STREAMING_NOTIFICATION_ID, mNotificationUserHandle);
+            }
+        }
+    }
+
+    public static Bitmap drawableToBitmap(@Nullable Drawable drawable, int width, int height) {
+        if (drawable == null) {
+            return null;
+        }
+
+        Bitmap bitmap;
+        if (drawable instanceof BitmapDrawable) {
+            bitmap = ((BitmapDrawable) drawable).getBitmap();
+        } else {
+            if (width > 0 || height > 0) {
+                bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+            } else if (drawable.getIntrinsicWidth() <= 0 || drawable.getIntrinsicHeight() <= 0) {
+                // Needed for drawables that are just a colour.
+                bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
+            } else {
+                bitmap =
+                        Bitmap.createBitmap(
+                                drawable.getIntrinsicWidth(),
+                                drawable.getIntrinsicHeight(),
+                                Bitmap.Config.ARGB_8888);
+            }
+
+            Canvas canvas = new Canvas(bitmap);
+            drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
+            drawable.draw(canvas);
+        }
+        return bitmap;
+    }
+}
diff --git a/src/com/android/server/telecom/ui/NotificationChannelManager.java b/src/com/android/server/telecom/ui/NotificationChannelManager.java
index 58794a6..a0baa03 100644
--- a/src/com/android/server/telecom/ui/NotificationChannelManager.java
+++ b/src/com/android/server/telecom/ui/NotificationChannelManager.java
@@ -40,6 +40,7 @@
     public static final String CHANNEL_ID_AUDIO_PROCESSING = "TelecomBackgroundAudioProcessing";
     public static final String CHANNEL_ID_DISCONNECTED_CALLS = "TelecomDisconnectedCalls";
     public static final String CHANNEL_ID_IN_CALL_SERVICE_CRASH = "TelecomInCallServiceCrash";
+    public static final String CHANNEL_ID_CALL_STREAMING = "TelecomCallStreaming";
 
     private BroadcastReceiver mLocaleChangeReceiver = new BroadcastReceiver() {
         @Override
@@ -63,6 +64,7 @@
         createOrUpdateChannel(context, CHANNEL_ID_AUDIO_PROCESSING);
         createOrUpdateChannel(context, CHANNEL_ID_DISCONNECTED_CALLS);
         createOrUpdateChannel(context, CHANNEL_ID_IN_CALL_SERVICE_CRASH);
+        createOrUpdateChannel(context, CHANNEL_ID_CALL_STREAMING);
     }
 
     private void createOrUpdateChannel(Context context, String channelId) {
@@ -127,6 +129,14 @@
                 lights = true;
                 vibration = true;
                 sound = null;
+            case CHANNEL_ID_CALL_STREAMING:
+                name = context.getText(R.string.notification_channel_call_streaming);
+                importance = NotificationManager.IMPORTANCE_DEFAULT;
+                canShowBadge = false;
+                lights = false;
+                vibration = false;
+                sound = null;
+                break;
         }
 
         NotificationChannel channel = new NotificationChannel(channelId, name, importance);
diff --git a/src/com/android/server/telecom/voip/IncomingCallTransaction.java b/src/com/android/server/telecom/voip/IncomingCallTransaction.java
index c0bb93d..d35030c 100644
--- a/src/com/android/server/telecom/voip/IncomingCallTransaction.java
+++ b/src/com/android/server/telecom/voip/IncomingCallTransaction.java
@@ -17,6 +17,7 @@
 package com.android.server.telecom.voip;
 
 import static android.telecom.CallAttributes.CALL_CAPABILITIES_KEY;
+import static android.telecom.CallAttributes.DISPLAY_NAME_KEY;
 
 import android.os.Bundle;
 import android.telecom.CallAttributes;
@@ -80,6 +81,9 @@
         mExtras.putString(TelecomManager.TRANSACTION_CALL_ID_KEY, mCallId);
         mExtras.putInt(CALL_CAPABILITIES_KEY, callAttributes.getCallCapabilities());
         mExtras.putInt(TelecomManager.EXTRA_INCOMING_VIDEO_STATE, callAttributes.getCallType());
+        mExtras.putCharSequence(DISPLAY_NAME_KEY, callAttributes.getDisplayName());
+        mExtras.putParcelable(TelecomManager.EXTRA_INCOMING_CALL_ADDRESS,
+                callAttributes.getAddress());
         return mExtras;
     }
 }
diff --git a/src/com/android/server/telecom/voip/OutgoingCallTransaction.java b/src/com/android/server/telecom/voip/OutgoingCallTransaction.java
index 0b17da2..b2625e6 100644
--- a/src/com/android/server/telecom/voip/OutgoingCallTransaction.java
+++ b/src/com/android/server/telecom/voip/OutgoingCallTransaction.java
@@ -18,6 +18,7 @@
 
 import static android.Manifest.permission.CALL_PRIVILEGED;
 import static android.telecom.CallAttributes.CALL_CAPABILITIES_KEY;
+import static android.telecom.CallAttributes.DISPLAY_NAME_KEY;
 import static android.telecom.CallException.CODE_CALL_NOT_PERMITTED_AT_PRESENT_TIME;
 
 import android.content.Context;
@@ -126,6 +127,7 @@
         mExtras.putInt(CALL_CAPABILITIES_KEY, callAttributes.getCallCapabilities());
         mExtras.putInt(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE,
                 callAttributes.getCallType());
+        mExtras.putCharSequence(DISPLAY_NAME_KEY, callAttributes.getDisplayName());
         return mExtras;
     }
 }
diff --git a/src/com/android/server/telecom/voip/VoipCallTransaction.java b/src/com/android/server/telecom/voip/VoipCallTransaction.java
index a1cc13c..38f1d54 100644
--- a/src/com/android/server/telecom/voip/VoipCallTransaction.java
+++ b/src/com/android/server/telecom/voip/VoipCallTransaction.java
@@ -18,6 +18,7 @@
 
 import android.os.Handler;
 import android.os.HandlerThread;
+import android.telecom.Log;
 
 import com.android.server.telecom.LoggedHandlerExecutor;
 import com.android.server.telecom.TelecomSystem;
@@ -80,7 +81,11 @@
                             }
                             finish();
                             return null;
-                        });
+                        })
+                .exceptionally((throwable) -> {
+                    Log.e(this, throwable, "Error while executing transaction.");
+                    return null;
+                });;
     }
 
     public CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
diff --git a/testapps/AndroidManifest.xml b/testapps/AndroidManifest.xml
index dd8258a..645a42b 100644
--- a/testapps/AndroidManifest.xml
+++ b/testapps/AndroidManifest.xml
@@ -259,6 +259,15 @@
           </intent-filter>
         </service>
 
+        <service android:name="com.android.server.telecom.testapps.OtherSelfManagedConnectionService"
+                 android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE"
+                 android:process="com.android.server.telecom.testapps.SelfMangingCallingApp"
+                 android:exported="true">
+            <intent-filter>
+                <action android:name="android.telecom.ConnectionService"/>
+            </intent-filter>
+        </service>
+
         <receiver android:exported="false"
              android:process="com.android.server.telecom.testapps.SelfMangingCallingApp"
              android:name="com.android.server.telecom.testapps.SelfManagedCallNotificationReceiver"/>
diff --git a/testapps/res/layout/self_managed_sample_main.xml b/testapps/res/layout/self_managed_sample_main.xml
index d26d629..98b879a 100644
--- a/testapps/res/layout/self_managed_sample_main.xml
+++ b/testapps/res/layout/self_managed_sample_main.xml
@@ -55,6 +55,12 @@
                 android:layout_height="wrap_content"
                 android:background="@color/test_call_b_color"
                 android:text="2"/>
+            <RadioButton
+                android:id="@+id/useAcct3Button"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:background="@color/test_call_c_color"
+                android:text="3"/>
         </RadioGroup>
         <TextView
             android:id="@+id/hasFocus"
diff --git a/testapps/res/values/colors.xml b/testapps/res/values/colors.xml
index 3939e78..9447ac8 100644
--- a/testapps/res/values/colors.xml
+++ b/testapps/res/values/colors.xml
@@ -17,4 +17,5 @@
 <resources>
     <color name="test_call_a_color">#f2eebf</color>
     <color name="test_call_b_color">#afc5e6</color>
+    <color name="test_call_c_color">#c5afe6</color>
 </resources>
diff --git a/testapps/src/com/android/server/telecom/testapps/OtherSelfManagedConnectionService.java b/testapps/src/com/android/server/telecom/testapps/OtherSelfManagedConnectionService.java
new file mode 100644
index 0000000..7bb9830
--- /dev/null
+++ b/testapps/src/com/android/server/telecom/testapps/OtherSelfManagedConnectionService.java
@@ -0,0 +1,20 @@
+/*
+ * 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.testapps;
+
+public class OtherSelfManagedConnectionService extends SelfManagedConnectionService {
+}
diff --git a/testapps/src/com/android/server/telecom/testapps/SelfManagedCallList.java b/testapps/src/com/android/server/telecom/testapps/SelfManagedCallList.java
index d4661ff..273b060 100644
--- a/testapps/src/com/android/server/telecom/testapps/SelfManagedCallList.java
+++ b/testapps/src/com/android/server/telecom/testapps/SelfManagedCallList.java
@@ -46,20 +46,27 @@
 
     public static String SELF_MANAGED_ACCOUNT_1 = "1";
     public static String SELF_MANAGED_ACCOUNT_2 = "2";
+    public static String SELF_MANAGED_ACCOUNT_1A = "1A";
     public static String SELF_MANAGED_ACCOUNT_3 = "3";
     public static String SELF_MANAGED_NAME_1 = "SuperCall";
     public static String SELF_MANAGED_NAME_2 = "Mega Call";
-    public static String SELF_MANAGED_NAME_3 = "SM Call";
+    public static String SELF_MANAGED_NAME_1A = "SM Call";
+    public static String SELF_MANAGED_NAME_3 = "Sep Process";
     public static String CUSTOM_URI_SCHEME = "custom";
 
     private static SelfManagedCallList sInstance;
     private static ComponentName COMPONENT_NAME = new ComponentName(
             SelfManagedCallList.class.getPackage().getName(),
             SelfManagedConnectionService.class.getName());
+    private static ComponentName OTHER_COMPONENT_NAME = new ComponentName(
+            SelfManagedCallList.class.getPackage().getName(),
+            OtherSelfManagedConnectionService.class.getName());
     private static Uri SELF_MANAGED_ADDRESS_1 = Uri.fromParts(PhoneAccount.SCHEME_TEL, "555-1212",
             "");
     private static Uri SELF_MANAGED_ADDRESS_2 = Uri.fromParts(PhoneAccount.SCHEME_SIP,
             "me@test.org", "");
+    private static Uri SELF_MANAGED_ADDRESS_3 = Uri.fromParts(PhoneAccount.SCHEME_SIP,
+            "hilda@test.org", "");
     private static Map<String, PhoneAccountHandle> mPhoneAccounts = new ArrayMap();
 
     public static SelfManagedCallList getInstance() {
@@ -101,20 +108,29 @@
                 SELF_MANAGED_NAME_1, true /* areCallsLogged */);
         registerPhoneAccount(context, SELF_MANAGED_ACCOUNT_2, SELF_MANAGED_ADDRESS_2,
                 SELF_MANAGED_NAME_2, false /* areCallsLogged */);
-        registerPhoneAccount(context, SELF_MANAGED_ACCOUNT_3, SELF_MANAGED_ADDRESS_1,
-                SELF_MANAGED_NAME_3, true /* areCallsLogged */);
+        registerPhoneAccount(context, SELF_MANAGED_ACCOUNT_1A, SELF_MANAGED_ADDRESS_1,
+                SELF_MANAGED_NAME_1A, true /* areCallsLogged */);
+        registerPhoneAccount(context, SELF_MANAGED_ACCOUNT_1A, SELF_MANAGED_ADDRESS_1,
+                SELF_MANAGED_NAME_1A, true /* areCallsLogged */);
+        registerPhoneAccount(context, OTHER_COMPONENT_NAME, SELF_MANAGED_ACCOUNT_3,
+                SELF_MANAGED_ADDRESS_3, SELF_MANAGED_NAME_3, false /* areCallsLogged */);
     }
 
     public void registerPhoneAccount(Context context, String id, Uri address, String name,
-                                     boolean areCallsLogged) {
-        PhoneAccountHandle handle = new PhoneAccountHandle(COMPONENT_NAME, id);
+            boolean areCallsLogged) {
+        registerPhoneAccount(context, COMPONENT_NAME, id, address, name, areCallsLogged);
+    }
+
+    public void registerPhoneAccount(Context context, ComponentName componentName, String id,
+            Uri address, String name, boolean areCallsLogged) {
+        PhoneAccountHandle handle = new PhoneAccountHandle(componentName, id);
         mPhoneAccounts.put(id, handle);
         Bundle extras = new Bundle();
         extras.putBoolean(PhoneAccount.EXTRA_SUPPORTS_HANDOVER_TO, true);
         if (areCallsLogged) {
             extras.putBoolean(PhoneAccount.EXTRA_LOG_SELF_MANAGED_CALLS, true);
         }
-        if (id.equals(SELF_MANAGED_ACCOUNT_3)) {
+        if (id.equals(SELF_MANAGED_ACCOUNT_1A)) {
             extras.putBoolean(PhoneAccount.EXTRA_ADD_SELF_MANAGED_CALLS_TO_INCALLSERVICE, true);
         }
         PhoneAccount.Builder builder = PhoneAccount.builder(handle, name)
diff --git a/testapps/src/com/android/server/telecom/testapps/SelfManagedCallListAdapter.java b/testapps/src/com/android/server/telecom/testapps/SelfManagedCallListAdapter.java
index 75ceb62..475f255 100644
--- a/testapps/src/com/android/server/telecom/testapps/SelfManagedCallListAdapter.java
+++ b/testapps/src/com/android/server/telecom/testapps/SelfManagedCallListAdapter.java
@@ -166,8 +166,10 @@
                 SelfManagedConnection.EXTRA_PHONE_ACCOUNT_HANDLE);
         if (phoneAccountHandle.getId().equals(SelfManagedCallList.SELF_MANAGED_ACCOUNT_1)) {
             result.setBackgroundColor(result.getContext().getColor(R.color.test_call_a_color));
-        } else {
+        } else if (phoneAccountHandle.getId().equals(SelfManagedCallList.SELF_MANAGED_ACCOUNT_2)) {
             result.setBackgroundColor(result.getContext().getColor(R.color.test_call_b_color));
+        } else {
+            result.setBackgroundColor(result.getContext().getColor(R.color.test_call_c_color));
         }
 
         CallAudioState audioState = connection.getCallAudioState();
diff --git a/testapps/src/com/android/server/telecom/testapps/SelfManagedCallingActivity.java b/testapps/src/com/android/server/telecom/testapps/SelfManagedCallingActivity.java
index 44410d2..5cdaf3d 100644
--- a/testapps/src/com/android/server/telecom/testapps/SelfManagedCallingActivity.java
+++ b/testapps/src/com/android/server/telecom/testapps/SelfManagedCallingActivity.java
@@ -43,8 +43,6 @@
 import android.widget.TextView;
 import android.widget.Toast;
 
-import com.android.server.telecom.testapps.R;
-
 import java.util.Objects;
 
 /**
@@ -66,6 +64,7 @@
     private Button mDisableCarMode;
     private RadioButton mUseAcct1Button;
     private RadioButton mUseAcct2Button;
+    private RadioButton mUseAcct3Button;
     private CheckBox mHoldableCheckbox;
     private CheckBox mVideoCallCheckbox;
     private EditText mNumber;
@@ -165,6 +164,7 @@
         }));
         mUseAcct1Button = findViewById(R.id.useAcct1Button);
         mUseAcct2Button = findViewById(R.id.useAcct2Button);
+        mUseAcct3Button = findViewById(R.id.useAcct3Button);
         mHasFocus = findViewById(R.id.hasFocus);
         mVideoCallCheckbox = findViewById(R.id.videoCall);
         mHoldableCheckbox = findViewById(R.id.holdable);
@@ -183,6 +183,8 @@
             return mCallList.getPhoneAccountHandle(SelfManagedCallList.SELF_MANAGED_ACCOUNT_1);
         } else if (mUseAcct2Button.isChecked()) {
             return mCallList.getPhoneAccountHandle(SelfManagedCallList.SELF_MANAGED_ACCOUNT_2);
+        } else if (mUseAcct3Button.isChecked()) {
+            return mCallList.getPhoneAccountHandle(SelfManagedCallList.SELF_MANAGED_ACCOUNT_3);
         }
         return null;
     }
@@ -214,8 +216,7 @@
 
     private void placeSelfManagedOutgoingCall() {
         TelecomManager tm = TelecomManager.from(this);
-        PhoneAccountHandle phoneAccountHandle = mCallList.getPhoneAccountHandle(
-                SelfManagedCallList.SELF_MANAGED_ACCOUNT_3);
+        PhoneAccountHandle phoneAccountHandle = getSelectedPhoneAccountHandle();
 
         if (mCheckIfPermittedBeforeCalling.isChecked()) {
             Toast.makeText(this, R.string.outgoingCallNotPermitted, Toast.LENGTH_SHORT).show();
@@ -264,7 +265,7 @@
     private void placeSelfManagedIncomingCall() {
         TelecomManager tm = TelecomManager.from(this);
         PhoneAccountHandle phoneAccountHandle = mCallList.getPhoneAccountHandle(
-                SelfManagedCallList.SELF_MANAGED_ACCOUNT_3);
+                SelfManagedCallList.SELF_MANAGED_ACCOUNT_1A);
 
         if (mCheckIfPermittedBeforeCalling.isChecked()) {
             if (!tm.isIncomingCallPermitted(phoneAccountHandle)) {
diff --git a/testapps/streamingtest/Android.bp b/testapps/streamingtest/Android.bp
new file mode 100644
index 0000000..bd0a582
--- /dev/null
+++ b/testapps/streamingtest/Android.bp
@@ -0,0 +1,31 @@
+//
+// 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.
+//
+
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+    name: "streamingTestApp",
+    static_libs: [
+        "androidx.legacy_legacy-support-v4",
+        "guava",
+    ],
+    srcs: ["src/**/*.java"],
+    platform_apis: true,
+    certificate: "platform",
+    privileged: true,
+}
diff --git a/testapps/streamingtest/AndroidManifest.xml b/testapps/streamingtest/AndroidManifest.xml
new file mode 100644
index 0000000..47e4abc
--- /dev/null
+++ b/testapps/streamingtest/AndroidManifest.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          coreApp="true"
+          package="com.android.server.telecom.streamingtest">
+
+    <uses-sdk android:minSdkVersion="28"
+              android:targetSdkVersion="33"/>
+
+    <uses-permission android:name="android.permission.RECORD_AUDIO"/>
+    <uses-permission android:name="android.permission.CALL_AUDIO_INTERCEPTION"/>
+
+    <application android:label="Streaming Test App">
+        <uses-library android:name="android.test.runner"/>
+
+        <service android:name="com.android.server.telecom.streamingtest.StreamingService"
+                 android:exported="true"
+                 android:permission="android.permission.BIND_CALL_STREAMING_SERVICE">
+            <intent-filter>
+                <action android:name="android.telecom.CallStreamingService"/>
+            </intent-filter>
+        </service>
+    </application>
+</manifest>
diff --git a/testapps/streamingtest/src/com/android/server/telecom/streamingtest/StreamingService.java b/testapps/streamingtest/src/com/android/server/telecom/streamingtest/StreamingService.java
new file mode 100644
index 0000000..c76b349
--- /dev/null
+++ b/testapps/streamingtest/src/com/android/server/telecom/streamingtest/StreamingService.java
@@ -0,0 +1,46 @@
+/*
+ * 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.streamingtest;
+
+import android.annotation.NonNull;
+import android.content.Intent;
+import android.telecom.CallStreamingService;
+import android.telecom.StreamingCall;
+import android.telecom.Log;
+
+public class StreamingService extends CallStreamingService {
+    @Override
+    public void onCallStreamingStarted(@NonNull StreamingCall call) {
+        Log.i(this, "onCallStreamingStarted: call %s", call);
+    }
+
+    @Override
+    public void onCallStreamingStopped() {
+        Log.i(this, "onCallStreamingStopped");
+    }
+
+    @Override
+    public void onCallStreamingStateChanged(int state) {
+        Log.i(this, "onCallStreamingStateChanged; state=%d", state);
+    }
+
+    @Override
+    public boolean onUnbind(Intent intent) {
+        Log.i(this, "onUnbind");
+        return false;
+    }
+}
diff --git a/testapps/transactionalVoipApp/src/com/android/server/telecom/transactionalVoipApp/InCallActivity.java b/testapps/transactionalVoipApp/src/com/android/server/telecom/transactionalVoipApp/InCallActivity.java
index b868b70..3e53800 100644
--- a/testapps/transactionalVoipApp/src/com/android/server/telecom/transactionalVoipApp/InCallActivity.java
+++ b/testapps/transactionalVoipApp/src/com/android/server/telecom/transactionalVoipApp/InCallActivity.java
@@ -57,6 +57,11 @@
         setContentView(R.layout.in_call_activity);
 
         Bundle extras = getIntent().getExtras();
+        // Copy the extras with properties like call direction into the extras so the below
+        // code can access them.
+        if (extras != null && extras.containsKey(Utils.sEXTRAS_KEY)) {
+            extras.putAll(extras.getBundle(Utils.sEXTRAS_KEY));
+        }
         if (extras != null) {
             mCallDirection = extras.getInt(Utils.sCALL_DIRECTION_KEY, DIRECTION_INCOMING);
         }
@@ -211,7 +216,7 @@
                         Utils.PHONE_ACCOUNT_HANDLE,
                         mCallDirection,
                         "Alan Turing",
-                        Uri.parse("tel:6506959001")).build();
+                        Uri.parse("tel:+16506959001")).build();
 
         mTelecomManager.addCall(callAttributes, Runnable::run,
                 new OutcomeReceiver<CallControl, CallException>() {
diff --git a/tests/src/com/android/server/telecom/tests/BasicCallTests.java b/tests/src/com/android/server/telecom/tests/BasicCallTests.java
index 453450d..9047da3 100644
--- a/tests/src/com/android/server/telecom/tests/BasicCallTests.java
+++ b/tests/src/com/android/server/telecom/tests/BasicCallTests.java
@@ -44,6 +44,7 @@
 import android.os.Bundle;
 import android.os.Process;
 import android.os.UserHandle;
+import android.os.UserManager;
 import android.provider.BlockedNumberContract;
 import android.telecom.Call;
 import android.telecom.CallAudioState;
diff --git a/tests/src/com/android/server/telecom/tests/CallsManagerTest.java b/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
index 2d7fcc7..8a7d22c 100644
--- a/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
@@ -16,6 +16,8 @@
 
 package com.android.server.telecom.tests;
 
+import static android.provider.CallLog.Calls.USER_MISSED_NOT_RUNNING;
+
 import static junit.framework.Assert.assertNotNull;
 import static junit.framework.TestCase.fail;
 
@@ -58,7 +60,9 @@
 import android.os.ResultReceiver;
 import android.os.SystemClock;
 import android.os.UserHandle;
+import android.os.UserManager;
 import android.provider.BlockedNumberContract;
+import android.provider.Telephony;
 import android.telecom.CallException;
 import android.telecom.CallScreeningService;
 import android.telecom.CallerInfo;
@@ -121,6 +125,7 @@
 import com.android.server.telecom.callfiltering.BlockedNumbersAdapter;
 import com.android.server.telecom.callfiltering.CallFilteringResult;
 import com.android.server.telecom.ui.AudioProcessingNotification;
+import com.android.server.telecom.ui.CallStreamingNotification;
 import com.android.server.telecom.ui.DisconnectedCallNotifier;
 import com.android.server.telecom.ui.ToastFactory;
 import com.android.server.telecom.voip.TransactionManager;
@@ -166,6 +171,8 @@
             ComponentName.unflattenFromString("com.voip/.Stuff"), "Voip1");
     private static final PhoneAccountHandle SELF_MANAGED_HANDLE = new PhoneAccountHandle(
             ComponentName.unflattenFromString("com.baz/.Self"), "Self");
+    private static final PhoneAccountHandle SELF_MANAGED_2_HANDLE = new PhoneAccountHandle(
+            ComponentName.unflattenFromString("com.baz/.Self2"), "Self2");
     private static final PhoneAccount SIM_1_ACCOUNT = new PhoneAccount.Builder(SIM_1_HANDLE, "Sim1")
             .setCapabilities(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION
                     | PhoneAccount.CAPABILITY_CALL_PROVIDER
@@ -190,6 +197,11 @@
             .setCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED)
             .setIsEnabled(true)
             .build();
+    private static final PhoneAccount SELF_MANAGED_2_ACCOUNT = new PhoneAccount.Builder(
+            SELF_MANAGED_2_HANDLE, "Self2")
+            .setCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED)
+            .setIsEnabled(true)
+            .build();
     private static final Uri TEST_ADDRESS = Uri.parse("tel:555-1212");
     private static final Uri TEST_ADDRESS2 = Uri.parse("tel:555-1213");
     private static final Uri TEST_ADDRESS3 = Uri.parse("tel:555-1214");
@@ -246,6 +258,7 @@
     @Mock private Ringer.AccessibilityManagerAdapter mAccessibilityManagerAdapter;
     @Mock private BlockedNumbersAdapter mBlockedNumbersAdapter;
     @Mock private PhoneCapability mPhoneCapability;
+    @Mock private CallStreamingNotification mCallStreamingNotification;
 
     private CallsManager mCallsManager;
 
@@ -316,7 +329,8 @@
                 command -> command.run(),
                 mBlockedNumbersAdapter,
                 TransactionManager.getTestInstance(),
-                mEmergencyCallDiagnosticLogger);
+                mEmergencyCallDiagnosticLogger,
+                mCallStreamingNotification);
 
         when(mPhoneAccountRegistrar.getPhoneAccount(
                 eq(SELF_MANAGED_HANDLE), any())).thenReturn(SELF_MANAGED_ACCOUNT);
@@ -818,7 +832,7 @@
         mCallsManager.answerCall(incomingCall, VideoProfile.STATE_AUDIO_ONLY);
 
         // THEN the ongoing call is held and the focus request for incoming call is sent
-        verify(ongoingCall).hold();
+        verify(ongoingCall).hold(anyString());
         verifyFocusRequestAndExecuteCallback(incomingCall);
 
         // and the incoming call is answered.
@@ -1084,7 +1098,7 @@
         mCallsManager.markCallAsActive(newCall);
 
         // THEN the ongoing call is held
-        verify(ongoingCall).hold();
+        verify(ongoingCall).hold(anyString());
         verifyFocusRequestAndExecuteCallback(newCall);
 
         // and the new call is active
@@ -1741,6 +1755,57 @@
         assertTrue(mCallsManager.makeRoomForOutgoingCall(ongoingCall2));
     }
 
+    /**
+     * Test where a VoIP app adds another new call and has one active already; ensure we hold the
+     * active call.  This assumes same connection service in the same app.
+     */
+    @SmallTest
+    @Test
+    public void testMakeRoomForOutgoingCallForSameVoipApp() {
+        Call activeCall = addSpyCall(SELF_MANAGED_HANDLE, null /* connMgr */,
+                CallState.ACTIVE, Connection.CAPABILITY_HOLD | Connection.CAPABILITY_SUPPORT_HOLD,
+                0 /* properties */);
+        Call newDialingCall = createCall(SELF_MANAGED_HANDLE, CallState.DIALING);
+        newDialingCall.setConnectionProperties(Connection.CAPABILITY_HOLD
+                        | Connection.CAPABILITY_SUPPORT_HOLD);
+        assertTrue(mCallsManager.makeRoomForOutgoingCall(newDialingCall));
+        verify(activeCall).hold(anyString());
+    }
+
+    /**
+     * Test where a VoIP app adds another new call and has one active already; ensure we hold the
+     * active call.  This assumes different connection services in the same app.
+     */
+    @SmallTest
+    @Test
+    public void testMakeRoomForOutgoingCallForSameVoipAppDifferentConnectionService() {
+        Call activeCall = addSpyCall(SELF_MANAGED_HANDLE, null /* connMgr */,
+                CallState.ACTIVE, Connection.CAPABILITY_HOLD | Connection.CAPABILITY_SUPPORT_HOLD,
+                0 /* properties */);
+        Call newDialingCall = createCall(SELF_MANAGED_2_HANDLE, CallState.DIALING);
+        newDialingCall.setConnectionProperties(Connection.CAPABILITY_HOLD
+                | Connection.CAPABILITY_SUPPORT_HOLD);
+        assertTrue(mCallsManager.makeRoomForOutgoingCall(newDialingCall));
+        verify(activeCall).hold(anyString());
+    }
+
+    /**
+     * Test where a VoIP app adds another new call and has one active already; ensure we hold the
+     * active call.  This assumes different connection services in the same app.
+     */
+    @SmallTest
+    @Test
+    public void testMakeRoomForOutgoingCallForSameNonVoipApp() {
+        Call activeCall = addSpyCall(SIM_1_HANDLE, null /* connMgr */,
+                CallState.ACTIVE, Connection.CAPABILITY_HOLD | Connection.CAPABILITY_SUPPORT_HOLD,
+                0 /* properties */);
+        Call newDialingCall = createCall(SIM_1_HANDLE, CallState.DIALING);
+        newDialingCall.setConnectionProperties(Connection.CAPABILITY_HOLD
+                | Connection.CAPABILITY_SUPPORT_HOLD);
+        assertTrue(mCallsManager.makeRoomForOutgoingCall(newDialingCall));
+        verify(activeCall, never()).hold(anyString());
+    }
+
     @SmallTest
     @Test
     public void testMakeRoomForOutgoingCallHasOutgoingCallSelectingAccount() {
@@ -2415,6 +2480,65 @@
         assertEquals(DEFAULT_CALL_SCREENING_APP, outgoingCall.getPostCallPackageName());
     }
 
+    @SmallTest
+    @Test
+    public void testRejectIncomingCallOnPAHInactive() throws Exception {
+        ConnectionServiceWrapper service = mock(ConnectionServiceWrapper.class);
+        doReturn(SIM_2_HANDLE.getComponentName()).when(service).getComponentName();
+        mCallsManager.addConnectionServiceRepositoryCache(SIM_2_HANDLE.getComponentName(),
+                SIM_2_HANDLE.getUserHandle(), service);
+
+        UserManager um = mContext.getSystemService(UserManager.class);
+        when(um.isQuietModeEnabled(eq(SIM_2_HANDLE.getUserHandle()))).thenReturn(true);
+        Call newCall = mCallsManager.processIncomingCallIntent(
+                SIM_2_HANDLE, new Bundle(), false);
+
+        verify(service, timeout(TEST_TIMEOUT)).createConnectionFailed(any());
+        assertFalse(newCall.isInECBM());
+        assertEquals(USER_MISSED_NOT_RUNNING, newCall.getMissedReason());
+    }
+
+    @SmallTest
+    @Test
+    public void testAcceptIncomingCallOnPAHInactiveAndECBMActive() throws Exception {
+        ConnectionServiceWrapper service = mock(ConnectionServiceWrapper.class);
+        doReturn(SIM_2_HANDLE.getComponentName()).when(service).getComponentName();
+        mCallsManager.addConnectionServiceRepositoryCache(SIM_2_HANDLE.getComponentName(),
+                SIM_2_HANDLE.getUserHandle(), service);
+
+        when(mEmergencyCallHelper.isLastOutgoingEmergencyCallPAH(eq(SIM_2_HANDLE)))
+                .thenReturn(true);
+        UserManager um = mContext.getSystemService(UserManager.class);
+        when(um.isQuietModeEnabled(eq(SIM_2_HANDLE.getUserHandle()))).thenReturn(true);
+        Call newCall = mCallsManager.processIncomingCallIntent(
+                SIM_2_HANDLE, new Bundle(), false);
+
+        assertTrue(newCall.isInECBM());
+        verify(service, timeout(TEST_TIMEOUT).times(0)).createConnectionFailed(any());
+    }
+
+    @SmallTest
+    @Test
+    public void testAcceptIncomingEmergencyCallOnPAHInactive() throws Exception {
+        ConnectionServiceWrapper service = mock(ConnectionServiceWrapper.class);
+        doReturn(SIM_2_HANDLE.getComponentName()).when(service).getComponentName();
+        mCallsManager.addConnectionServiceRepositoryCache(SIM_2_HANDLE.getComponentName(),
+                SIM_2_HANDLE.getUserHandle(), service);
+
+        Bundle extras = new Bundle();
+        extras.putParcelable(TelecomManager.EXTRA_INCOMING_CALL_ADDRESS, TEST_ADDRESS);
+        TelephonyManager tm = mContext.getSystemService(TelephonyManager.class);
+        UserManager um = mContext.getSystemService(UserManager.class);
+        when(um.isQuietModeEnabled(eq(SIM_2_HANDLE.getUserHandle()))).thenReturn(true);
+        when(tm.isEmergencyNumber(any(String.class))).thenReturn(true);
+        Call newCall = mCallsManager.processIncomingCallIntent(
+                SIM_2_HANDLE, extras, false);
+
+        assertFalse(newCall.isInECBM());
+        assertTrue(newCall.isEmergencyCall());
+        verify(service, timeout(TEST_TIMEOUT).times(0)).createConnectionFailed(any());
+    }
+
     public class LatchedOutcomeReceiver implements OutcomeReceiver<Boolean,
             CallException> {
         CountDownLatch mCountDownLatch;
@@ -3026,6 +3150,10 @@
                 mClockProxy,
                 mToastFactory);
         ongoingCall.setState(initialState, "just cuz");
+        if (targetPhoneAccount == SELF_MANAGED_HANDLE
+                || targetPhoneAccount == SELF_MANAGED_2_HANDLE) {
+            ongoingCall.setIsSelfManaged(true);
+        }
         return ongoingCall;
     }
 
diff --git a/tests/src/com/android/server/telecom/tests/InCallControllerTests.java b/tests/src/com/android/server/telecom/tests/InCallControllerTests.java
index 7d7a829..16fd630 100644
--- a/tests/src/com/android/server/telecom/tests/InCallControllerTests.java
+++ b/tests/src/com/android/server/telecom/tests/InCallControllerTests.java
@@ -595,6 +595,34 @@
     @MediumTest
     @Test
     public void
+    testBindToService_UserAssociatedWithCallIsInQuietMode_NonEmergCallECBM_BindsToPrimaryUser()
+            throws Exception {
+        when(mMockCallsManager.getCurrentUserHandle()).thenReturn(mUserHandle);
+        when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager);
+        when(mMockCall.isEmergencyCall()).thenReturn(false);
+        when(mMockCall.isInECBM()).thenReturn(true);
+        when(mMockCall.getUserHandleFromTargetPhoneAccount()).thenReturn(DUMMY_USER_HANDLE);
+        when(mMockContext.getSystemService(eq(UserManager.class)))
+                .thenReturn(mMockUserManager);
+        when(mMockUserManager.isQuietModeEnabled(any(UserHandle.class))).thenReturn(true);
+        setupMockPackageManager(true /* default */, true /* system */, false /* external calls */);
+        setupMockPackageManagerLocationPermission(SYS_PKG, false /* granted */);
+
+        mInCallController.bindToServices(mMockCall);
+
+        ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
+        verify(mMockContext, times(1)).bindServiceAsUser(
+                bindIntentCaptor.capture(),
+                any(ServiceConnection.class),
+                eq(serviceBindingFlags),
+                eq(mUserHandle));
+        Intent bindIntent = bindIntentCaptor.getValue();
+        assertEquals(InCallService.SERVICE_INTERFACE, bindIntent.getAction());
+    }
+
+    @MediumTest
+    @Test
+    public void
     testBindToService_UserAssociatedWithCallNotInQuietMode_EmergCallInCallUi_BindsToAssociatedUser()
         throws Exception {
         when(mMockCallsManager.getCurrentUserHandle()).thenReturn(mUserHandle);
diff --git a/tests/src/com/android/server/telecom/tests/RingerTest.java b/tests/src/com/android/server/telecom/tests/RingerTest.java
index abbfe34..a02415c 100644
--- a/tests/src/com/android/server/telecom/tests/RingerTest.java
+++ b/tests/src/com/android/server/telecom/tests/RingerTest.java
@@ -435,42 +435,65 @@
     }
 
     /**
-     * assert {@link Ringer#shouldRingForContact(Call, Context) } sets the Call object with suppress
-     * caller
-     *
-     * @throws Exception; should not throw exception.
+     * test shouldRingForContact will suppress the incoming call if matchesCallFilter returns
+     * false (meaning DND is ON and the caller cannot bypass the settings)
      */
     @Test
-    public void testShouldRingForContact_CallSuppressed() throws Exception {
+    public void testShouldRingForContact_CallSuppressed() {
         // WHEN
         when(mockCall1.wasDndCheckComputedForCall()).thenReturn(false);
         when(mockCall1.getHandle()).thenReturn(Uri.parse(""));
-
         when(mContext.getSystemService(NotificationManager.class)).thenReturn(
                 mockNotificationManager);
+        // suppress the call
         when(mockNotificationManager.matchesCallFilter(any(Bundle.class))).thenReturn(false);
 
-        // THEN
+        // run the method under test
         assertFalse(mRingerUnderTest.shouldRingForContact(mockCall1));
-        verify(mockCall1, atLeastOnce()).setCallIsSuppressedByDoNotDisturb(true);
+
+        // THEN
+        // verify we never set the call object and matchesCallFilter is called
+        verify(mockCall1, never()).setCallIsSuppressedByDoNotDisturb(true);
+        verify(mockNotificationManager, times(1))
+                .matchesCallFilter(any(Bundle.class));
     }
 
     /**
-     * assert {@link Ringer#shouldRingForContact(Call, Context) } sets the Call object with ring
-     * caller
-     *
-     * @throws Exception; should not throw exception.
+     * test shouldRingForContact will alert the user of an incoming call if matchesCallFilter
+     * returns true (meaning DND is NOT suppressing the caller)
      */
     @Test
-    public void testShouldRingForContact_CallShouldRing() throws Exception {
+    public void testShouldRingForContact_CallShouldRing() {
         // WHEN
         when(mockCall1.wasDndCheckComputedForCall()).thenReturn(false);
         when(mockCall1.getHandle()).thenReturn(Uri.parse(""));
+        // alert the user of the call
         when(mockNotificationManager.matchesCallFilter(any(Bundle.class))).thenReturn(true);
 
-        // THEN
+        // run the method under test
         assertTrue(mRingerUnderTest.shouldRingForContact(mockCall1));
-        verify(mockCall1, atLeastOnce()).setCallIsSuppressedByDoNotDisturb(false);
+
+        // THEN
+        // verify we never set the call object and matchesCallFilter is called
+        verify(mockCall1, never()).setCallIsSuppressedByDoNotDisturb(false);
+        verify(mockNotificationManager, times(1))
+                .matchesCallFilter(any(Bundle.class));
+    }
+
+    /**
+     * ensure Telecom does not re-query the NotificationManager if the call object already has
+     * the result.
+     */
+    @Test
+    public void testShouldRingForContact_matchesCallFilterIsAlreadyComputed() {
+        // WHEN
+        when(mockCall1.wasDndCheckComputedForCall()).thenReturn(true);
+        when(mockCall1.isCallSuppressedByDoNotDisturb()).thenReturn(true);
+
+        // THEN
+        assertFalse(mRingerUnderTest.shouldRingForContact(mockCall1));
+        verify(mockCall1, never()).setCallIsSuppressedByDoNotDisturb(false);
+        verify(mockNotificationManager, never()).matchesCallFilter(any(Bundle.class));
     }
 
     @Test