Merge "Platform Telecom Transactional APIs"

This commit is contained in:
TreeHugger Robot
2022-12-21 07:02:51 +00:00
committed by Android (Google) Code Review
16 changed files with 1610 additions and 1 deletions

View File

@@ -41038,6 +41038,32 @@ package android.telecom {
field public static final int RTT_MODE_VCO = 3; // 0x3
}
public final class CallAttributes implements android.os.Parcelable {
method public int describeContents();
method @NonNull public android.net.Uri getAddress();
method public int getCallCapabilities();
method public int getCallType();
method public int getDirection();
method @NonNull public CharSequence getDisplayName();
method @NonNull public android.telecom.PhoneAccountHandle getPhoneAccountHandle();
method public void writeToParcel(@Nullable android.os.Parcel, int);
field public static final int AUDIO_CALL = 1; // 0x1
field @NonNull public static final android.os.Parcelable.Creator<android.telecom.CallAttributes> CREATOR;
field public static final int DIRECTION_INCOMING = 1; // 0x1
field public static final int DIRECTION_OUTGOING = 2; // 0x2
field public static final int SUPPORTS_SET_INACTIVE = 2; // 0x2
field public static final int SUPPORTS_STREAM = 4; // 0x4
field public static final int SUPPORTS_TRANSFER = 8; // 0x8
field public static final int VIDEO_CALL = 2; // 0x2
}
public static final class CallAttributes.Builder {
ctor public CallAttributes.Builder(@NonNull android.telecom.PhoneAccountHandle, int, @NonNull CharSequence, @NonNull android.net.Uri);
method @NonNull public android.telecom.CallAttributes build();
method @NonNull public android.telecom.CallAttributes.Builder setCallCapabilities(int);
method @NonNull public android.telecom.CallAttributes.Builder setCallType(int);
}
public final class CallAudioState implements android.os.Parcelable {
ctor public CallAudioState(boolean, int, int);
method public static String audioRouteToString(int);
@@ -41056,6 +41082,15 @@ package android.telecom {
field public static final int ROUTE_WIRED_OR_EARPIECE = 5; // 0x5
}
public final class CallControl implements java.lang.AutoCloseable {
method public void close();
method public void disconnect(@NonNull android.telecom.DisconnectCause, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.telecom.CallException>);
method @NonNull public android.os.ParcelUuid getCallId();
method public void rejectCall(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.telecom.CallException>);
method public void setActive(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.telecom.CallException>);
method public void setInactive(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.telecom.CallException>);
}
public final class CallEndpoint implements android.os.Parcelable {
ctor public CallEndpoint(@NonNull CharSequence, int, @NonNull android.os.ParcelUuid);
method public int describeContents();
@@ -41086,6 +41121,30 @@ package android.telecom {
field public static final int ERROR_UNSPECIFIED = 4; // 0x4
}
public interface CallEventCallback {
method public void onAnswer(int, @NonNull java.util.function.Consumer<java.lang.Boolean>);
method public void onCallAudioStateChanged(@NonNull android.telecom.CallAudioState);
method public void onDisconnect(@NonNull java.util.function.Consumer<java.lang.Boolean>);
method public void onReject(@NonNull java.util.function.Consumer<java.lang.Boolean>);
method public void onSetActive(@NonNull java.util.function.Consumer<java.lang.Boolean>);
method public void onSetInactive(@NonNull java.util.function.Consumer<java.lang.Boolean>);
}
public final class CallException extends java.lang.RuntimeException implements android.os.Parcelable {
ctor public CallException(@Nullable String);
ctor public CallException(@Nullable String, int);
method public int describeContents();
method public int getCode();
method public void writeToParcel(@NonNull android.os.Parcel, int);
field public static final int CODE_CALL_CANNOT_BE_SET_TO_ACTIVE = 4; // 0x4
field public static final int CODE_CALL_IS_NOT_BEING_TRACKED = 3; // 0x3
field public static final int CODE_CALL_NOT_PERMITTED_AT_PRESENT_TIME = 5; // 0x5
field public static final int CODE_CANNOT_HOLD_CURRENT_ACTIVE_CALL = 2; // 0x2
field public static final int CODE_ERROR_UNKNOWN = 1; // 0x1
field public static final int CODE_OPERATION_TIMED_OUT = 6; // 0x6
field @NonNull public static final android.os.Parcelable.Creator<android.telecom.CallException> CREATOR;
}
public abstract class CallRedirectionService extends android.app.Service {
ctor public CallRedirectionService();
method public final void cancelCall();
@@ -41595,6 +41654,7 @@ package android.telecom {
field public static final int CAPABILITY_RTT = 4096; // 0x1000
field public static final int CAPABILITY_SELF_MANAGED = 2048; // 0x800
field public static final int CAPABILITY_SIM_SUBSCRIPTION = 4; // 0x4
field public static final int CAPABILITY_SUPPORTS_TRANSACTIONAL_OPERATIONS = 262144; // 0x40000
field public static final int CAPABILITY_SUPPORTS_VIDEO_CALLING = 1024; // 0x400
field public static final int CAPABILITY_SUPPORTS_VOICE_CALLING_INDICATIONS = 65536; // 0x10000
field public static final int CAPABILITY_VIDEO_CALLING = 8; // 0x8
@@ -41787,6 +41847,7 @@ package android.telecom {
method public void acceptHandover(android.net.Uri, int, android.telecom.PhoneAccountHandle);
method @Deprecated @RequiresPermission(anyOf={android.Manifest.permission.ANSWER_PHONE_CALLS, android.Manifest.permission.MODIFY_PHONE_STATE}) public void acceptRingingCall();
method @Deprecated @RequiresPermission(anyOf={android.Manifest.permission.ANSWER_PHONE_CALLS, android.Manifest.permission.MODIFY_PHONE_STATE}) public void acceptRingingCall(int);
method @RequiresPermission(android.Manifest.permission.MANAGE_OWN_CALLS) public void addCall(@NonNull android.telecom.CallAttributes, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.telecom.CallControl,android.telecom.CallException>, @NonNull android.telecom.CallEventCallback);
method public void addNewIncomingCall(android.telecom.PhoneAccountHandle, android.os.Bundle);
method public void addNewIncomingConference(@NonNull android.telecom.PhoneAccountHandle, @NonNull android.os.Bundle);
method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void cancelMissedCallsNotification();

View File

@@ -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 CallAttributes;

View File

@@ -0,0 +1,344 @@
/*
* 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.Nullable;
import android.net.Uri;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
import java.util.Objects;
/**
* CallAttributes represents a set of properties that define a new Call. Apps should build an
* instance of this class and use {@link TelecomManager#addCall} to start a new call with Telecom.
*
* <p>
* Apps should first register a {@link PhoneAccount} via {@link TelecomManager#registerPhoneAccount}
* and use the same {@link PhoneAccountHandle} registered with Telecom when creating an
* instance of CallAttributes.
*/
public final class CallAttributes implements Parcelable {
/** PhoneAccountHandle associated with the App managing calls **/
private final PhoneAccountHandle mPhoneAccountHandle;
/** Display name of the person on the other end of the call **/
private final CharSequence mDisplayName;
/** Address of the call. Note, this can be extended to a meeting link **/
private final Uri mAddress;
/** The direction (Outgoing/Incoming) of the new Call **/
@Direction private final int mDirection;
/** Information related to data being transmitted (voice, video, etc. ) **/
@CallType private final int mCallType;
/** Allows a package to opt into capabilities on the telecom side, on a per-call basis **/
@CallCapability private final int mCallCapabilities;
/** @hide **/
public static final String CALL_CAPABILITIES_KEY = "TelecomCapabilities";
private CallAttributes(@NonNull PhoneAccountHandle phoneAccountHandle,
@NonNull CharSequence displayName,
@NonNull Uri address,
int direction,
int callType,
int callCapabilities) {
mPhoneAccountHandle = phoneAccountHandle;
mDisplayName = displayName;
mAddress = address;
mDirection = direction;
mCallType = callType;
mCallCapabilities = callCapabilities;
}
/** @hide */
@IntDef(value = {DIRECTION_INCOMING, DIRECTION_OUTGOING})
public @interface Direction {
}
/**
* Indicates that the call is an incoming call.
*/
public static final int DIRECTION_INCOMING = 1;
/**
* Indicates that the call is an outgoing call.
*/
public static final int DIRECTION_OUTGOING = 2;
/** @hide */
@IntDef(value = {AUDIO_CALL, VIDEO_CALL})
public @interface CallType {
}
/**
* Used when answering or dialing a call to indicate that the call does not have a video
* component
*/
public static final int AUDIO_CALL = 1;
/**
* Indicates video transmission is supported
*/
public static final int VIDEO_CALL = 2;
/** @hide */
@IntDef(value = {SUPPORTS_SET_INACTIVE, SUPPORTS_STREAM, SUPPORTS_TRANSFER}, flag = true)
public @interface CallCapability {
}
/**
* The call being created can be set to inactive (traditionally referred to as hold). This
* means that once a new call goes active, if the active call needs to be held in order to
* place or receive an incoming call, the active call will be placed on hold. otherwise, the
* active call may be disconnected.
*/
public static final int SUPPORTS_SET_INACTIVE = 1 << 1;
/**
* The call can be streamed from a root device to another device to continue the call without
* completely transferring it.
*/
public static final int SUPPORTS_STREAM = 1 << 2;
/**
* The call can be completely transferred from one endpoint to another
*/
public static final int SUPPORTS_TRANSFER = 1 << 3;
/**
* Build an instance of {@link CallAttributes}. In order to build a valid instance, a
* {@link PhoneAccountHandle}, call {@link Direction}, display name, and {@link Uri} address
* are required.
*
* <p>
* Note: Pass in the same {@link PhoneAccountHandle} that was used to register a
* {@link PhoneAccount} with Telecom. see {@link TelecomManager#registerPhoneAccount}
*/
public static final class Builder {
// required and final fields
private final PhoneAccountHandle mPhoneAccountHandle;
@Direction private final int mDirection;
private final CharSequence mDisplayName;
private final Uri mAddress;
// optional fields
@CallType private int mCallType = CallAttributes.AUDIO_CALL;
@CallCapability private int mCallCapabilities = SUPPORTS_SET_INACTIVE;
/**
* Constructor for the CallAttributes.Builder class
*
* @param phoneAccountHandle that belongs to package registered with Telecom
* @param callDirection of the new call that will be added to Telecom
* @param displayName of the caller for incoming calls or initiating user for outgoing calls
* @param address of the caller for incoming calls or destination for outgoing calls
*/
public Builder(@NonNull PhoneAccountHandle phoneAccountHandle,
@Direction int callDirection, @NonNull CharSequence displayName,
@NonNull Uri address) {
if (!isInRange(DIRECTION_INCOMING, DIRECTION_OUTGOING, callDirection)) {
throw new IllegalArgumentException(TextUtils.formatSimple("CallDirection=[%d] is"
+ " invalid. CallDirections value should be within [%d, %d]",
callDirection, DIRECTION_INCOMING, DIRECTION_OUTGOING));
}
Objects.requireNonNull(phoneAccountHandle);
Objects.requireNonNull(displayName);
Objects.requireNonNull(address);
mPhoneAccountHandle = phoneAccountHandle;
mDirection = callDirection;
mDisplayName = displayName;
mAddress = address;
}
/**
* @param callType see {@link CallType} for valid arguments
* @return Builder
*/
@NonNull
public Builder setCallType(@CallType int callType) {
if (!isInRange(AUDIO_CALL, VIDEO_CALL, callType)) {
throw new IllegalArgumentException(TextUtils.formatSimple("CallType=[%d] is"
+ " invalid. CallTypes value should be within [%d, %d]",
callType, AUDIO_CALL, VIDEO_CALL));
}
mCallType = callType;
return this;
}
/**
* @param callCapabilities see {@link CallCapability} for valid arguments
* @return Builder
*/
@NonNull
public Builder setCallCapabilities(@CallCapability int callCapabilities) {
mCallCapabilities = callCapabilities;
return this;
}
/**
* Build an instance of {@link CallAttributes} based on the last values passed to the
* setters or default values.
*
* @return an instance of {@link CallAttributes}
*/
@NonNull
public CallAttributes build() {
return new CallAttributes(mPhoneAccountHandle, mDisplayName, mAddress, mDirection,
mCallType, mCallCapabilities);
}
/** @hide */
private boolean isInRange(int floor, int ceiling, int value) {
return value >= floor && value <= ceiling;
}
}
/**
* The {@link PhoneAccountHandle} that should be registered to Telecom to allow calls. The
* {@link PhoneAccountHandle} should be registered before creating a CallAttributes instance.
*
* @return the {@link PhoneAccountHandle} for this package that allows this call to be created
*/
@NonNull public PhoneAccountHandle getPhoneAccountHandle() {
return mPhoneAccountHandle;
}
/**
* @return display name of the incoming caller or the person being called for an outgoing call
*/
@NonNull public CharSequence getDisplayName() {
return mDisplayName;
}
/**
* @return address of the incoming caller
* or the address of the person being called for an outgoing call
*/
@NonNull public Uri getAddress() {
return mAddress;
}
/**
* @return the direction of the new call.
*/
public @Direction int getDirection() {
return mDirection;
}
/**
* @return Information related to data being transmitted (voice, video, etc. )
*/
public @CallType int getCallType() {
return mCallType;
}
/**
* @return The allowed capabilities of the new call
*/
public @CallCapability int getCallCapabilities() {
return mCallCapabilities;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(@Nullable Parcel dest, int flags) {
dest.writeParcelable(mPhoneAccountHandle, flags);
dest.writeCharSequence(mDisplayName);
dest.writeParcelable(mAddress, flags);
dest.writeInt(mDirection);
dest.writeInt(mCallType);
dest.writeInt(mCallCapabilities);
}
/**
* Responsible for creating CallAttribute objects for deserialized Parcels.
*/
public static final @android.annotation.NonNull
Parcelable.Creator<CallAttributes> CREATOR =
new Parcelable.Creator<>() {
@Override
public CallAttributes createFromParcel(Parcel source) {
return new CallAttributes(source.readParcelable(getClass().getClassLoader(),
android.telecom.PhoneAccountHandle.class),
source.readCharSequence(),
source.readParcelable(getClass().getClassLoader(),
android.net.Uri.class),
source.readInt(),
source.readInt(),
source.readInt());
}
@Override
public CallAttributes[] newArray(int size) {
return new CallAttributes[size];
}
};
/**
* {@inheritDoc}
*/
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("{ CallAttributes: [phoneAccountHandle: ")
.append(mPhoneAccountHandle) /* PhoneAccountHandle#toString handles PII */
.append("], [contactName: ")
.append(Log.pii(mDisplayName))
.append("], [address=")
.append(Log.pii(mAddress))
.append("], [direction=")
.append(mDirection)
.append("], [callType=")
.append(mCallType)
.append("], [mCallCapabilities=")
.append(mCallCapabilities)
.append("] }");
return sb.toString();
}
/**
* {@inheritDoc}
*/
@Override
public boolean equals(Object obj) {
if (obj == null || obj.getClass() != this.getClass()) {
return false;
}
CallAttributes that = (CallAttributes) obj;
return this.mDirection == that.mDirection
&& this.mCallType == that.mCallType
&& this.mCallCapabilities == that.mCallCapabilities
&& Objects.equals(this.mPhoneAccountHandle, that.mPhoneAccountHandle)
&& Objects.equals(this.mAddress, that.mAddress)
&& Objects.equals(this.mDisplayName, that.mDisplayName);
}
/**
* {@inheritDoc}
*/
@Override
public int hashCode() {
return Objects.hash(mPhoneAccountHandle, mAddress, mDisplayName,
mDirection, mCallType, mCallCapabilities);
}
}

View File

@@ -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 CallControl;

View File

@@ -0,0 +1,250 @@
/*
* 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 static android.telecom.CallException.TRANSACTION_EXCEPTION_KEY;
import android.annotation.CallbackExecutor;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.Binder;
import android.os.Bundle;
import android.os.OutcomeReceiver;
import android.os.ParcelUuid;
import android.os.RemoteException;
import android.os.ResultReceiver;
import com.android.internal.telecom.ClientTransactionalServiceRepository;
import com.android.internal.telecom.ICallControl;
import java.util.concurrent.Executor;
/**
* CallControl provides client side control of a call. Each Call will get an individual CallControl
* instance in which the client can alter the state of the associated call.
*
* <p>
* Each method is Transactional meaning that it can succeed or fail. If a transaction succeeds,
* the {@link OutcomeReceiver#onResult} will be called by Telecom. Otherwise, the
* {@link OutcomeReceiver#onError} is called and provides a {@link CallException} that details why
* the operation failed.
*/
public final class CallControl implements AutoCloseable {
private static final String TAG = CallControl.class.getSimpleName();
private static final String INTERFACE_ERROR_MSG = "Call Control is not available";
private final String mCallId;
private final ICallControl mServerInterface;
private final PhoneAccountHandle mPhoneAccountHandle;
private final ClientTransactionalServiceRepository mRepository;
/** @hide */
public CallControl(@NonNull String callId, @Nullable ICallControl serverInterface,
@NonNull ClientTransactionalServiceRepository repository,
@NonNull PhoneAccountHandle pah) {
mCallId = callId;
mServerInterface = serverInterface;
mRepository = repository;
mPhoneAccountHandle = pah;
}
/**
* @return the callId Telecom assigned to this CallControl object which should be attached to
* an individual call.
*/
@NonNull
public ParcelUuid getCallId() {
return ParcelUuid.fromString(mCallId);
}
/**
* Request Telecom set the call state to active.
*
* @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
* switched the call state to active
*
* {@link OutcomeReceiver#onError} will be called if Telecom has failed to set
* the call state to active. A {@link CallException} will be passed
* that details why the operation failed.
*/
public void setActive(@CallbackExecutor @NonNull Executor executor,
@NonNull OutcomeReceiver<Void, CallException> callback) {
if (mServerInterface != null) {
try {
mServerInterface.setActive(mCallId,
new CallControlResultReceiver("setActive", executor, callback));
} catch (RemoteException e) {
throw e.rethrowAsRuntimeException();
}
} else {
throw new IllegalStateException(INTERFACE_ERROR_MSG);
}
}
/**
* Request Telecom set the call state to inactive. This the same as hold for two call endpoints
* but can be extended to setting a meeting to inactive.
*
* @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
* switched the call state to inactive
*
* {@link OutcomeReceiver#onError} will be called if Telecom has failed to set
* the call state to inactive. A {@link CallException} will be passed
* that details why the operation failed.
*/
public void setInactive(@CallbackExecutor @NonNull Executor executor,
@NonNull OutcomeReceiver<Void, CallException> callback) {
if (mServerInterface != null) {
try {
mServerInterface.setInactive(mCallId,
new CallControlResultReceiver("setInactive", executor, callback));
} catch (RemoteException e) {
throw e.rethrowAsRuntimeException();
}
} else {
throw new IllegalStateException(INTERFACE_ERROR_MSG);
}
}
/**
* Request Telecom set the call state to disconnect.
*
* @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
* disconnected the call.
*
* {@link OutcomeReceiver#onError} will be called if Telecom has failed to
* disconnect the call. A {@link CallException} will be passed
* that details why the operation failed.
*/
public void disconnect(@NonNull DisconnectCause disconnectCause,
@CallbackExecutor @NonNull Executor executor,
@NonNull OutcomeReceiver<Void, CallException> callback) {
if (mServerInterface != null) {
try {
mServerInterface.disconnect(mCallId, disconnectCause,
new CallControlResultReceiver("disconnect", executor, callback));
} catch (RemoteException e) {
throw e.rethrowAsRuntimeException();
}
} else {
throw new IllegalStateException(INTERFACE_ERROR_MSG);
}
}
/**
* Request Telecom reject the incoming call.
*
* @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 rejectCall(@CallbackExecutor @NonNull Executor executor,
@NonNull OutcomeReceiver<Void, CallException> callback) {
if (mServerInterface != null) {
try {
mServerInterface.rejectCall(mCallId,
new CallControlResultReceiver("rejectCall", 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)}
* to destroy all references of this object and avoid memory leaks.
*/
@Override
public void close() {
mRepository.removeCallFromServiceWrapper(mPhoneAccountHandle, mCallId);
}
/**
* Since {@link OutcomeReceiver}s cannot be passed via AIDL, a ResultReceiver (which can) must
* wrap the Clients {@link OutcomeReceiver} passed in and await for the Telecom Server side
* response in {@link ResultReceiver#onReceiveResult(int, Bundle)}.
* @hide */
private class CallControlResultReceiver extends ResultReceiver {
private final String mCallingMethod;
private final Executor mExecutor;
private final OutcomeReceiver<Void, CallException> mClientCallback;
CallControlResultReceiver(String method, Executor executor,
OutcomeReceiver<Void, CallException> clientCallback) {
super(null);
mCallingMethod = method;
mExecutor = executor;
mClientCallback = clientCallback;
}
@Override
protected void onReceiveResult(int resultCode, Bundle resultData) {
Log.d(CallControl.TAG, "%s: oRR: resultCode=[%s]", mCallingMethod, resultCode);
super.onReceiveResult(resultCode, resultData);
final long identity = Binder.clearCallingIdentity();
try {
if (resultCode == TelecomManager.TELECOM_TRANSACTION_SUCCESS) {
mExecutor.execute(() -> mClientCallback.onResult(null));
} else {
mExecutor.execute(() ->
mClientCallback.onError(getTransactionException(resultData)));
}
} finally {
Binder.restoreCallingIdentity(identity);
}
}
}
/** @hide */
private CallException getTransactionException(Bundle resultData) {
String message = "unknown error";
if (resultData != null && resultData.containsKey(TRANSACTION_EXCEPTION_KEY)) {
return resultData.getParcelable(TRANSACTION_EXCEPTION_KEY,
CallException.class);
}
return new CallException(message, CallException.CODE_ERROR_UNKNOWN);
}
}

View File

@@ -0,0 +1,103 @@
/*
* 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.NonNull;
import java.util.function.Consumer;
/**
* CallEventCallback relays updates to a call from the Telecom framework.
* This can include operations which the app must implement on a Call due to the presence of other
* calls on the device, requests relayed from a Bluetooth device, or from another calling surface.
*
* <p>
* CallEventCallbacks with {@link Consumer}s are transactional, meaning that a client must
* complete the {@link Consumer} via {@link Consumer#accept(Object)} in order to complete the
* CallEventCallback. If a CallEventCallback can be completed, the
* {@link Consumer#accept(Object)} should be called with {@link Boolean#TRUE}. Otherwise,
* {@link Consumer#accept(Object)} should be called with {@link Boolean#FALSE} to represent the
* CallEventCallback cannot be completed on the client side.
*
* <p>
* Note: Each CallEventCallback has a timeout of 5000 milliseconds. Failing to complete the
* {@link Consumer} before the timeout will result in a failed transaction.
*/
public interface CallEventCallback {
/**
* Telecom is informing the client to set the call active
*
* @param wasCompleted The {@link Consumer} to be completed. If the client can set the call
* active on their end, the {@link Consumer#accept(Object)} should be
* called with {@link Boolean#TRUE}. Otherwise,
* {@link Consumer#accept(Object)} should be called with
* {@link Boolean#FALSE}.
*/
void onSetActive(@NonNull Consumer<Boolean> wasCompleted);
/**
* Telecom is informing the client to set the call inactive. This is the same as holding a call
* for two endpoints but can be extended to setting a meeting inactive.
*
* @param wasCompleted The {@link Consumer} to be completed. If the client can set the call
* inactive on their end, the {@link Consumer#accept(Object)} should be
* called with {@link Boolean#TRUE}. Otherwise,
* {@link Consumer#accept(Object)} should be called with
* {@link Boolean#FALSE}.
*/
void onSetInactive(@NonNull Consumer<Boolean> wasCompleted);
/**
* Telecom is informing the client to answer an incoming call and set it to active.
*
* @param videoState see {@link android.telecom.CallAttributes.CallType} for valid states
* @param wasCompleted The {@link Consumer} to be completed. If the client can answer 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 onAnswer(@android.telecom.CallAttributes.CallType int videoState,
@NonNull Consumer<Boolean> wasCompleted);
/**
* Telecom is informing the client to reject the incoming call
*
* @param wasCompleted The {@link Consumer} to be completed. If the client can reject the
* incoming call, {@link Consumer#accept(Object)} should be called with
* {@link Boolean#TRUE}. Otherwise, {@link Consumer#accept(Object)}
* should be called with {@link Boolean#FALSE}.
*/
void onReject(@NonNull Consumer<Boolean> wasCompleted);
/**
* Telecom is informing the client to disconnect the call
*
* @param wasCompleted The {@link Consumer} to be completed. If the client can disconnect 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 onDisconnect(@NonNull Consumer<Boolean> wasCompleted);
/**
* update the client on the new {@link CallAudioState}
*
* @param callAudioState that is currently being used
*/
void onCallAudioStateChanged(@NonNull CallAudioState callAudioState);
}

View File

@@ -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 CallException;

View File

@@ -0,0 +1,160 @@
/*
* 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.Nullable;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* This class defines exceptions that can be thrown when using Telecom APIs with
* {@link android.os.OutcomeReceiver}s. Most of these exceptions are thrown when changing a call
* state with {@link CallControl}s or {@link CallEventCallback}s.
*/
public final class CallException extends RuntimeException implements Parcelable {
/** @hide **/
public static final String TRANSACTION_EXCEPTION_KEY = "TelecomTransactionalExceptionKey";
/**
* The operation has failed due to an unknown or unspecified error.
*/
public static final int CODE_ERROR_UNKNOWN = 1;
/**
* The operation has failed due to Telecom failing to hold the current active call for the
* call attempting to become the new active call. The client should end the current active call
* and re-try the failed operation.
*/
public static final int CODE_CANNOT_HOLD_CURRENT_ACTIVE_CALL = 2;
/**
* The operation has failed because Telecom has already removed the call from the server side
* and destroyed all the objects associated with it. The client should re-add the call.
*/
public static final int CODE_CALL_IS_NOT_BEING_TRACKED = 3;
/**
* The operation has failed because Telecom cannot set the requested call as the current active
* call. The client should end the current active call and re-try the operation.
*/
public static final int CODE_CALL_CANNOT_BE_SET_TO_ACTIVE = 4;
/**
* The operation has failed because there is either no PhoneAccount registered with Telecom
* for the given operation, or the limit of calls has been reached. The client should end the
* current active call and re-try the failed operation.
*/
public static final int CODE_CALL_NOT_PERMITTED_AT_PRESENT_TIME = 5;
/**
* The operation has failed because the operation failed to complete before the timeout
*/
public static final int CODE_OPERATION_TIMED_OUT = 6;
private int mCode = CODE_ERROR_UNKNOWN;
private final String mMessage;
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
dest.writeString8(mMessage);
dest.writeInt(mCode);
}
/**
* Responsible for creating CallAttribute objects for deserialized Parcels.
*/
public static final @android.annotation.NonNull
Parcelable.Creator<CallException> CREATOR = new Parcelable.Creator<>() {
@Override
public CallException createFromParcel(Parcel source) {
return new CallException(source.readString8(), source.readInt());
}
@Override
public CallException[] newArray(int size) {
return new CallException[size];
}
};
/** @hide */
@Retention(RetentionPolicy.SOURCE)
@IntDef(prefix = "CODE_ERROR_", value = {
CODE_ERROR_UNKNOWN,
CODE_CANNOT_HOLD_CURRENT_ACTIVE_CALL,
CODE_CALL_IS_NOT_BEING_TRACKED,
CODE_CALL_CANNOT_BE_SET_TO_ACTIVE,
CODE_CALL_NOT_PERMITTED_AT_PRESENT_TIME,
CODE_OPERATION_TIMED_OUT
})
public @interface CallErrorCode {
}
/**
* Constructor for a new CallException when only message can be specified.
* {@code CODE_ERROR_UNKNOWN} will be default code returned when calling {@code getCode}
*
* @param message related to why the exception was created
*/
public CallException(@Nullable String message) {
super(getMessage(message, CODE_ERROR_UNKNOWN));
mMessage = message;
}
/**
* Constructor for a new CallException that has a defined error code in this class
*
* @param message related to why the exception was created
* @param code defined above that caused this exception to be created
*/
public CallException(@Nullable String message, @CallErrorCode int code) {
super(getMessage(message, code));
mCode = code;
mMessage = message;
}
/**
* @return one of the error codes defined in this class that was passed into the constructor
*/
public @CallErrorCode int getCode() {
return mCode;
}
private static String getMessage(String message, int code) {
StringBuilder builder;
if (!TextUtils.isEmpty(message)) {
builder = new StringBuilder(message);
builder.append(" (code: ");
builder.append(code);
builder.append(")");
return builder.toString();
} else {
return "code: " + code;
}
}
}

View File

@@ -418,7 +418,26 @@ public final class PhoneAccount implements Parcelable {
*/
public static final int CAPABILITY_VOICE_CALLING_AVAILABLE = 0x20000;
/* NEXT CAPABILITY: 0x40000 */
/**
* Flag indicating that this {@link PhoneAccount} supports the use TelecomManager APIs that
* utilize {@link android.os.OutcomeReceiver}s or {@link java.util.function.Consumer}s.
* Be aware, if this capability is set, {@link #CAPABILITY_SELF_MANAGED} will be amended by
* Telecom when this {@link PhoneAccount} is registered via
* {@link TelecomManager#registerPhoneAccount(PhoneAccount)}.
*
* <p>
* {@link android.os.OutcomeReceiver}s and {@link java.util.function.Consumer}s represent
* transactional operations because the operation can succeed or fail. An app wishing to use
* transactional operations should define behavior for a successful and failed TelecomManager
* API call.
*
* @see #CAPABILITY_SELF_MANAGED
* @see #getCapabilities
*/
public static final int CAPABILITY_SUPPORTS_TRANSACTIONAL_OPERATIONS = 0x40000;
/* NEXT CAPABILITY: [0x80000, 0x100000, 0x200000] */
/**
* URI scheme for telephone number URIs.

View File

@@ -18,6 +18,7 @@ import static android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE;
import static android.content.Intent.LOCAL_FLAG_FROM_SYSTEM;
import android.Manifest;
import android.annotation.CallbackExecutor;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -38,6 +39,7 @@ import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
import android.os.OutcomeReceiver;
import android.os.Process;
import android.os.RemoteException;
import android.os.ServiceManager;
@@ -49,6 +51,8 @@ import android.text.TextUtils;
import android.util.Log;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.telecom.ClientTransactionalServiceRepository;
import com.android.internal.telecom.ClientTransactionalServiceWrapper;
import com.android.internal.telecom.ITelecomService;
import java.lang.annotation.Retention;
@@ -1056,6 +1060,14 @@ public class TelecomManager {
private final ITelecomService mTelecomServiceOverride;
/** @hide **/
private final ClientTransactionalServiceRepository mTransactionalServiceRepository =
new ClientTransactionalServiceRepository();
/** @hide **/
public static final int TELECOM_TRANSACTION_SUCCESS = 0;
/** @hide **/
public static final String TRANSACTION_CALL_ID_KEY = "TelecomCallId";
/**
* @hide
*/
@@ -2639,6 +2651,92 @@ public class TelecomManager {
}
}
/**
* Adds a new call with the specified {@link CallAttributes} to the telecom service. This method
* can be used to add both incoming and outgoing calls.
*
* <p>
* The difference between this API call and {@link TelecomManager#placeCall(Uri, Bundle)} or
* {@link TelecomManager#addNewIncomingCall(PhoneAccountHandle, Bundle)} is that this API
* will asynchronously provide an update on whether the new call was added successfully via
* an {@link OutcomeReceiver}. Additionally, callbacks will run on the executor thread that was
* passed in.
*
* <p>
* Note: Only packages that register with
* {@link PhoneAccount#CAPABILITY_SUPPORTS_TRANSACTIONAL_OPERATIONS}
* can utilize this API. {@link PhoneAccount}s that set the capabilities
* {@link PhoneAccount#CAPABILITY_SIM_SUBSCRIPTION},
* {@link PhoneAccount#CAPABILITY_CALL_PROVIDER},
* {@link PhoneAccount#CAPABILITY_CONNECTION_MANAGER}
* are not supported and will cause an exception to be thrown.
*
* <p>
* Usage example:
* <pre>
*
* // An app should first define their own construct of a Call that overrides all the
* // {@link CallEventCallback}s
* private class MyVoipCall implements CallEventCallback {
* // override all the {@link CallEventCallback}s
* }
*
* PhoneAccountHandle handle = new PhoneAccountHandle(
* new ComponentName("com.example.voip.app",
* "com.example.voip.app.NewCallActivity"), "123");
*
* CallAttributes callAttributes = new CallAttributes.Builder(handle,
* CallAttributes.DIRECTION_OUTGOING,
* "John Smith", Uri.fromParts("tel", "123", null))
* .build();
*
* telecomManager.addCall(callAttributes, Runnable::run, new OutcomeReceiver() {
* public void onResult(CallControl callControl) {
* // The call has been added successfully
* }
* }, new MyVoipCall());
* </pre>
*
* @param callAttributes attributes of the new call (incoming or outgoing, address, etc. )
* @param executor thread to run background CallEventCallback updates on
* @param pendingControl OutcomeReceiver that receives the result of addCall transaction
* @param callEventCallback object that overrides CallEventCallback
*/
@RequiresPermission(android.Manifest.permission.MANAGE_OWN_CALLS)
@SuppressLint("SamShouldBeLast")
public void addCall(@NonNull CallAttributes callAttributes,
@NonNull @CallbackExecutor Executor executor,
@NonNull OutcomeReceiver<CallControl, CallException> pendingControl,
@NonNull CallEventCallback callEventCallback) {
Objects.requireNonNull(callAttributes);
Objects.requireNonNull(executor);
Objects.requireNonNull(pendingControl);
Objects.requireNonNull(callEventCallback);
ITelecomService service = getTelecomService();
if (service != null) {
try {
// create or add the new call to a service wrapper w/ the same phoneAccountHandle
ClientTransactionalServiceWrapper transactionalServiceWrapper =
mTransactionalServiceRepository.addNewCallForTransactionalServiceWrapper(
callAttributes.getPhoneAccountHandle());
// couple all the args passed by the client
String newCallId = transactionalServiceWrapper.trackCall(callAttributes, executor,
pendingControl, callEventCallback);
// send args to server to process new call
service.addCall(callAttributes, transactionalServiceWrapper.getCallEventCallback(),
newCallId, mContext.getOpPackageName());
} catch (RemoteException e) {
Log.e(TAG, "RemoteException addCall: " + e);
e.rethrowFromSystemServer();
}
} else {
throw new IllegalStateException("Telecom service is not present");
}
}
/**
* Handles {@link Intent#ACTION_CALL} intents trampolined from UserCallActivity.
* @param intent The {@link Intent#ACTION_CALL} intent to handle.

View File

@@ -0,0 +1,90 @@
/*
* 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 com.android.internal.telecom;
import android.telecom.PhoneAccountHandle;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* @hide
*/
public class ClientTransactionalServiceRepository {
private static final Map<PhoneAccountHandle, ClientTransactionalServiceWrapper> LOOKUP_TABLE =
new ConcurrentHashMap<>();
/**
* creates a new {@link ClientTransactionalServiceWrapper} if this is the first call being
* tracked for a particular package Or adds a new call for an existing
* {@link ClientTransactionalServiceWrapper}
*
* @param phoneAccountHandle for a particular package requesting to create a call
* @return the {@link ClientTransactionalServiceWrapper} that is tied tot the PhoneAccountHandle
*/
public ClientTransactionalServiceWrapper addNewCallForTransactionalServiceWrapper(
PhoneAccountHandle phoneAccountHandle) {
ClientTransactionalServiceWrapper service = null;
if (!hasExistingServiceWrapper(phoneAccountHandle)) {
service = new ClientTransactionalServiceWrapper(phoneAccountHandle, this);
} else {
service = getTransactionalServiceWrapper(phoneAccountHandle);
}
LOOKUP_TABLE.put(phoneAccountHandle, service);
return service;
}
private ClientTransactionalServiceWrapper getTransactionalServiceWrapper(
PhoneAccountHandle pah) {
return LOOKUP_TABLE.get(pah);
}
private boolean hasExistingServiceWrapper(PhoneAccountHandle pah) {
return LOOKUP_TABLE.containsKey(pah);
}
/**
* @param pah that is tied to a particular package with potential tracked calls
* @return if the {@link ClientTransactionalServiceWrapper} was successfully removed
*/
public boolean removeServiceWrapper(PhoneAccountHandle pah) {
if (!hasExistingServiceWrapper(pah)) {
return false;
}
LOOKUP_TABLE.remove(pah);
return true;
}
/**
* @param pah that is tied to a particular package with potential tracked calls
* @param callId of the TransactionalCall that you want to remove
* @return if the call was successfully removed from the service wrapper
*/
public boolean removeCallFromServiceWrapper(PhoneAccountHandle pah, String callId) {
if (!hasExistingServiceWrapper(pah)) {
return false;
}
ClientTransactionalServiceWrapper service = LOOKUP_TABLE.get(pah);
service.untrackCall(callId);
return true;
}
}

View File

@@ -0,0 +1,263 @@
/*
* 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 com.android.internal.telecom;
import static android.telecom.TelecomManager.TELECOM_TRANSACTION_SUCCESS;
import android.os.Binder;
import android.os.OutcomeReceiver;
import android.os.ResultReceiver;
import android.telecom.CallAttributes;
import android.telecom.CallAudioState;
import android.telecom.CallControl;
import android.telecom.CallEventCallback;
import android.telecom.CallException;
import android.telecom.PhoneAccountHandle;
import android.text.TextUtils;
import android.util.Log;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.function.Consumer;
/**
* wraps {@link CallEventCallback} and {@link CallControl} on a
* per-{@link android.telecom.PhoneAccountHandle} basis to track ongoing calls.
*
* @hide
*/
public class ClientTransactionalServiceWrapper {
private static final String TAG = ClientTransactionalServiceWrapper.class.getSimpleName();
private final PhoneAccountHandle mPhoneAccountHandle;
private final ClientTransactionalServiceRepository mRepository;
private final ConcurrentHashMap<String, TransactionalCall> mCallIdToTransactionalCall =
new ConcurrentHashMap<>();
private static final String EXECUTOR_FAIL_MSG =
"Telecom hit an exception while handling a CallEventCallback on an executor: ";
public ClientTransactionalServiceWrapper(PhoneAccountHandle handle,
ClientTransactionalServiceRepository repo) {
mPhoneAccountHandle = handle;
mRepository = repo;
}
/**
* remove the given call from the class HashMap
*
* @param callId that is tied to TransactionalCall object
*/
public void untrackCall(String callId) {
Log.i(TAG, TextUtils.formatSimple("removeCall: with id=[%s]", callId));
if (mCallIdToTransactionalCall.containsKey(callId)) {
// remove the call from the hashmap
TransactionalCall call = mCallIdToTransactionalCall.remove(callId);
// null out interface to avoid memory leaks
CallControl control = call.getCallControl();
if (control != null) {
call.setCallControl(null);
}
}
// possibly cleanup service wrapper if there are no more calls
if (mCallIdToTransactionalCall.size() == 0) {
mRepository.removeServiceWrapper(mPhoneAccountHandle);
}
}
/**
* start tracking a newly created call for a particular package
*
* @param callAttributes of the new call
* @param executor to run callbacks on
* @param pendingControl that allows telecom to call into the client
* @param callback that overrides the CallEventCallback
* @return the callId of the newly created call
*/
public String trackCall(CallAttributes callAttributes, Executor executor,
OutcomeReceiver<CallControl, CallException> pendingControl,
CallEventCallback callback) {
// generate a new id for this new call
String newCallId = UUID.randomUUID().toString();
// couple the objects passed from the client side
mCallIdToTransactionalCall.put(newCallId, new TransactionalCall(newCallId, callAttributes,
executor, pendingControl, callback));
return newCallId;
}
public ICallEventCallback getCallEventCallback() {
return mCallEventCallback;
}
/**
* Consumers that is to be completed by the client and the result relayed back to telecom server
* side via a {@link ResultReceiver}. see com.android.server.telecom.TransactionalServiceWrapper
* for how the response is handled.
*/
private class ReceiverWrapper implements Consumer<Boolean> {
private final ResultReceiver mRepeaterReceiver;
ReceiverWrapper(ResultReceiver resultReceiver) {
mRepeaterReceiver = resultReceiver;
}
@Override
public void accept(Boolean clientCompletedCallbackSuccessfully) {
if (clientCompletedCallbackSuccessfully) {
mRepeaterReceiver.send(TELECOM_TRANSACTION_SUCCESS, null);
} else {
mRepeaterReceiver.send(CallException.CODE_ERROR_UNKNOWN, null);
}
}
@Override
public Consumer<Boolean> andThen(Consumer<? super Boolean> after) {
return Consumer.super.andThen(after);
}
}
private final ICallEventCallback mCallEventCallback = new ICallEventCallback.Stub() {
private static final String ON_SET_ACTIVE = "onSetActive";
private static final String ON_SET_INACTIVE = "onSetInactive";
private static final String ON_ANSWER = "onAnswer";
private static final String ON_REJECT = "onReject";
private static final String ON_DISCONNECT = "onDisconnect";
private void handleCallEventCallback(String action, String callId, int code,
ResultReceiver ackResultReceiver) {
Log.i(TAG, TextUtils.formatSimple("hCEC: id=[%s], action=[%s]", callId, action));
// lookup the callEventCallback associated with the particular call
TransactionalCall call = mCallIdToTransactionalCall.get(callId);
if (call != null) {
// Get the CallEventCallback interface
CallEventCallback callback = call.getCallEventCallback();
// Get Receiver to wait on client ack
ReceiverWrapper outcomeReceiverWrapper = new ReceiverWrapper(ackResultReceiver);
// wait for the client to complete the CallEventCallback
final long identity = Binder.clearCallingIdentity();
try {
call.getExecutor().execute(() -> {
switch (action) {
case ON_SET_ACTIVE:
callback.onSetActive(outcomeReceiverWrapper);
break;
case ON_SET_INACTIVE:
callback.onSetInactive(outcomeReceiverWrapper);
break;
case ON_REJECT:
callback.onReject(outcomeReceiverWrapper);
untrackCall(callId);
break;
case ON_DISCONNECT:
callback.onDisconnect(outcomeReceiverWrapper);
untrackCall(callId);
break;
case ON_ANSWER:
callback.onAnswer(code, outcomeReceiverWrapper);
break;
}
});
} catch (Exception e) {
Log.e(TAG, EXECUTOR_FAIL_MSG + e);
} finally {
Binder.restoreCallingIdentity(identity);
}
}
}
@Override
public void onAddCallControl(String callId, int resultCode, ICallControl callControl,
CallException transactionalException) {
Log.i(TAG, TextUtils.formatSimple("oACC: id=[%s], code=[%d]", callId, resultCode));
TransactionalCall call = mCallIdToTransactionalCall.get(callId);
if (call != null) {
OutcomeReceiver<CallControl, CallException> pendingControl =
call.getPendingControl();
if (resultCode == TELECOM_TRANSACTION_SUCCESS) {
// create the interface object that the client will interact with
CallControl control = new CallControl(callId, callControl, mRepository,
mPhoneAccountHandle);
// give the client the object via the OR that was passed into addCall
pendingControl.onResult(control);
// store for later reference
call.setCallControl(control);
} else {
pendingControl.onError(transactionalException);
mCallIdToTransactionalCall.remove(callId);
}
} else {
untrackCall(callId);
Log.e(TAG, "oACC: TransactionalCall object not found for call w/ id=" + callId);
}
}
@Override
public void onSetActive(String callId, ResultReceiver resultReceiver) {
handleCallEventCallback(ON_SET_ACTIVE, callId, 0, resultReceiver);
}
@Override
public void onSetInactive(String callId, ResultReceiver resultReceiver) {
handleCallEventCallback(ON_SET_INACTIVE, callId, 0, resultReceiver);
}
@Override
public void onAnswer(String callId, int videoState, ResultReceiver resultReceiver) {
handleCallEventCallback(ON_ANSWER, callId, videoState, resultReceiver);
}
@Override
public void onReject(String callId, ResultReceiver resultReceiver) {
handleCallEventCallback(ON_REJECT, callId, 0, resultReceiver);
}
@Override
public void onDisconnect(String callId, ResultReceiver resultReceiver) {
handleCallEventCallback(ON_DISCONNECT, callId, 0, resultReceiver);
}
@Override
public void onCallAudioStateChanged(String callId, CallAudioState callAudioState) {
Log.i(TAG, TextUtils.formatSimple("onCallAudioStateChanged: callId=[%s]", callId));
// lookup the callEventCallback associated with the particular call
TransactionalCall call = mCallIdToTransactionalCall.get(callId);
if (call != null) {
CallEventCallback callback = call.getCallEventCallback();
Executor executor = call.getExecutor();
executor.execute(() -> {
callback.onCallAudioStateChanged(callAudioState);
});
}
}
@Override
public void removeCallFromTransactionalServiceWrapper(String callId) {
untrackCall(callId);
}
};
}

View File

@@ -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 com.android.internal.telecom;
import android.telecom.CallControl;
import android.telecom.DisconnectCause;
import android.os.ResultReceiver;
/**
* {@hide}
*/
oneway interface ICallControl {
void setActive(String callId, in ResultReceiver callback);
void setInactive(String callId, in ResultReceiver callback);
void disconnect(String callId, in DisconnectCause disconnectCause, in ResultReceiver callback);
void rejectCall(String callId, in ResultReceiver callback);
}

View File

@@ -0,0 +1,40 @@
/*
* 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 com.android.internal.telecom;
import android.telecom.CallControl;
import com.android.internal.telecom.ICallControl;
import android.os.ResultReceiver;
import android.telecom.CallAudioState;
import android.telecom.CallException;
/**
* {@hide}
*/
oneway interface ICallEventCallback {
// publicly exposed. Client should override
void onAddCallControl(String callId, int resultCode, in ICallControl callControl,
in CallException exception);
void onSetActive(String callId, in ResultReceiver callback);
void onSetInactive(String callId, in ResultReceiver callback);
void onAnswer(String callId, int videoState, in ResultReceiver callback);
void onReject(String callId, in ResultReceiver callback);
void onDisconnect(String callId, in ResultReceiver callback);
void onCallAudioStateChanged(String callId, in CallAudioState callAudioState);
// hidden methods that help with cleanup
void removeCallFromTransactionalServiceWrapper(String callId);
}

View File

@@ -25,6 +25,8 @@ import android.os.Bundle;
import android.os.UserHandle;
import android.telecom.PhoneAccount;
import android.content.pm.ParceledListSlice;
import android.telecom.CallAttributes;
import com.android.internal.telecom.ICallEventCallback;
/**
* Interface used to interact with Telecom. Mostly this is used by TelephonyManager for passing
@@ -391,4 +393,10 @@ interface ITelecomService {
*/
boolean isInSelfManagedCall(String packageName, in UserHandle userHandle,
String callingPackage);
/**
* @see TelecomServiceImpl#addCall
*/
void addCall(in CallAttributes callAttributes, in ICallEventCallback callback, String callId,
String callingPackage);
}

View File

@@ -0,0 +1,76 @@
/*
* 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 com.android.internal.telecom;
import android.os.OutcomeReceiver;
import android.telecom.CallAttributes;
import android.telecom.CallControl;
import android.telecom.CallEventCallback;
import android.telecom.CallException;
import java.util.concurrent.Executor;
/**
* @hide
*/
public class TransactionalCall {
private final String mCallId;
private final CallAttributes mCallAttributes;
private final Executor mExecutor;
private final OutcomeReceiver<CallControl, CallException> mPendingControl;
private final CallEventCallback mCallEventCallback;
private CallControl mCallControl;
public TransactionalCall(String callId, CallAttributes callAttributes,
Executor executor, OutcomeReceiver<CallControl, CallException> pendingControl,
CallEventCallback callEventCallback) {
mCallId = callId;
mCallAttributes = callAttributes;
mExecutor = executor;
mPendingControl = pendingControl;
mCallEventCallback = callEventCallback;
}
public void setCallControl(CallControl callControl) {
mCallControl = callControl;
}
public CallControl getCallControl() {
return mCallControl;
}
public String getCallId() {
return mCallId;
}
public CallAttributes getCallAttributes() {
return mCallAttributes;
}
public Executor getExecutor() {
return mExecutor;
}
public OutcomeReceiver<CallControl, CallException> getPendingControl() {
return mPendingControl;
}
public CallEventCallback getCallEventCallback() {
return mCallEventCallback;
}
}