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) {
+ }
+ }
+}