Add call streaming related API.

Bug: 262412844
Test: build, cts test
Change-Id: Ib9013291ba5bac4f94fb2919fb2eccb8aa25acb8
diff --git a/telecomm/java/android/telecom/CallAudioState.java b/telecomm/java/android/telecom/CallAudioState.java
index fccdf76..c7cc1bd 100644
--- a/telecomm/java/android/telecom/CallAudioState.java
+++ b/telecomm/java/android/telecom/CallAudioState.java
@@ -27,7 +27,6 @@
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
@@ -58,6 +57,9 @@
     /** Direct the audio stream through the device's speakerphone. */
     public static final int ROUTE_SPEAKER       = 0x00000008;
 
+    /** Direct the audio stream through another device. */
+    public static final int ROUTE_STREAMING     = 0x00000010;
+
     /**
      * Direct the audio stream through the device's earpiece or wired headset if one is
      * connected.
@@ -70,7 +72,7 @@
      * @hide
      **/
     public static final int ROUTE_ALL = ROUTE_EARPIECE | ROUTE_BLUETOOTH | ROUTE_WIRED_HEADSET |
-            ROUTE_SPEAKER;
+            ROUTE_SPEAKER | ROUTE_STREAMING;
 
     private final boolean isMuted;
     private final int route;
@@ -189,7 +191,11 @@
      */
     @CallAudioRoute
     public int getSupportedRouteMask() {
-        return supportedRouteMask;
+        if (route == ROUTE_STREAMING) {
+            return ROUTE_STREAMING;
+        } else {
+            return supportedRouteMask;
+        }
     }
 
     /**
@@ -232,6 +238,9 @@
         if ((route & ROUTE_SPEAKER) == ROUTE_SPEAKER) {
             listAppend(buffer, "SPEAKER");
         }
+        if ((route & ROUTE_STREAMING) == ROUTE_STREAMING) {
+            listAppend(buffer, "STREAMING");
+        }
 
         return buffer.toString();
     }
diff --git a/telecomm/java/android/telecom/CallControl.java b/telecomm/java/android/telecom/CallControl.java
index 3bda6f4..867bcc7 100644
--- a/telecomm/java/android/telecom/CallControl.java
+++ b/telecomm/java/android/telecom/CallControl.java
@@ -191,6 +191,38 @@
     }
 
     /**
+     * Request start a call streaming session. On receiving valid request, telecom will bind to
+     * the {@link CallStreamingService} implemented by a general call streaming sender. So that the
+     * call streaming sender can perform streaming local device audio to another remote device and
+     * control the call during streaming.
+     *
+     * @param executor The {@link Executor} on which the {@link OutcomeReceiver} callback
+     *                 will be called on.
+     * @param callback that will be completed on the Telecom side that details success or failure
+     *                 of the requested operation.
+     *
+     *                 {@link OutcomeReceiver#onResult} will be called if Telecom has successfully
+     *                 rejected the incoming call.
+     *
+     *                 {@link OutcomeReceiver#onError} will be called if Telecom has failed to
+     *                 reject the incoming call.  A {@link CallException} will be passed that
+     *                 details why the operation failed.
+     */
+    public void startCallStreaming(@CallbackExecutor @NonNull Executor executor,
+            @NonNull OutcomeReceiver<Void, CallException> callback) {
+        if (mServerInterface != null) {
+            try {
+                mServerInterface.startCallStreaming(mCallId,
+                        new CallControlResultReceiver("startCallStreaming", executor, callback));
+            } catch (RemoteException e) {
+                throw e.rethrowAsRuntimeException();
+            }
+        } else {
+            throw new IllegalStateException(INTERFACE_ERROR_MSG);
+        }
+    }
+
+    /**
      * This method should be called after
      * {@link CallControl#disconnect(DisconnectCause, Executor, OutcomeReceiver)} or
      * {@link CallControl#rejectCall(Executor, OutcomeReceiver)}
diff --git a/telecomm/java/android/telecom/CallEventCallback.java b/telecomm/java/android/telecom/CallEventCallback.java
index a26291f..fd7e101 100644
--- a/telecomm/java/android/telecom/CallEventCallback.java
+++ b/telecomm/java/android/telecom/CallEventCallback.java
@@ -100,4 +100,22 @@
      * @param callAudioState that is currently being used
      */
     void onCallAudioStateChanged(@NonNull CallAudioState callAudioState);
+
+    /**
+     * Telecom is informing the client to set the call in streaming.
+     *
+     * @param wasCompleted The {@link Consumer} to be completed. If the client can stream the
+     *                     call on their end, {@link Consumer#accept(Object)} should be called with
+     *                     {@link Boolean#TRUE}. Otherwise, {@link Consumer#accept(Object)}
+     *                     should be called with {@link Boolean#FALSE}.
+     */
+    void onCallStreamingStarted(@NonNull Consumer<Boolean> wasCompleted);
+
+    /**
+     * Telecom is informing the client user requested call streaming but the stream can't be
+     * started.
+     *
+     * @param reason Code to indicate the reason of this failure
+     */
+    void onCallStreamingFailed(@CallStreamingService.StreamingFailedReason int reason);
 }
diff --git a/telecomm/java/android/telecom/CallStreamingService.java b/telecomm/java/android/telecom/CallStreamingService.java
new file mode 100644
index 0000000..affa6b6
--- /dev/null
+++ b/telecomm/java/android/telecom/CallStreamingService.java
@@ -0,0 +1,184 @@
+/*
+ * 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 android.telecom;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.SdkConstant;
+import android.app.Service;
+import android.content.Intent;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+
+import androidx.annotation.Nullable;
+
+import com.android.internal.telecom.ICallStreamingService;
+import com.android.internal.telecom.IStreamingCallAdapter;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * This service is implemented by an app that wishes to provide functionality for a general call
+ * streaming sender for voip calls.
+ *
+ * TODO: add doc of how to be the general streaming sender
+ *
+ */
+public abstract class CallStreamingService extends Service {
+    /**
+     * The {@link android.content.Intent} that must be declared as handled by the service.
+     */
+    @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION)
+    public static final String SERVICE_INTERFACE = "android.telecom.CallStreamingService";
+
+    private static final int MSG_SET_STREAMING_CALL_ADAPTER = 1;
+    private static final int MSG_CALL_STREAMING_STARTED = 2;
+    private static final int MSG_CALL_STREAMING_STOPPED = 3;
+    private static final int MSG_CALL_STREAMING_CHANGED_CHANGED = 4;
+
+    /** Default Handler used to consolidate binder method calls onto a single thread. */
+    private final Handler mHandler = new Handler(Looper.getMainLooper()) {
+        @Override
+        public void handleMessage(Message msg) {
+            if (mStreamingCallAdapter == null && msg.what != MSG_SET_STREAMING_CALL_ADAPTER) {
+                return;
+            }
+
+            switch (msg.what) {
+                case MSG_SET_STREAMING_CALL_ADAPTER:
+                    mStreamingCallAdapter = new StreamingCallAdapter(
+                            (IStreamingCallAdapter) msg.obj);
+                    break;
+                case MSG_CALL_STREAMING_STARTED:
+                    mCall = (StreamingCall) msg.obj;
+                    mCall.setAdapter(mStreamingCallAdapter);
+                    CallStreamingService.this.onCallStreamingStarted(mCall);
+                    break;
+                case MSG_CALL_STREAMING_STOPPED:
+                    mCall = null;
+                    mStreamingCallAdapter = null;
+                    CallStreamingService.this.onCallStreamingStopped();
+                    break;
+                case MSG_CALL_STREAMING_CHANGED_CHANGED:
+                    int state = (int) msg.obj;
+                    mCall.setStreamingState(state);
+                    CallStreamingService.this.onCallStreamingStateChanged(state);
+                    break;
+                default:
+                    break;
+            }
+        }
+    };
+
+    @Nullable
+    @Override
+    public IBinder onBind(@NonNull Intent intent) {
+        return new CallStreamingServiceBinder();
+    }
+
+    /** Manages the binder calls so that the implementor does not need to deal with it. */
+    private final class CallStreamingServiceBinder extends ICallStreamingService.Stub {
+        @Override
+        public void setStreamingCallAdapter(IStreamingCallAdapter streamingCallAdapter)
+                throws RemoteException {
+            mHandler.obtainMessage(MSG_SET_STREAMING_CALL_ADAPTER, mStreamingCallAdapter)
+                    .sendToTarget();
+        }
+
+        @Override
+        public void onCallStreamingStarted(StreamingCall call) throws RemoteException {
+            mHandler.obtainMessage(MSG_CALL_STREAMING_STARTED, call).sendToTarget();
+        }
+
+        @Override
+        public void onCallStreamingStopped() throws RemoteException {
+            mHandler.obtainMessage(MSG_CALL_STREAMING_STOPPED).sendToTarget();
+        }
+
+        @Override
+        public void onCallStreamingStateChanged(int state) throws RemoteException {
+            mHandler.obtainMessage(MSG_CALL_STREAMING_CHANGED_CHANGED, state).sendToTarget();
+        }
+    }
+
+    /**
+     * Call streaming request reject reason used with
+     * {@link CallEventCallback#onCallStreamingFailed(int)} to indicate that telecom is rejecting a
+     * call streaming request because there's an ongoing streaming call on this device.
+     */
+    public static final int STREAMING_FAILED_ALREADY_STREAMING = 1;
+
+    /**
+     * Call streaming request reject reason used with
+     * {@link CallEventCallback#onCallStreamingFailed(int)} to indicate that telecom is rejecting a
+     * call streaming request because telecom can't find existing general streaming sender on this
+     * device.
+     */
+    public static final int STREAMING_FAILED_NO_SENDER = 2;
+
+    /**
+     * Call streaming request reject reason used with
+     * {@link CallEventCallback#onCallStreamingFailed(int)} to indicate that telecom is rejecting a
+     * call streaming request because telecom can't bind to the general streaming sender app.
+     */
+    public static final int STREAMING_FAILED_SENDER_BINDING_ERROR = 3;
+
+    private StreamingCallAdapter mStreamingCallAdapter;
+    private StreamingCall mCall;
+
+    /**
+     * @hide
+     */
+    @IntDef(prefix = {"STREAMING_FAILED"},
+            value = {
+                    STREAMING_FAILED_ALREADY_STREAMING,
+                    STREAMING_FAILED_NO_SENDER,
+                    STREAMING_FAILED_SENDER_BINDING_ERROR
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface StreamingFailedReason {};
+
+    /**
+     * Called when a {@code StreamingCall} has been added to this call streaming session. The call
+     * streaming sender should start to intercept the device audio using audio records and audio
+     * tracks from Audio frameworks.
+     *
+     * @param call a newly added {@code StreamingCall}.
+     */
+    public void onCallStreamingStarted(@NonNull StreamingCall call) {
+    }
+
+    /**
+     * Called when a current {@code StreamingCall} has been removed from this call streaming
+     * session. The call streaming sender should notify the streaming receiver that the call is
+     * stopped streaming and stop the device audio interception.
+     */
+    public void onCallStreamingStopped() {
+    }
+
+    /**
+     * Called when the streaming state of current {@code StreamingCall} changed. General streaming
+     * sender usually get notified of the holding/unholding from the original owner voip app of the
+     * call.
+     */
+    public void onCallStreamingStateChanged(@StreamingCall.StreamingCallState int state) {
+    }
+}
diff --git a/telecomm/java/android/telecom/PhoneAccount.java b/telecomm/java/android/telecom/PhoneAccount.java
index 047ab3a..b8c056e 100644
--- a/telecomm/java/android/telecom/PhoneAccount.java
+++ b/telecomm/java/android/telecom/PhoneAccount.java
@@ -437,7 +437,15 @@
      */
     public static final int CAPABILITY_SUPPORTS_TRANSACTIONAL_OPERATIONS = 0x40000;
 
-    /* NEXT CAPABILITY: [0x80000, 0x100000, 0x200000] */
+    /**
+     * Flag indicating that this voip app {@link PhoneAccount} supports the call streaming session
+     * to stream call audio to another remote device via streaming app.
+     *
+     * @see #getCapabilities
+     */
+    public static final int CAPABILITY_SUPPORTS_CALL_STREAMING = 0x80000;
+
+    /* NEXT CAPABILITY: [0x100000, 0x200000, 0x400000] */
 
     /**
      * URI scheme for telephone number URIs.
diff --git a/telecomm/java/android/telecom/StreamingCall.aidl b/telecomm/java/android/telecom/StreamingCall.aidl
new file mode 100644
index 0000000..d286658
--- /dev/null
+++ b/telecomm/java/android/telecom/StreamingCall.aidl
@@ -0,0 +1,22 @@
+/*
+ * Copyright 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 android.telecom;
+
+/**
+ * {@hide}
+ */
+parcelable StreamingCall;
\ No newline at end of file
diff --git a/telecomm/java/android/telecom/StreamingCall.java b/telecomm/java/android/telecom/StreamingCall.java
new file mode 100644
index 0000000..985cccc
--- /dev/null
+++ b/telecomm/java/android/telecom/StreamingCall.java
@@ -0,0 +1,178 @@
+/*
+ * 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 android.telecom;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.content.ComponentName;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Represents a voip call requested to stream to another device that the general streaming sender
+ * app should present to the receiver.
+ */
+public final class StreamingCall implements Parcelable {
+    /**
+     * The state of a {@code StreamingCall} when newly created. General streaming sender should
+     * continuously stream call audio to the sender device as long as the {@code StreamingCall} is
+     * in this state.
+     */
+    public static final int STATE_STREAMING = 1;
+
+    /**
+     * The state of a {@code StreamingCall} when in a holding state.
+     */
+    public static final int STATE_HOLDING = 2;
+
+    /**
+     * The state of a {@code StreamingCall} when it's either disconnected or pulled back to the
+     * original device.
+     */
+    public static final int STATE_DISCONNECTED = 3;
+
+    private StreamingCall(@NonNull Parcel in) {
+        mComponentName = in.readParcelable(ComponentName.class.getClassLoader());
+        mDisplayName = in.readString16NoHelper();
+        mAddress = in.readParcelable(Uri.class.getClassLoader());
+        mExtras = in.readBundle();
+        mState = in.readInt();
+    }
+
+    @NonNull
+    public static final Creator<StreamingCall> CREATOR = new Creator<>() {
+        @Override
+        public StreamingCall createFromParcel(@NonNull Parcel in) {
+            return new StreamingCall(in);
+        }
+
+        @Override
+        public StreamingCall[] newArray(int size) {
+            return new StreamingCall[size];
+        }
+    };
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@androidx.annotation.NonNull Parcel dest, int flags) {
+        dest.writeParcelable(mComponentName, flags);
+        dest.writeString16NoHelper(mDisplayName);
+        dest.writeParcelable(mAddress, flags);
+        dest.writeBundle(mExtras);
+        dest.writeInt(mState);
+    }
+
+    /**
+     * @hide
+     */
+    @IntDef(prefix = { "STATE_" },
+            value = {
+                    STATE_STREAMING,
+                    STATE_HOLDING,
+                    STATE_DISCONNECTED
+            })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface StreamingCallState {}
+
+    private final ComponentName mComponentName;
+    private final String mDisplayName;
+    private final Uri mAddress;
+    private final Bundle mExtras;
+    @StreamingCallState
+    private int mState;
+    private StreamingCallAdapter mAdapter = null;
+
+    public StreamingCall(@NonNull ComponentName componentName, @NonNull String displayName,
+            @NonNull Uri address, @NonNull Bundle extras) {
+        mComponentName = componentName;
+        mDisplayName = displayName;
+        mAddress = address;
+        mExtras = extras;
+        mState = STATE_STREAMING;
+    }
+
+    /**
+     * @hide
+     */
+    public void setAdapter(StreamingCallAdapter adapter) {
+        mAdapter = adapter;
+    }
+
+    /**
+     * @return The {@link ComponentName} to identify the original voip app of this
+     * {@code StreamingCall}. General streaming sender app can use this to query necessary
+     * information (app icon etc.) in order to present notification of the streaming call on the
+     * receiver side.
+     */
+    @NonNull
+    public ComponentName getComponentName() {
+        return mComponentName;
+    }
+
+    /**
+     * @return The display name that the general streaming sender app can use this to present the
+     * {@code StreamingCall} to the receiver side.
+     */
+    @NonNull
+    public String getDisplayName() {
+        return mDisplayName;
+    }
+
+    /**
+     * @return The address (e.g., phone number) to which the {@code StreamingCall} is currently
+     * connected.
+     */
+    @NonNull
+    public Uri getAddress() {
+        return mAddress;
+    }
+
+    /**
+     * @return The state of this {@code StreamingCall}.
+     */
+    @StreamingCallState
+    public int getState() {
+        return mState;
+    }
+
+    /**
+     * @return The extra info the general streaming app need to stream the call from voip app or
+     * D2DI sdk.
+     */
+    @NonNull
+    public Bundle getExtras() {
+        return mExtras;
+    }
+
+    /**
+     * Sets the state of this {@code StreamingCall}. The general streaming sender app can use this
+     * to request holding, unholding and disconnecting this {@code StreamingCall}.
+     * @param state The current streaming state of the call.
+     */
+    public void setStreamingState(@StreamingCallState int state) {
+        mAdapter.setStreamingState(state);
+    }
+}
diff --git a/telecomm/java/android/telecom/StreamingCallAdapter.java b/telecomm/java/android/telecom/StreamingCallAdapter.java
new file mode 100644
index 0000000..bd8727d
--- /dev/null
+++ b/telecomm/java/android/telecom/StreamingCallAdapter.java
@@ -0,0 +1,54 @@
+/*
+ * 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 android.telecom;
+
+import android.os.RemoteException;
+
+import com.android.internal.telecom.IStreamingCallAdapter;
+
+/**
+ * Receives commands from {@link CallStreamingService} implementations which should be executed by
+ * Telecom. When Telecom binds to a {@link CallStreamingService}, an instance of this class is given
+ * to the general streaming app through which it can manipulate the streaming calls. Whe the general
+ * streaming app is notified of new ongoing streaming calls, it can execute
+ * {@link StreamingCall#setStreamingState(int)} for the ongoing streaming calls the user on the
+ * receiver side would like to hold, unhold and disconnect.
+ *
+ * @hide
+ */
+public final class StreamingCallAdapter {
+    private final IStreamingCallAdapter mAdapter;
+
+    /**
+     * {@hide}
+     */
+    public StreamingCallAdapter(IStreamingCallAdapter adapter) {
+        mAdapter = adapter;
+    }
+
+    /**
+     * Instruct telecom to change the state of the streaming call.
+     *
+     * @param state The streaming state to set
+     */
+    public void setStreamingState(@StreamingCall.StreamingCallState int state) {
+        try {
+            mAdapter.setStreamingState(state);
+        } catch (RemoteException e) {
+        }
+    }
+}