diff options
author | Xin Li <delphij@google.com> | 2023-08-25 12:59:08 -0700 |
---|---|---|
committer | Xin Li <delphij@google.com> | 2023-08-25 12:59:08 -0700 |
commit | 7d3ffbae618e9e728644a96647ed709bf39ae759 (patch) | |
tree | ab369a30c6a0e17a69c8f80c6353be4de3692e10 /telecomm | |
parent | a8a87bbca9162af7add830139198c4ee899fa123 (diff) | |
parent | 8a809c6e46007521f75ac035ad4b1dcc1d00d9cf (diff) | |
download | base-7d3ffbae618e9e728644a96647ed709bf39ae759.tar.gz |
Merge Android U (ab/10368041)
Bug: 291102124
Merged-In: I3c9e9d15786fbead1b874636b46844f6c24bccc2
Change-Id: Id6cf6cc13baef4e67486c6271a1510146204affa
Diffstat (limited to 'telecomm')
53 files changed, 4223 insertions, 63 deletions
diff --git a/telecomm/java/android/telecom/Call.java b/telecomm/java/android/telecom/Call.java index 9c1e8dd25a0e..c152a41c8694 100644 --- a/telecomm/java/android/telecom/Call.java +++ b/telecomm/java/android/telecom/Call.java @@ -168,6 +168,18 @@ public final class Call { public static final String AVAILABLE_PHONE_ACCOUNTS = "selectPhoneAccountAccounts"; /** + * Extra key intended for {@link InCallService}s that notify the user of an incoming call. When + * EXTRA_IS_SUPPRESSED_BY_DO_NOT_DISTURB returns true, the {@link InCallService} should not + * interrupt the user of the incoming call because the call is being suppressed by Do Not + * Disturb settings. + * + * This extra will be removed from the {@link Call} object for {@link InCallService}s that do + * not hold the {@link android.Manifest.permission#READ_CONTACTS} permission. + */ + public static final String EXTRA_IS_SUPPRESSED_BY_DO_NOT_DISTURB = + "android.telecom.extra.IS_SUPPRESSED_BY_DO_NOT_DISTURB"; + + /** * Key for extra used to pass along a list of {@link PhoneAccountSuggestion}s to the in-call * UI when a call enters the {@link #STATE_SELECT_PHONE_ACCOUNT} state. The list included here * will have the same length and be in the same order as the list passed with @@ -348,6 +360,18 @@ public final class Call { "android.telecom.extra.DIAGNOSTIC_MESSAGE"; /** + * Event reported from the Telecom stack to indicate that the {@link Connection} is not able to + * find any network and likely will not get connected. Upon receiving this event, the dialer + * app should show satellite SOS button if satellite is provisioned. + * <p> + * The dialer app receives this event via + * {@link Call.Callback#onConnectionEvent(Call, String, Bundle)}. + * @hide + */ + public static final String EVENT_DISPLAY_SOS_MESSAGE = + "android.telecom.event.DISPLAY_SOS_MESSAGE"; + + /** * Reject reason used with {@link #reject(int)} to indicate that the user is rejecting this * call because they have declined to answer it. This typically means that they are unable * to answer the call at this time and would prefer it be sent to voicemail. @@ -726,6 +750,7 @@ public final class Call { private final String mContactDisplayName; private final @CallDirection int mCallDirection; private final @Connection.VerificationStatus int mCallerNumberVerificationStatus; + private final Uri mContactPhotoUri; /** * Whether the supplied capabilities supports the specified capability. @@ -933,6 +958,17 @@ public final class Call { } /** + * @return The contact photo URI which corresponds to + * {@link android.provider.ContactsContract.PhoneLookup#PHOTO_URI}, or {@code null} if the + * lookup is not yet complete, if there's no contacts entry for the caller, + * or if the {@link InCallService} does not hold the + * {@link android.Manifest.permission#READ_CONTACTS} permission. + */ + public @Nullable Uri getContactPhotoUri() { + return mContactPhotoUri; + } + + /** * The display name for the caller. * <p> * This is the name as reported by the {@link ConnectionService} associated with this call. @@ -1131,7 +1167,8 @@ public final class Call { Objects.equals(mContactDisplayName, d.mContactDisplayName) && Objects.equals(mCallDirection, d.mCallDirection) && Objects.equals(mCallerNumberVerificationStatus, - d.mCallerNumberVerificationStatus); + d.mCallerNumberVerificationStatus) && + Objects.equals(mContactPhotoUri, d.mContactPhotoUri); } return false; } @@ -1156,7 +1193,8 @@ public final class Call { mCreationTimeMillis, mContactDisplayName, mCallDirection, - mCallerNumberVerificationStatus); + mCallerNumberVerificationStatus, + mContactPhotoUri); } /** {@hide} */ @@ -1180,7 +1218,8 @@ public final class Call { long creationTimeMillis, String contactDisplayName, int callDirection, - int callerNumberVerificationStatus) { + int callerNumberVerificationStatus, + Uri contactPhotoUri) { mState = state; mTelecomCallId = telecomCallId; mHandle = handle; @@ -1201,6 +1240,7 @@ public final class Call { mContactDisplayName = contactDisplayName; mCallDirection = callDirection; mCallerNumberVerificationStatus = callerNumberVerificationStatus; + mContactPhotoUri = contactPhotoUri; } /** {@hide} */ @@ -1225,7 +1265,9 @@ public final class Call { parcelableCall.getCreationTimeMillis(), parcelableCall.getContactDisplayName(), parcelableCall.getCallDirection(), - parcelableCall.getCallerNumberVerificationStatus()); + parcelableCall.getCallerNumberVerificationStatus(), + parcelableCall.getContactPhotoUri() + ); } @Override @@ -2100,6 +2142,14 @@ public final class Call { * <p> * No assumptions should be made as to how an In-Call UI or service will handle these * extras. Keys should be fully qualified (e.g., com.example.MY_EXTRA) to avoid conflicts. + * <p> + * Extras added using this method will be made available to the {@link ConnectionService} + * associated with this {@link Call} and notified via + * {@link Connection#onExtrasChanged(Bundle)}. + * <p> + * Extras added using this method will also be available to other running {@link InCallService}s + * and notified via {@link Call.Callback#onDetailsChanged(Call, Details)}. The extras can be + * accessed via {@link Details#getExtras()}. * * @param extras The extras to add. */ @@ -2639,7 +2689,8 @@ public final class Call { mDetails.getCreationTimeMillis(), mDetails.getContactDisplayName(), mDetails.getCallDirection(), - mDetails.getCallerNumberVerificationStatus() + mDetails.getCallerNumberVerificationStatus(), + mDetails.getContactPhotoUri() ); fireDetailsChanged(mDetails); } diff --git a/telecomm/java/android/telecom/CallAttributes.aidl b/telecomm/java/android/telecom/CallAttributes.aidl new file mode 100644 index 000000000000..19bada72190f --- /dev/null +++ b/telecomm/java/android/telecom/CallAttributes.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 CallAttributes;
\ No newline at end of file diff --git a/telecomm/java/android/telecom/CallAttributes.java b/telecomm/java/android/telecom/CallAttributes.java new file mode 100644 index 000000000000..b1a7d819cd17 --- /dev/null +++ b/telecomm/java/android/telecom/CallAttributes.java @@ -0,0 +1,356 @@ +/* + * 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"; + + /** @hide **/ + public static final String DISPLAY_NAME_KEY = "DisplayName"; + + /** @hide **/ + public static final String CALLER_PID_KEY = "CallerPid"; + + /** @hide **/ + public static final String CALLER_UID_KEY = "CallerUid"; + + 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 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; + } + + /** + * Sets the type of call; uses to indicate if a call is a video call or audio call. + * @param callType The call type. + * @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; + } + + /** + * Sets the capabilities of this call. Use this to indicate whether your app supports + * holding, streaming and call transfers. + * @param callCapabilities Bitmask of call capabilities. + * @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); + } +} diff --git a/telecomm/java/android/telecom/CallAudioState.java b/telecomm/java/android/telecom/CallAudioState.java index fccdf76372dd..c7cc1bd88bdf 100644 --- a/telecomm/java/android/telecom/CallAudioState.java +++ b/telecomm/java/android/telecom/CallAudioState.java @@ -27,7 +27,6 @@ import android.os.Parcelable; 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 @@ public final class CallAudioState implements Parcelable { /** 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 @@ public final class CallAudioState implements Parcelable { * @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 @@ public final class CallAudioState implements Parcelable { */ @CallAudioRoute public int getSupportedRouteMask() { - return supportedRouteMask; + if (route == ROUTE_STREAMING) { + return ROUTE_STREAMING; + } else { + return supportedRouteMask; + } } /** @@ -232,6 +238,9 @@ public final class CallAudioState implements Parcelable { 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.aidl b/telecomm/java/android/telecom/CallControl.aidl new file mode 100644 index 000000000000..0f780e612d5b --- /dev/null +++ b/telecomm/java/android/telecom/CallControl.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 CallControl; diff --git a/telecomm/java/android/telecom/CallControl.java b/telecomm/java/android/telecom/CallControl.java new file mode 100644 index 000000000000..50f2ad4561cc --- /dev/null +++ b/telecomm/java/android/telecom/CallControl.java @@ -0,0 +1,398 @@ +/* + * 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.annotation.SuppressLint; +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 android.text.TextUtils; + +import com.android.internal.telecom.ClientTransactionalServiceRepository; +import com.android.internal.telecom.ICallControl; + +import java.util.List; +import java.util.Objects; +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. + */ +@SuppressLint("NotCloseable") +public final class CallControl { + 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. This method should be called when either an + * outgoing call is ready to go active or a held call is ready to go active again. For incoming + * calls that are ready to be answered, use + * {@link CallControl#answer(int, Executor, OutcomeReceiver)}. + * + * @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 answer an incoming call. For outgoing calls and calls that have been placed + * on hold, use {@link CallControl#setActive(Executor, OutcomeReceiver)}. + * + * @param videoState to report to Telecom. Telecom will store VideoState in the event another + * service/device requests it in order to continue the call on another screen. + * @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 answer(@android.telecom.CallAttributes.CallType int videoState, + @CallbackExecutor @NonNull Executor executor, + @NonNull OutcomeReceiver<Void, CallException> callback) { + validateVideoState(videoState); + Objects.requireNonNull(executor); + Objects.requireNonNull(callback); + if (mServerInterface != null) { + try { + mServerInterface.answer(videoState, mCallId, + new CallControlResultReceiver("answer", 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 disconnect the call and remove the call from telecom tracking. + * + * @param disconnectCause represents the cause for disconnecting the call. The only valid + * codes for the {@link android.telecom.DisconnectCause} passed in are: + * <ul> + * <li>{@link DisconnectCause#LOCAL}</li> + * <li>{@link DisconnectCause#REMOTE}</li> + * <li>{@link DisconnectCause#REJECTED}</li> + * <li>{@link DisconnectCause#MISSED}</li> + * </ul> + * @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. + * + * <p> + * Note: After the call has been successfully disconnected, calling any CallControl API will + * result in the {@link OutcomeReceiver#onError} with + * {@link CallException#CODE_CALL_IS_NOT_BEING_TRACKED}. + */ + public void disconnect(@NonNull DisconnectCause disconnectCause, + @CallbackExecutor @NonNull Executor executor, + @NonNull OutcomeReceiver<Void, CallException> callback) { + Objects.requireNonNull(disconnectCause); + Objects.requireNonNull(executor); + Objects.requireNonNull(callback); + validateDisconnectCause(disconnectCause); + 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 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 + * started the call streaming. + * + * {@link OutcomeReceiver#onError} will be called if Telecom has failed to + * start the call streaming. 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); + } + } + + /** + * Request a CallEndpoint change. Clients should not define their own CallEndpoint when + * requesting a change. Instead, the new endpoint should be one of the valid endpoints provided + * by {@link CallEventCallback#onAvailableCallEndpointsChanged(List)}. + * + * @param callEndpoint The {@link CallEndpoint} to change to. + * @param executor The {@link Executor} on which the {@link OutcomeReceiver} callback + * will be called on. + * @param callback The {@link OutcomeReceiver} 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 changed the CallEndpoint that was requested. + * + * {@link OutcomeReceiver#onError} will be called if Telecom has failed to + * switch to the requested CallEndpoint. A {@link CallException} will be + * passed that details why the operation failed. + */ + public void requestCallEndpointChange(@NonNull CallEndpoint callEndpoint, + @CallbackExecutor @NonNull Executor executor, + @NonNull OutcomeReceiver<Void, CallException> callback) { + Objects.requireNonNull(callEndpoint); + Objects.requireNonNull(executor); + Objects.requireNonNull(callback); + if (mServerInterface != null) { + try { + mServerInterface.requestCallEndpointChange(callEndpoint, + new CallControlResultReceiver("endpointChange", executor, callback)); + } catch (RemoteException e) { + throw e.rethrowAsRuntimeException(); + } + } else { + throw new IllegalStateException(INTERFACE_ERROR_MSG); + } + } + + /** + * Raises an event to the {@link android.telecom.InCallService} implementations tracking this + * call via {@link android.telecom.Call.Callback#onConnectionEvent(Call, String, Bundle)}. + * These events and the associated extra keys for the {@code Bundle} parameter are mutually + * defined by a VoIP application and {@link android.telecom.InCallService}. This API is used to + * relay additional information about a call other than what is specified in the + * {@link android.telecom.CallAttributes} to {@link android.telecom.InCallService}s. This might + * include, for example, a change to the list of participants in a meeting, or the name of the + * speakers who have their hand raised. Where appropriate, the {@link InCallService}s tracking + * this call may choose to render this additional information about the call. An automotive + * calling UX, for example may have enough screen real estate to indicate the number of + * participants in a meeting, but to prevent distractions could suppress the list of + * participants. + * + * @param event a string event identifier agreed upon between a VoIP application and an + * {@link android.telecom.InCallService} + * @param extras a {@link android.os.Bundle} containing information about the event, as agreed + * upon between a VoIP application and {@link android.telecom.InCallService}. + */ + public void sendEvent(@NonNull String event, @NonNull Bundle extras) { + Objects.requireNonNull(event); + Objects.requireNonNull(extras); + if (mServerInterface != null) { + try { + mServerInterface.sendEvent(mCallId, event, extras); + } catch (RemoteException e) { + throw e.rethrowAsRuntimeException(); + } + } else { + throw new IllegalStateException(INTERFACE_ERROR_MSG); + } + } + + /** + * 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); + } + + /** @hide */ + private void validateDisconnectCause(DisconnectCause disconnectCause) { + final int code = disconnectCause.getCode(); + if (code != DisconnectCause.LOCAL && code != DisconnectCause.REMOTE + && code != DisconnectCause.MISSED && code != DisconnectCause.REJECTED) { + throw new IllegalArgumentException(TextUtils.formatSimple( + "The DisconnectCause code provided, %d , is not a valid Disconnect code. Valid " + + "DisconnectCause codes are limited to [DisconnectCause.LOCAL, " + + "DisconnectCause.REMOTE, DisconnectCause.MISSED, or " + + "DisconnectCause.REJECTED]", disconnectCause.getCode())); + } + } + + /** @hide */ + private void validateVideoState(@android.telecom.CallAttributes.CallType int videoState) { + if (videoState != CallAttributes.AUDIO_CALL && videoState != CallAttributes.VIDEO_CALL) { + throw new IllegalArgumentException(TextUtils.formatSimple( + "The VideoState argument passed in, %d , is not a valid VideoState. The " + + "VideoState choices are limited to CallAttributes.AUDIO_CALL or" + + "CallAttributes.VIDEO_CALL", videoState)); + } + } +} diff --git a/telecomm/java/android/telecom/CallControlCallback.java b/telecomm/java/android/telecom/CallControlCallback.java new file mode 100644 index 000000000000..eac2e64aa2ab --- /dev/null +++ b/telecomm/java/android/telecom/CallControlCallback.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.telecom; + +import android.annotation.NonNull; + +import java.util.function.Consumer; + +/** + * CallControlCallback relays call updates (that require a response) from the Telecom framework out + * to the application.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> + * All CallControlCallbacks are transactional, meaning that a client must + * complete the {@link Consumer} via {@link Consumer#accept(Object)} in order to complete the + * CallControlCallbacks. If a CallControlCallbacks 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 + * CallControlCallbacks 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 CallControlCallback { + /** + * 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}. Telecom will effectively ignore the remote + * setActive request and the call will remain in whatever state it is in. + */ + 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}. Telecom will effectively ignore the remote + * setInactive request and the call will remain in whatever state it is in. + */ + 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}. However, Telecom will still disconnect + * the call and remove it from tracking. + */ + void onAnswer(@android.telecom.CallAttributes.CallType int videoState, + @NonNull Consumer<Boolean> wasCompleted); + + /** + * Telecom is informing the client to disconnect the call + * + * @param disconnectCause represents the cause for disconnecting 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}. However, Telecom will still disconnect + * the call and remove it from tracking. + */ + void onDisconnect(@NonNull DisconnectCause disconnectCause, + @NonNull Consumer<Boolean> wasCompleted); + + /** + * 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); +} diff --git a/telecomm/java/android/telecom/CallEndpoint.aidl b/telecomm/java/android/telecom/CallEndpoint.aidl new file mode 100644 index 000000000000..45b2249c00cd --- /dev/null +++ b/telecomm/java/android/telecom/CallEndpoint.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 CallEndpoint;
\ No newline at end of file diff --git a/telecomm/java/android/telecom/CallEndpoint.java b/telecomm/java/android/telecom/CallEndpoint.java new file mode 100644 index 000000000000..0b2211ddb94a --- /dev/null +++ b/telecomm/java/android/telecom/CallEndpoint.java @@ -0,0 +1,222 @@ +/* + * 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.os.Parcel; +import android.os.ParcelUuid; +import android.os.Parcelable; +import android.text.TextUtils; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Objects; +import java.util.UUID; + +/** + * Encapsulates the endpoint where call media can flow + */ +public final class CallEndpoint implements Parcelable { + /** @hide */ + public static final int ENDPOINT_OPERATION_SUCCESS = 0; + /** @hide */ + public static final int ENDPOINT_OPERATION_FAILED = 1; + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({TYPE_UNKNOWN, TYPE_EARPIECE, TYPE_BLUETOOTH, TYPE_WIRED_HEADSET, TYPE_SPEAKER, + TYPE_STREAMING}) + public @interface EndpointType {} + + /** Indicates that the type of endpoint through which call media flows is unknown type. */ + public static final int TYPE_UNKNOWN = -1; + + /** Indicates that the type of endpoint through which call media flows is an earpiece. */ + public static final int TYPE_EARPIECE = 1; + + /** Indicates that the type of endpoint through which call media flows is a Bluetooth. */ + public static final int TYPE_BLUETOOTH = 2; + + /** Indicates that the type of endpoint through which call media flows is a wired headset. */ + public static final int TYPE_WIRED_HEADSET = 3; + + /** Indicates that the type of endpoint through which call media flows is a speakerphone. */ + public static final int TYPE_SPEAKER = 4; + + /** Indicates that the type of endpoint through which call media flows is an external. */ + public static final int TYPE_STREAMING = 5; + + private final CharSequence mName; + private final int mType; + private final ParcelUuid mIdentifier; + + /** + * Constructor for a {@link CallEndpoint} object. + * + * @param name Human-readable name associated with the endpoint + * @param type The type of endpoint through which call media being routed + * Allowed values: + * {@link #TYPE_EARPIECE} + * {@link #TYPE_BLUETOOTH} + * {@link #TYPE_WIRED_HEADSET} + * {@link #TYPE_SPEAKER} + * {@link #TYPE_STREAMING} + * {@link #TYPE_UNKNOWN} + * @param id A unique identifier for this endpoint on the device + */ + public CallEndpoint(@NonNull CharSequence name, @EndpointType int type, + @NonNull ParcelUuid id) { + this.mName = name; + this.mType = type; + this.mIdentifier = id; + } + + /** @hide */ + public CallEndpoint(@NonNull CharSequence name, @EndpointType int type) { + this(name, type, new ParcelUuid(UUID.randomUUID())); + } + + /** @hide */ + public CallEndpoint(CallEndpoint endpoint) { + mName = endpoint.getEndpointName(); + mType = endpoint.getEndpointType(); + mIdentifier = endpoint.getIdentifier(); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (!(obj instanceof CallEndpoint)) { + return false; + } + CallEndpoint endpoint = (CallEndpoint) obj; + return getEndpointName().toString().contentEquals(endpoint.getEndpointName()) + && getEndpointType() == endpoint.getEndpointType() + && getIdentifier().equals(endpoint.getIdentifier()); + } + + /** + * {@inheritDoc} + */ + @Override + public int hashCode() { + return Objects.hash(mName, mType, mIdentifier); + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + return TextUtils.formatSimple("[CallEndpoint Name: %s, Type: %s, Identifier: %s]", + mName.toString(), endpointTypeToString(mType), mIdentifier.toString()); + } + + /** + * @return Human-readable name associated with the endpoint + */ + @NonNull + public CharSequence getEndpointName() { + return mName; + } + + /** + * @return The type of endpoint through which call media being routed + */ + @EndpointType + public int getEndpointType() { + return mType; + } + + /** + * @return A unique identifier for this endpoint on the device + */ + @NonNull + public ParcelUuid getIdentifier() { + return mIdentifier; + } + + /** + * Converts the provided endpoint type into a human-readable string representation. + * + * @param endpointType to convert into a string. + * @return String representation of the provided endpoint type. + * @hide + */ + @NonNull + public static String endpointTypeToString(int endpointType) { + switch (endpointType) { + case TYPE_EARPIECE: + return "EARPIECE"; + case TYPE_BLUETOOTH: + return "BLUETOOTH"; + case TYPE_WIRED_HEADSET: + return "WIRED_HEADSET"; + case TYPE_SPEAKER: + return "SPEAKER"; + case TYPE_STREAMING: + return "EXTERNAL"; + default: + return "UNKNOWN (" + endpointType + ")"; + } + } + + /** + * Responsible for creating CallEndpoint objects for deserialized Parcels. + */ + public static final @android.annotation.NonNull Parcelable.Creator<CallEndpoint> CREATOR = + new Parcelable.Creator<CallEndpoint>() { + + @Override + public CallEndpoint createFromParcel(Parcel source) { + CharSequence name = source.readCharSequence(); + int type = source.readInt(); + ParcelUuid id = ParcelUuid.CREATOR.createFromParcel(source); + + return new CallEndpoint(name, type, id); + } + + @Override + public CallEndpoint[] newArray(int size) { + return new CallEndpoint[size]; + } + }; + + /** + * {@inheritDoc} + */ + @Override + public int describeContents() { + return 0; + } + + /** + * {@inheritDoc} + */ + @Override + public void writeToParcel(@NonNull Parcel destination, int flags) { + destination.writeCharSequence(mName); + destination.writeInt(mType); + mIdentifier.writeToParcel(destination, flags); + } +} diff --git a/telecomm/java/android/telecom/CallEndpointException.aidl b/telecomm/java/android/telecom/CallEndpointException.aidl new file mode 100644 index 000000000000..19b43c4b4a69 --- /dev/null +++ b/telecomm/java/android/telecom/CallEndpointException.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 CallEndpointException;
\ No newline at end of file diff --git a/telecomm/java/android/telecom/CallEndpointException.java b/telecomm/java/android/telecom/CallEndpointException.java new file mode 100644 index 000000000000..994e1c9f9412 --- /dev/null +++ b/telecomm/java/android/telecom/CallEndpointException.java @@ -0,0 +1,117 @@ +/* + * 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 represents a set of exceptions that can occur when requesting a + * {@link CallEndpoint} change. + */ +public final class CallEndpointException extends RuntimeException implements Parcelable { + /** @hide */ + public static final String CHANGE_ERROR = "ChangeErrorKey"; + + /** + * The operation has failed because requested CallEndpoint does not exist. + */ + public static final int ERROR_ENDPOINT_DOES_NOT_EXIST = 1; + + /** + * The operation was not completed on time. + */ + public static final int ERROR_REQUEST_TIME_OUT = 2; + + /** + * The operation was canceled by another request. + */ + public static final int ERROR_ANOTHER_REQUEST = 3; + + /** + * The operation has failed due to an unknown or unspecified error. + */ + public static final int ERROR_UNSPECIFIED = 4; + + private int mCode = ERROR_UNSPECIFIED; + 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 CallEndpointException objects for deserialized Parcels. + */ + public static final @android.annotation.NonNull Parcelable.Creator<CallEndpointException> + CREATOR = new Parcelable.Creator<>() { + @Override + public CallEndpointException createFromParcel(Parcel source) { + return new CallEndpointException(source.readString8(), source.readInt()); + } + + @Override + public CallEndpointException[] newArray(int size) { + return new CallEndpointException[size]; + } + }; + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({ERROR_ENDPOINT_DOES_NOT_EXIST, ERROR_REQUEST_TIME_OUT, ERROR_ANOTHER_REQUEST, + ERROR_UNSPECIFIED}) + public @interface CallEndpointErrorCode { + } + + public CallEndpointException(@Nullable String message, @CallEndpointErrorCode int code) { + super(getMessage(message, code)); + mCode = code; + mMessage = message; + } + + public @CallEndpointErrorCode 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; + } + } +} diff --git a/telecomm/java/android/telecom/CallEventCallback.java b/telecomm/java/android/telecom/CallEventCallback.java new file mode 100644 index 000000000000..a41c0113e933 --- /dev/null +++ b/telecomm/java/android/telecom/CallEventCallback.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.telecom; + +import android.annotation.NonNull; +import android.os.Bundle; + +import java.util.List; + +/** + * CallEventCallback relays call updates (that do not require any action) from the Telecom framework + * out to the application. 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. + */ +public interface CallEventCallback { + /** + * Telecom is informing the client the current {@link CallEndpoint} changed. + * + * @param newCallEndpoint The new {@link CallEndpoint} through which call media flows + * (i.e. speaker, bluetooth, etc.). + */ + void onCallEndpointChanged(@NonNull CallEndpoint newCallEndpoint); + + /** + * Telecom is informing the client that the available {@link CallEndpoint}s have changed. + * + * @param availableEndpoints The set of available {@link CallEndpoint}s reported by Telecom. + */ + void onAvailableCallEndpointsChanged(@NonNull List<CallEndpoint> availableEndpoints); + + /** + * Called when the mute state changes. + * + * @param isMuted The current mute state. + */ + void onMuteStateChanged(boolean isMuted); + + /** + * 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); + + /** + * Informs this {@link android.telecom.CallEventCallback} on events raised from a + * {@link android.telecom.InCallService} presenting this call. These events and the + * associated extra keys for the {@code Bundle} parameter are mutually defined by a VoIP + * application and {@link android.telecom.InCallService}. This enables alternative calling + * surfaces, such as an automotive UI, to relay requests to perform other non-standard call + * actions to the app. For example, an automotive calling solution may offer the ability for + * the user to raise their hand during a meeting. + * + * @param event a string event identifier agreed upon between a VoIP application and an + * {@link android.telecom.InCallService} + * @param extras a {@link android.os.Bundle} containing information about the event, as agreed + * upon between a VoIP application and {@link android.telecom.InCallService}. + */ + void onEvent(@NonNull String event, @NonNull Bundle extras); +} diff --git a/telecomm/java/android/telecom/CallException.aidl b/telecomm/java/android/telecom/CallException.aidl new file mode 100644 index 000000000000..a16af121145d --- /dev/null +++ b/telecomm/java/android/telecom/CallException.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 CallException;
\ No newline at end of file diff --git a/telecomm/java/android/telecom/CallException.java b/telecomm/java/android/telecom/CallException.java new file mode 100644 index 000000000000..e554082fe410 --- /dev/null +++ b/telecomm/java/android/telecom/CallException.java @@ -0,0 +1,149 @@ +/* + * 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 CallControlCallback}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 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; + } + } +} diff --git a/telecomm/java/android/telecom/CallScreeningService.java b/telecomm/java/android/telecom/CallScreeningService.java index 37b4e657973b..d1d16ff8b641 100644 --- a/telecomm/java/android/telecom/CallScreeningService.java +++ b/telecomm/java/android/telecom/CallScreeningService.java @@ -495,6 +495,9 @@ public abstract class CallScreeningService extends Service { * Note: Calls will still be logged with type * {@link android.provider.CallLog.Calls#BLOCKED_TYPE}, regardless of how this property * is set. + * <p> + * Note: Only the carrier and system call screening apps can use this parameter; + * this parameter is ignored otherwise. */ public Builder setSkipCallLog(boolean shouldSkipCallLog) { mShouldSkipCallLog = shouldSkipCallLog; diff --git a/telecomm/java/android/telecom/CallStreamingService.java b/telecomm/java/android/telecom/CallStreamingService.java new file mode 100644 index 000000000000..581cd7ee6d50 --- /dev/null +++ b/telecomm/java/android/telecom/CallStreamingService.java @@ -0,0 +1,220 @@ +/* + * 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.annotation.SystemApi; +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. + * <p> + * Below is an example manifest registration for a {@code CallStreamingService}. + * <pre> + * {@code + * <service android:name=".EgCallStreamingService" + * android:permission="android.permission.BIND_CALL_STREAMING_SERVICE" > + * ... + * <intent-filter> + * <action android:name="android.telecom.CallStreamingService" /> + * </intent-filter> + * </service> + * } + * </pre> + * + * @hide + */ +@SystemApi +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_STATE_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) { + Log.i(this, "handleMessage: null adapter!"); + return; + } + + switch (msg.what) { + case MSG_SET_STREAMING_CALL_ADAPTER: + if (msg.obj != null) { + Log.i(this, "MSG_SET_STREAMING_CALL_ADAPTER"); + mStreamingCallAdapter = new StreamingCallAdapter( + (IStreamingCallAdapter) msg.obj); + } + break; + case MSG_CALL_STREAMING_STARTED: + Log.i(this, "MSG_CALL_STREAMING_STARTED"); + mCall = (StreamingCall) msg.obj; + mCall.setAdapter(mStreamingCallAdapter); + CallStreamingService.this.onCallStreamingStarted(mCall); + break; + case MSG_CALL_STREAMING_STOPPED: + Log.i(this, "MSG_CALL_STREAMING_STOPPED"); + mCall = null; + mStreamingCallAdapter = null; + CallStreamingService.this.onCallStreamingStopped(); + break; + case MSG_CALL_STREAMING_STATE_CHANGED: + int state = (int) msg.obj; + if (mStreamingCallAdapter != null) { + mCall.requestStreamingState(state); + CallStreamingService.this.onCallStreamingStateChanged(state); + } + break; + default: + break; + } + } + }; + + @Nullable + @Override + public IBinder onBind(@NonNull Intent intent) { + Log.i(this, "onBind"); + 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 { + Log.i(this, "setCallStreamingAdapter"); + mHandler.obtainMessage(MSG_SET_STREAMING_CALL_ADAPTER, streamingCallAdapter) + .sendToTarget(); + } + + @Override + public void onCallStreamingStarted(StreamingCall call) throws RemoteException { + Log.i(this, "onCallStreamingStarted"); + 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_STATE_CHANGED, state).sendToTarget(); + } + } + + /** + * Call streaming request reject reason used with + * {@link CallEventCallback#onCallStreamingFailed(int)} to indicate that telecom is rejecting a + * call streaming request due to unknown reason. + */ + public static final int STREAMING_FAILED_UNKNOWN = 0; + + /** + * 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_UNKNOWN, + 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/Conference.java b/telecomm/java/android/telecom/Conference.java index f84dd7b0bb17..f8037175fb05 100644 --- a/telecomm/java/android/telecom/Conference.java +++ b/telecomm/java/android/telecom/Conference.java @@ -88,6 +88,7 @@ public abstract class Conference extends Conferenceable { private String mTelecomCallId; private PhoneAccountHandle mPhoneAccount; private CallAudioState mCallAudioState; + private CallEndpoint mCallEndpoint; private int mState = Connection.STATE_NEW; private DisconnectCause mDisconnectCause; private int mConnectionCapabilities; @@ -223,12 +224,26 @@ public abstract class Conference extends Conferenceable { * @return The audio state of the conference, describing how its audio is currently * being routed by the system. This is {@code null} if this Conference * does not directly know about its audio state. + * @deprecated Use {@link #getCurrentCallEndpoint()}, + * {@link #onAvailableCallEndpointsChanged(List)} and + * {@link #onMuteStateChanged(boolean)} instead. */ + @Deprecated public final CallAudioState getCallAudioState() { return mCallAudioState; } /** + * Obtains the current CallEndpoint. + * + * @return An object encapsulating the CallEndpoint. + */ + @NonNull + public final CallEndpoint getCurrentCallEndpoint() { + return mCallEndpoint; + } + + /** * Returns VideoProvider of the primary call. This can be null. */ public VideoProvider getVideoProvider() { @@ -314,10 +329,35 @@ public abstract class Conference extends Conferenceable { * value. * * @param state The new call audio state. + * @deprecated Use {@link #onCallEndpointChanged(CallEndpoint)}, + * {@link #onAvailableCallEndpointsChanged(List)} and + * {@link #onMuteStateChanged(boolean)} instead. */ + @Deprecated public void onCallAudioStateChanged(CallAudioState state) {} /** + * Notifies the {@link Conference} that the audio endpoint has been changed. + * + * @param callEndpoint The new call endpoint. + */ + public void onCallEndpointChanged(@NonNull CallEndpoint callEndpoint) {} + + /** + * Notifies the {@link Conference} that the available call endpoints have been changed. + * + * @param availableEndpoints The available call endpoints. + */ + public void onAvailableCallEndpointsChanged(@NonNull List<CallEndpoint> availableEndpoints) {} + + /** + * Notifies the {@link Conference} that its audio mute state has been changed. + * + * @param isMuted The new mute state. + */ + public void onMuteStateChanged(boolean isMuted) {} + + /** * Notifies the {@link Conference} that a {@link Connection} has been added to it. * * @param connection The newly added connection. @@ -730,6 +770,40 @@ public abstract class Conference extends Conferenceable { onCallAudioStateChanged(state); } + /** + * Inform this Conference that the audio endpoint has been changed. + * + * @param endpoint The new call endpoint. + * @hide + */ + final void setCallEndpoint(CallEndpoint endpoint) { + Log.d(this, "setCallEndpoint %s", endpoint); + mCallEndpoint = endpoint; + onCallEndpointChanged(endpoint); + } + + /** + * Inform this Conference that the available call endpoints have been changed. + * + * @param availableEndpoints The available call endpoints. + * @hide + */ + final void setAvailableCallEndpoints(List<CallEndpoint> availableEndpoints) { + Log.d(this, "setAvailableCallEndpoints"); + onAvailableCallEndpointsChanged(availableEndpoints); + } + + /** + * Inform this Conference that its audio mute state has been changed. + * + * @param isMuted The new mute state. + * @hide + */ + final void setMuteState(boolean isMuted) { + Log.d(this, "setMuteState %s", isMuted); + onMuteStateChanged(isMuted); + } + private void setState(int newState) { if (mState != newState) { int oldState = mState; diff --git a/telecomm/java/android/telecom/Connection.java b/telecomm/java/android/telecom/Connection.java index 7c60f81259c7..943d8d6cdd55 100644 --- a/telecomm/java/android/telecom/Connection.java +++ b/telecomm/java/android/telecom/Connection.java @@ -19,6 +19,7 @@ package android.telecom; import static android.Manifest.permission.MODIFY_PHONE_STATE; import android.Manifest; +import android.annotation.CallbackExecutor; import android.annotation.ElapsedRealtimeLong; import android.annotation.IntDef; import android.annotation.IntRange; @@ -32,6 +33,7 @@ import android.compat.annotation.UnsupportedAppUsage; import android.content.ComponentName; import android.content.Intent; import android.hardware.camera2.CameraManager; +import android.location.Location; import android.net.Uri; import android.os.Binder; import android.os.Bundle; @@ -39,6 +41,7 @@ import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.os.Message; +import android.os.OutcomeReceiver; import android.os.Parcel; import android.os.ParcelFileDescriptor; import android.os.Parcelable; @@ -66,8 +69,10 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Objects; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executor; /** * Represents a phone call or connection to a remote endpoint that carries voice and/or video @@ -1046,6 +1051,13 @@ public abstract class Connection extends Conferenceable { public static final String EXTRA_CALL_QUALITY_REPORT = "android.telecom.extra.CALL_QUALITY_REPORT"; + /** + * Key to obtain location as a result of ({@code queryLocationForEmergency} from Bundle + * @hide + */ + public static final String EXTRA_KEY_QUERY_LOCATION = + "android.telecom.extra.KEY_QUERY_LOCATION"; + // Flag controlling whether PII is emitted into the logs private static final boolean PII_DEBUG = Log.isLoggable(android.util.Log.DEBUG); @@ -1279,6 +1291,11 @@ public abstract class Connection extends Conferenceable { /** @hide */ public void onPhoneAccountChanged(Connection c, PhoneAccountHandle pHandle) {} public void onConnectionTimeReset(Connection c) {} + public void onEndpointChanged(Connection c, CallEndpoint endpoint, Executor executor, + OutcomeReceiver<Void, CallEndpointException> callback) {} + public void onQueryLocation(Connection c, long timeoutMillis, @NonNull String provider, + @NonNull @CallbackExecutor Executor executor, + @NonNull OutcomeReceiver<Location, QueryLocationException> callback) {} } /** @@ -2161,6 +2178,7 @@ public abstract class Connection extends Conferenceable { private PhoneAccountHandle mPhoneAccountHandle; private int mState = STATE_NEW; private CallAudioState mCallAudioState; + private CallEndpoint mCallEndpoint; private Uri mAddress; private int mAddressPresentation; private String mCallerDisplayName; @@ -2289,7 +2307,11 @@ public abstract class Connection extends Conferenceable { * @return The audio state of the connection, describing how its audio is currently * being routed by the system. This is {@code null} if this Connection * does not directly know about its audio state. + * @deprecated Use {@link #getCurrentCallEndpoint()}, + * {@link #onAvailableCallEndpointsChanged(List)} and + * {@link #onMuteStateChanged(boolean)} instead. */ + @Deprecated public final CallAudioState getCallAudioState() { return mCallAudioState; } @@ -2456,6 +2478,43 @@ public abstract class Connection extends Conferenceable { } /** + * Inform this Connection that the audio endpoint has been changed. + * + * @param endpoint The new call endpoint. + * @hide + */ + final void setCallEndpoint(CallEndpoint endpoint) { + checkImmutable(); + Log.d(this, "setCallEndpoint %s", endpoint); + mCallEndpoint = endpoint; + onCallEndpointChanged(endpoint); + } + + /** + * Inform this Connection that the available call endpoints have been changed. + * + * @param availableEndpoints The available call endpoints. + * @hide + */ + final void setAvailableCallEndpoints(List<CallEndpoint> availableEndpoints) { + checkImmutable(); + Log.d(this, "setAvailableCallEndpoints"); + onAvailableCallEndpointsChanged(availableEndpoints); + } + + /** + * Inform this Connection that its audio mute state has been changed. + * + * @param isMuted The new mute state. + * @hide + */ + final void setMuteState(boolean isMuted) { + checkImmutable(); + Log.d(this, "setMuteState %s", isMuted); + onMuteStateChanged(isMuted); + } + + /** * @param state An integer value of a {@code STATE_*} constant. * @return A string representation of the value. */ @@ -2531,11 +2590,21 @@ public abstract class Connection extends Conferenceable { */ public final void setCallerDisplayName(String callerDisplayName, int presentation) { checkImmutable(); - Log.d(this, "setCallerDisplayName %s", callerDisplayName); - mCallerDisplayName = callerDisplayName; - mCallerDisplayNamePresentation = presentation; - for (Listener l : mListeners) { - l.onCallerDisplayNameChanged(this, callerDisplayName, presentation); + boolean nameChanged = !Objects.equals(mCallerDisplayName, callerDisplayName); + boolean presentationChanged = mCallerDisplayNamePresentation != presentation; + if (nameChanged) { + // Ensure the new name is not clobbering the old one with a null value due to the caller + // wanting to only set the presentation and not knowing the display name. + mCallerDisplayName = callerDisplayName; + } + if (presentationChanged) { + mCallerDisplayNamePresentation = presentation; + } + if (nameChanged || presentationChanged) { + for (Listener l : mListeners) { + l.onCallerDisplayNameChanged(this, mCallerDisplayName, + mCallerDisplayNamePresentation); + } } } @@ -2763,6 +2832,12 @@ public abstract class Connection extends Conferenceable { * @param isVoip True if the audio mode is VOIP. */ public final void setAudioModeIsVoip(boolean isVoip) { + if (!isVoip && (mConnectionProperties & PROPERTY_SELF_MANAGED) == PROPERTY_SELF_MANAGED) { + Log.i(this, + "setAudioModeIsVoip: Ignored request to set a self-managed connection's" + + " audioModeIsVoip to false. Doing so can cause unwanted behavior."); + return; + } checkImmutable(); mAudioModeIsVoip = isVoip; for (Listener l : mListeners) { @@ -3064,7 +3139,10 @@ public abstract class Connection extends Conferenceable { * @param route The audio route to use (one of {@link CallAudioState#ROUTE_BLUETOOTH}, * {@link CallAudioState#ROUTE_EARPIECE}, {@link CallAudioState#ROUTE_SPEAKER}, or * {@link CallAudioState#ROUTE_WIRED_HEADSET}). + * @deprecated Use {@link #requestCallEndpointChange(CallEndpoint, Executor, OutcomeReceiver)} + * instead. */ + @Deprecated public final void setAudioRoute(int route) { for (Listener l : mListeners) { l.onAudioRouteChanged(this, route, null); @@ -3084,7 +3162,10 @@ public abstract class Connection extends Conferenceable { * <p> * See also {@link InCallService#requestBluetoothAudio(BluetoothDevice)} * @param bluetoothDevice The bluetooth device to connect to. + * @deprecated Use {@link #requestCallEndpointChange(CallEndpoint, Executor, OutcomeReceiver)} + * instead. */ + @Deprecated public void requestBluetoothAudio(@NonNull BluetoothDevice bluetoothDevice) { for (Listener l : mListeners) { l.onAudioRouteChanged(this, CallAudioState.ROUTE_BLUETOOTH, @@ -3093,6 +3174,40 @@ public abstract class Connection extends Conferenceable { } /** + * Request audio routing to a specific CallEndpoint. Clients should not define their own + * CallEndpoint when requesting a change. Instead, the new endpoint should be one of the valid + * endpoints provided by {@link #onAvailableCallEndpointsChanged(List)}. + * When this request is honored, there will be change to the {@link #getCurrentCallEndpoint()}. + * <p> + * Used by self-managed {@link ConnectionService}s which wish to change the CallEndpoint for a + * self-managed {@link Connection} (see {@link PhoneAccount#CAPABILITY_SELF_MANAGED}.) + * <p> + * See also + * {@link InCallService#requestCallEndpointChange(CallEndpoint, Executor, OutcomeReceiver)}. + * + * @param endpoint The call endpoint to use. + * @param executor The executor of where the callback will execute. + * @param callback The callback to notify the result of the endpoint change. + */ + public final void requestCallEndpointChange(@NonNull CallEndpoint endpoint, + @NonNull @CallbackExecutor Executor executor, + @NonNull OutcomeReceiver<Void, CallEndpointException> callback) { + for (Listener l : mListeners) { + l.onEndpointChanged(this, endpoint, executor, callback); + } + } + + /** + * Obtains the current CallEndpoint. + * + * @return An object encapsulating the CallEndpoint. + */ + @NonNull + public final CallEndpoint getCurrentCallEndpoint() { + return mCallEndpoint; + } + + /** * Informs listeners that a previously requested RTT session via * {@link ConnectionRequest#isRequestingRtt()} or * {@link #onStartRtt(RttTextStream)} has succeeded. @@ -3129,6 +3244,36 @@ public abstract class Connection extends Conferenceable { } /** + * Query the device's location in order to place an Emergency Call. + * Only SIM call managers can call this method for Connections representing Emergency calls. + * If a previous location query request is not completed, the new location query request will + * be rejected and return a QueryLocationException with + * {@code QueryLocationException#ERROR_PREVIOUS_REQUEST_EXISTS} + * + * @param timeoutMillis long: Timeout in millis waiting for query response (MAX:5000, MIN:100). + * @param provider String: the location provider name, This value cannot be null. + * It is the caller's responsibility to select an enabled provider. The caller + * can use {@link android.location.LocationManager#getProviders(boolean)} + * to choose one of the enabled providers and pass it in. + * @param executor The executor of where the callback will execute. + * @param callback The callback to notify the result of queryLocation. + */ + public final void queryLocationForEmergency( + @IntRange(from = 100, to = 5000) long timeoutMillis, + @NonNull String provider, + @NonNull @CallbackExecutor Executor executor, + @NonNull OutcomeReceiver<Location, QueryLocationException> callback) { + if (provider == null || executor == null || callback == null) { + throw new IllegalArgumentException("There are arguments that must not be null"); + } + if (timeoutMillis < 100 || timeoutMillis > 5000) { + throw new IllegalArgumentException("The timeoutMillis should be min 100, max 5000"); + } + mListeners.forEach((l) -> + l.onQueryLocation(this, timeoutMillis, provider, executor, callback)); + } + + /** * Notifies this Connection that the {@link #getAudioState()} property has a new value. * * @param state The new connection audio state. @@ -3143,10 +3288,35 @@ public abstract class Connection extends Conferenceable { * Notifies this Connection that the {@link #getCallAudioState()} property has a new value. * * @param state The new connection audio state. + * @deprecated Use {@link #onCallEndpointChanged(CallEndpoint)}, + * {@link #onAvailableCallEndpointsChanged(List)} and + * {@link #onMuteStateChanged(boolean)} instead. */ + @Deprecated public void onCallAudioStateChanged(CallAudioState state) {} /** + * Notifies this Connection that the audio endpoint has been changed. + * + * @param callEndpoint The current CallEndpoint. + */ + public void onCallEndpointChanged(@NonNull CallEndpoint callEndpoint) {} + + /** + * Notifies this Connection that the available call endpoints have been changed. + * + * @param availableEndpoints The set of available CallEndpoint. + */ + public void onAvailableCallEndpointsChanged(@NonNull List<CallEndpoint> availableEndpoints) {} + + /** + * Notifies this Connection that its audio mute state has been changed. + * + * @param isMuted The current mute state. + */ + public void onMuteStateChanged(boolean isMuted) {} + + /** * Inform this Connection when it will or will not be tracked by an {@link InCallService} which * can provide an InCall UI. * This is primarily intended for use by Connections reported by self-managed diff --git a/telecomm/java/android/telecom/ConnectionService.java b/telecomm/java/android/telecom/ConnectionService.java index 6afc40064961..536e458159d1 100755 --- a/telecomm/java/android/telecom/ConnectionService.java +++ b/telecomm/java/android/telecom/ConnectionService.java @@ -16,6 +16,7 @@ package android.telecom; +import android.annotation.CallbackExecutor; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; @@ -25,12 +26,14 @@ import android.annotation.TestApi; import android.app.Service; import android.content.ComponentName; import android.content.Intent; +import android.location.Location; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.os.Message; +import android.os.OutcomeReceiver; import android.os.ParcelFileDescriptor; import android.os.RemoteException; import android.telecom.Logging.Session; @@ -48,6 +51,7 @@ import java.util.List; import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executor; /** * An abstract service that should be implemented by any apps which either: @@ -77,19 +81,240 @@ import java.util.concurrent.ConcurrentHashMap; * See {@link PhoneAccount} and {@link TelecomManager#registerPhoneAccount} for more information. * <p> * System managed {@link ConnectionService}s must be enabled by the user in the phone app settings - * before Telecom will bind to them. Self-managed {@link ConnectionService}s must be granted the - * appropriate permission before Telecom will bind to them. + * before Telecom will bind to them. Self-managed {@link ConnectionService}s must declare the + * {@link android.Manifest.permission#MANAGE_OWN_CALLS} permission in their manifest before Telecom + * will bind to them. * <p> * Once registered and enabled by the user in the phone app settings or granted permission, telecom * will bind to a {@link ConnectionService} implementation when it wants that * {@link ConnectionService} to place a call or the service has indicated that is has an incoming - * call through {@link TelecomManager#addNewIncomingCall}. The {@link ConnectionService} can then - * expect a call to {@link #onCreateIncomingConnection} or {@link #onCreateOutgoingConnection} + * call through {@link TelecomManager#addNewIncomingCall(PhoneAccountHandle, Bundle)}. The + * {@link ConnectionService} can then expect a call to + * {@link #onCreateIncomingConnection(PhoneAccountHandle, ConnectionRequest)} or + * {@link #onCreateOutgoingConnection(PhoneAccountHandle, ConnectionRequest)} * wherein it should provide a new instance of a {@link Connection} object. It is through this * {@link Connection} object that telecom receives state updates and the {@link ConnectionService} * receives call-commands such as answer, reject, hold and disconnect. * <p> * When there are no more live calls, telecom will unbind from the {@link ConnectionService}. + * <p> + * <h1>Self-Managed Connection Services</h1> + * A VoIP app can implement a {@link ConnectionService} to ensure that its calls are integrated + * into the Android platform. There are numerous benefits to using the Telecom APIs for a VoIP app: + * <ul> + * <li>Call concurrency is handled - the user is able to swap between calls in different + * apps and on the mobile network.</li> + * <li>Simplified audio routing - the platform provides your app with a unified list of the + * audio routes which are available + * (e.g. {@link android.telecom.Connection#onAvailableCallEndpointsChanged(List)}) and a + * standardized way to switch audio routes + * (e.g. {@link android.telecom.Connection#requestCallEndpointChange(CallEndpoint, Executor, + * OutcomeReceiver)} ).</li> + * <li>Bluetooth integration - your calls will be visible on and controllable via + * bluetooth devices (e.g. car head units and headsets).</li> + * <li>Companion device integration - wearable devices such as watches which implement an + * {@link InCallService} can optionally subscribe to see self-managed calls. Similar to a + * bluetooth headunit, wearables will typically render your call using a generic call UX and + * provide the user with basic call controls such as hangup, answer, reject.</li> + * <li>Automotive calling experiences - Android supports automotive optimized experiences which + * provides a means for calls to be controlled and viewed in an automobile; these experiences + * are capable of leveraging call metadata provided by your app.</li> + * </ul> + * <h2>Registering a Phone Account</h2> + * Before your app can handle incoming or outgoing calls through Telecom it needs to register a + * {@link PhoneAccount} with Telecom indicating to the platform that your app is capable of calling. + * <p> + * Your app should create a new instance of {@link PhoneAccount} which meets the following + * requirements: + * <ul> + * <li>Has {@link PhoneAccount#CAPABILITY_SELF_MANAGED} (set using + * {@link PhoneAccount.Builder#setCapabilities(int)}). This indicates to Telecom that your + * app will report calls but that it provides a primary UI for the calls by itself.</li> + * <li>Provide a unique identifier for the {@link PhoneAccount} via the + * {@link PhoneAccountHandle#getId()} attribute. As per the {@link PhoneAccountHandle} + * documentation, you should NOT use an identifier which contains PII or other sensitive + * information. A typical choice is a UUID.</li> + * </ul> + * Your app should register the new {@link PhoneAccount} with Telecom using + * {@link TelecomManager#registerPhoneAccount(PhoneAccount)}. {@link PhoneAccount}s persist across + * reboot. You can use {@link TelecomManager#getOwnSelfManagedPhoneAccounts()} to confirm the + * {@link PhoneAccount} you registered. Your app should generally only register a single + * {@link PhoneAccount}. + * + * <h2>Implementing ConnectionService</h2> + * Your app uses {@link TelecomManager#placeCall(Uri, Bundle)} to start new outgoing calls and + * {@link TelecomManager#addNewIncomingCall(PhoneAccountHandle, Bundle)} to report new incoming + * calls. Calling these APIs causes the Telecom stack to bind to your app's + * {@link ConnectionService} implementation. Telecom will either inform your app that it cannot + * handle a call request at the current time (i.e. there could be an ongoing emergency call, which + * means your app is not allowed to handle calls at the current time), or it will ask your app to + * create a new instance of {@link Connection} to represent a call in your app. + * + * Your app should implement the following {@link ConnectionService} methods: + * <ul> + * <li>{@link ConnectionService#onCreateOutgoingConnection(PhoneAccountHandle, + * ConnectionRequest)} - called by Telecom to ask your app to make a new {@link Connection} + * to represent an outgoing call your app requested via + * {@link TelecomManager#placeCall(Uri, Bundle)}.</li> + * <li><{@link ConnectionService#onCreateOutgoingConnectionFailed(PhoneAccountHandle, + * ConnectionRequest)} - called by Telecom to inform your app that a call it reported via + * {@link TelecomManager#placeCall(Uri, Bundle)} cannot be handled at this time. Your app + * should NOT place a call at the current time.</li> + * <li>{@link ConnectionService#onCreateIncomingConnection(PhoneAccountHandle, + * ConnectionRequest)} - called by Telecom to ask your app to make a new {@link Connection} + * to represent an incoming call your app reported via + * {@link TelecomManager#addNewIncomingCall(PhoneAccountHandle, Bundle)}.</li> + * <li>{@link ConnectionService#onCreateIncomingConnectionFailed(PhoneAccountHandle, + * ConnectionRequest)} - called by Telecom to inform your app that an incoming call it reported + * via {@link TelecomManager#addNewIncomingCall(PhoneAccountHandle, Bundle)} cannot be handled + * at this time. Your app should NOT post a new incoming call notification and should silently + * reject the call.</li> + * </ul> + * + * <h2>Implementing a Connection</h2> + * Your app should extend the {@link Connection} class to represent calls in your app. When you + * create new instances of your {@link Connection}, you should ensure the following properties are + * set on the new {@link Connection} instance returned by your {@link ConnectionService}: + * <ul> + * <li>{@link Connection#setAddress(Uri, int)} - the identifier for the other party. For + * apps that user phone numbers the {@link Uri} can be a {@link PhoneAccount#SCHEME_TEL} URI + * representing the phone number.</li> + * <li>{@link Connection#setCallerDisplayName(String, int)} - the display name of the other + * party. This is what will be shown on Bluetooth devices and other calling surfaces such + * as wearable devices. This is particularly important for calls that do not use a phone + * number to identify the caller or called party.</li> + * <li>{@link Connection#setConnectionProperties(int)} - ensure you set + * {@link Connection#PROPERTY_SELF_MANAGED} to identify to the platform that the call is + * handled by your app.</li> + * <li>{@link Connection#setConnectionCapabilities(int)} - if your app supports making calls + * inactive (i.e. holding calls) you should get {@link Connection#CAPABILITY_SUPPORT_HOLD} and + * {@link Connection#CAPABILITY_HOLD} to indicate to the platform that you calls can potentially + * be held for concurrent calling scenarios.</li> + * <li>{@link Connection#setAudioModeIsVoip(boolean)} - set to {@code true} to ensure that the + * platform knows your call is a VoIP call.</li> + * <li>For newly created {@link Connection} instances, do NOT change the state of your call + * using {@link Connection#setActive()}, {@link Connection#setOnHold()} until the call is added + * to Telecom (ie you have returned it via + * {@link ConnectionService#onCreateOutgoingConnection(PhoneAccountHandle, ConnectionRequest)} + * or + * {@link ConnectionService#onCreateIncomingConnection(PhoneAccountHandle, ConnectionRequest)}). + * </li> + * </ul> + * + * <h2>How to Place Outgoing Calls</h2> + * When your app wants to place an outgoing call it calls + * {@link TelecomManager#placeCall(Uri, Bundle)}. You should specify a {@link Uri} to identify + * who the call is being placed to, and specify the {@link PhoneAccountHandle} associated with the + * {@link PhoneAccount} you registered for your app using + * {@link TelecomManager#EXTRA_PHONE_ACCOUNT_HANDLE} in the {@link Bundle} parameter. + * <p> + * Telecom will bind to your app's {@link ConnectionService} implementation and call either: + * <ul> + * <li>{@link ConnectionService#onCreateOutgoingConnection(PhoneAccountHandle, + * ConnectionRequest)} - the {@link ConnectionRequest#getAddress()} will match the address + * you specified when placing the call. You should return a new instance of your app's + * {@link Connection} class to represent the outgoing call.</li> + * <li>{@link ConnectionService#onCreateOutgoingConnectionFailed(PhoneAccountHandle, + * ConnectionRequest)} - your app should not place the call at this time; the call should be + * cancelled and the user informed that the call cannot be placed.</li> + * </ul> + * <p> + * New outgoing calls will start in a {@link Connection#STATE_DIALING} state. This state indicates + * that your app is in the process of connecting the call to the other party. + * <p> + * Once the other party answers the call (or it is set up successfully), your app should call + * {@link Connection#setActive()} to inform Telecom that the call is now active. + * + * <h2>How to Add Incoming Calls</h2> + * When your app receives an incoming call, it should call + * {@link TelecomManager#addNewIncomingCall(PhoneAccountHandle, Bundle)}. Set the + * {@link PhoneAccountHandle} parameter to the {@link PhoneAccountHandle} associated with your + * app's {@link PhoneAccount}. + * <p> + * Telecom will bind to your app's {@link ConnectionService} implementation and call either: + * <ul> + * <li>{@link ConnectionService#onCreateIncomingConnection(PhoneAccountHandle, + * ConnectionRequest)} - You should return a new instance of your app's + * {@link Connection} class to represent the incoming call.</li> + * <li>{@link ConnectionService#onCreateIncomingConnectionFailed(PhoneAccountHandle, + * ConnectionRequest)} - your app should not receive the call at this time; the call should be + * rejected silently; the user may be informed of a missed call.</li> + * </ul> + * <p> + * New incoming calls will start with a {@link Connection#STATE_RINGING} state. This state + * indicates that your app has a new incoming call pending. Telecom will NOT play a ringtone or + * post a notification for your app. It is up to your app to post an incoming call notification + * with an associated ringtone. Telecom will call {@link Connection#onShowIncomingCallUi()} on the + * {@link Connection} when your app can post its incoming call notification. See + * {@link Connection#onShowIncomingCallUi() the docs} for more information on how to post the + * notification. + * <p> + * Your incoming call notification (or full screen UI) will typically have an "answer" and "decline" + * action which the user chooses. When your app receives the "answer" or "decline" + * {@link android.app.PendingIntent}, you should must call either {@link Connection#setActive()} to + * inform Telecom that the call was answered, or + * {@link Connection#setDisconnected(DisconnectCause)} to inform Telecom that the call was rejected. + * If the call was rejected, supply an instance of {@link DisconnectCause} with + * {@link DisconnectCause#REJECTED}, and then call {@link Connection#destroy()}. + * <p> + * In addition to handling requests to answer or decline the call via notification actions, your + * app should also be implement the {@link Connection#onAnswer(int)} and + * {@link Connection#onAnswer()} methods on the {@link Connection}. These will be raised if the + * user answers your call via a Bluetooth device or another device like a wearable or automotive + * calling UX. In response, your app should call {@link Connection#setActive()} to inform Telecom + * that the call was answered. + * <p> + * Additionally, your app should implement {@link Connection#onReject()} to handle requests to + * reject the call which are raised via Bluetooth or other calling surfaces. Your app should call + * {@link Connection#setDisconnected(DisconnectCause)} and supply an instance of + * {@link DisconnectCause} with {@link DisconnectCause#REJECTED} in this case. + * + * <h2>Ending Calls</h2> + * When an ongoing active call (incoming or outgoing) has ended, your app is responsible for + * informing Telecom that the call ended. + * <p> + * Your app calls: + * <ul> + * <li>{@link Connection#setDisconnected(DisconnectCause)} - this informs Telecom that the + * call has terminated. You should provide a new instance of {@link DisconnectCause} with + * either {@link DisconnectCause#LOCAL} or {@link DisconnectCause#REMOTE} to indicate where the + * call disconnection took place. {@link DisconnectCause#LOCAL} indicates that the call + * terminated in your app on the current device (i.e. via user action), where + * {@link DisconnectCause#REMOTE} indicates that the call terminates on the remote device.</li> + * <li>{@link Connection#destroy()} - this informs Telecom that your call instance can be + * cleaned up. You should always call this when you are finished with a call.</li> + * </ul> + * <p> + * Similar to answering incoming calls, requests to disconnect your call may originate from outside + * your app. You can handle these by implementing {@link Connection#onDisconnect()}. Your app + * should call {@link Connection#setDisconnected(DisconnectCause)} with an instance of + * {@link DisconnectCause} and reason {@link DisconnectCause#LOCAL} to indicate to Telecom that your + * app has disconnected the call as requested based on the user's request. + * + * <h2>Holding and Unholding Calls</h2> + * When your app specifies {@link Connection#CAPABILITY_SUPPORT_HOLD} and + * {@link Connection#CAPABILITY_HOLD} on your {@link Connection} instance, it is telling Telecom + * that your calls can be placed into a suspended, or "held" state if required. If your app + * supports holding its calls, it will be possible for the user to switch between calls in your app + * and holdable calls in another app or on the mobile network. If your app does not support + * holding its calls, you may receive a request to disconnect the call from Telecom if the user + * opts to answer an incoming call in another app or on the mobile network; this ensures that the + * user can only be in one call at a time. + * <p> + * Your app is free to change a call between the held and active state using + * {@link Connection#setOnHold()} and {@link Connection#setActive()}. + * <p> + * Your app may receive a request from Telecom to hold or unhold a call via + * {@link Connection#onHold()} and {@link Connection#onUnhold()}. Telecom can ask your app to + * hold or unhold its {@link Connection} either if the user requests this action through another + * calling surface such as Bluetooth, or if the user answers or switches to a call in a different + * app or on the mobile network. + * <p> + * When your app receives an {@link Connection#onHold()} it must call {@link Connection#setOnHold()} + * to inform Telecom that the call has been held successfully. + * <p> + * When your app receives an {@link Connection#onUnhold()} it must call + * {@link Connection#setActive()} to inform Telecom that the call has been resumed successfully. */ public abstract class ConnectionService extends Service { /** @@ -164,6 +389,9 @@ public abstract class ConnectionService extends Service { private static final String SESSION_CREATE_CONF = "CS.crConf"; private static final String SESSION_CREATE_CONF_COMPLETE = "CS.crConfC"; private static final String SESSION_CREATE_CONF_FAILED = "CS.crConfF"; + private static final String SESSION_CALL_ENDPOINT_CHANGED = "CS.oCEC"; + private static final String SESSION_AVAILABLE_CALL_ENDPOINTS_CHANGED = "CS.oACEC"; + private static final String SESSION_MUTE_STATE_CHANGED = "CS.oMSC"; private static final int MSG_ADD_CONNECTION_SERVICE_ADAPTER = 1; private static final int MSG_CREATE_CONNECTION = 2; @@ -208,6 +436,9 @@ public abstract class ConnectionService extends Service { private static final int MSG_ON_CALL_FILTERING_COMPLETED = 42; private static final int MSG_ON_USING_ALTERNATIVE_UI = 43; private static final int MSG_ON_TRACKED_BY_NON_UI_SERVICE = 44; + private static final int MSG_ON_CALL_ENDPOINT_CHANGED = 45; + private static final int MSG_ON_AVAILABLE_CALL_ENDPOINTS_CHANGED = 46; + private static final int MSG_ON_MUTE_STATE_CHANGED = 47; private static Connection sNullConnection; @@ -592,6 +823,51 @@ public abstract class ConnectionService extends Service { } @Override + public void onCallEndpointChanged(String callId, CallEndpoint callEndpoint, + Session.Info sessionInfo) { + Log.startSession(sessionInfo, SESSION_CALL_ENDPOINT_CHANGED); + try { + SomeArgs args = SomeArgs.obtain(); + args.arg1 = callId; + args.arg2 = callEndpoint; + args.arg3 = Log.createSubsession(); + mHandler.obtainMessage(MSG_ON_CALL_ENDPOINT_CHANGED, args).sendToTarget(); + } finally { + Log.endSession(); + } + } + + @Override + public void onAvailableCallEndpointsChanged(String callId, + List<CallEndpoint> availableCallEndpoints, Session.Info sessionInfo) { + Log.startSession(sessionInfo, SESSION_AVAILABLE_CALL_ENDPOINTS_CHANGED); + try { + SomeArgs args = SomeArgs.obtain(); + args.arg1 = callId; + args.arg2 = availableCallEndpoints; + args.arg3 = Log.createSubsession(); + mHandler.obtainMessage(MSG_ON_AVAILABLE_CALL_ENDPOINTS_CHANGED, args) + .sendToTarget(); + } finally { + Log.endSession(); + } + } + + @Override + public void onMuteStateChanged(String callId, boolean isMuted, Session.Info sessionInfo) { + Log.startSession(sessionInfo, SESSION_MUTE_STATE_CHANGED); + try { + SomeArgs args = SomeArgs.obtain(); + args.arg1 = callId; + args.arg2 = isMuted; + args.arg3 = Log.createSubsession(); + mHandler.obtainMessage(MSG_ON_MUTE_STATE_CHANGED, args).sendToTarget(); + } finally { + Log.endSession(); + } + } + + @Override public void onUsingAlternativeUi(String callId, boolean usingAlternativeUiShowing, Session.Info sessionInfo) { Log.startSession(sessionInfo, SESSION_USING_ALTERNATIVE_UI); @@ -1527,6 +1803,48 @@ public abstract class ConnectionService extends Service { case MSG_CONNECTION_SERVICE_FOCUS_LOST: onConnectionServiceFocusLost(); break; + case MSG_ON_CALL_ENDPOINT_CHANGED: { + SomeArgs args = (SomeArgs) msg.obj; + Log.continueSession((Session) args.arg3, + SESSION_HANDLER + SESSION_CALL_AUDIO_SC); + try { + String callId = (String) args.arg1; + CallEndpoint callEndpoint = (CallEndpoint) args.arg2; + onCallEndpointChanged(callId, callEndpoint); + } finally { + args.recycle(); + Log.endSession(); + } + break; + } + case MSG_ON_AVAILABLE_CALL_ENDPOINTS_CHANGED: { + SomeArgs args = (SomeArgs) msg.obj; + Log.continueSession((Session) args.arg3, + SESSION_HANDLER + SESSION_CALL_AUDIO_SC); + try { + String callId = (String) args.arg1; + List<CallEndpoint> availableCallEndpoints = (List<CallEndpoint>) args.arg2; + onAvailableCallEndpointsChanged(callId, availableCallEndpoints); + } finally { + args.recycle(); + Log.endSession(); + } + break; + } + case MSG_ON_MUTE_STATE_CHANGED: { + SomeArgs args = (SomeArgs) msg.obj; + Log.continueSession((Session) args.arg3, + SESSION_HANDLER + SESSION_CALL_AUDIO_SC); + try { + String callId = (String) args.arg1; + boolean isMuted = (boolean) args.arg2; + onMuteStateChanged(callId, isMuted); + } finally { + args.recycle(); + Log.endSession(); + } + break; + } default: break; } @@ -1916,6 +2234,25 @@ public abstract class ConnectionService extends Service { mAdapter.resetConnectionTime(id); } } + + @Override + public void onEndpointChanged(Connection c, CallEndpoint endpoint, Executor executor, + OutcomeReceiver<Void, CallEndpointException> callback) { + String id = mIdByConnection.get(c); + if (id != null) { + mAdapter.requestCallEndpointChange(id, endpoint, executor, callback); + } + } + + @Override + public void onQueryLocation(Connection c, long timeoutMillis, @NonNull String provider, + @NonNull @CallbackExecutor Executor executor, + @NonNull OutcomeReceiver<Location, QueryLocationException> callback) { + String id = mIdByConnection.get(c); + if (id != null) { + mAdapter.queryLocation(id, timeoutMillis, provider, executor, callback); + } + } }; /** {@inheritDoc} */ @@ -2044,7 +2381,7 @@ public abstract class ConnectionService extends Service { if (isHandover) { PhoneAccountHandle fromPhoneAccountHandle = request.getExtras() != null ? (PhoneAccountHandle) request.getExtras().getParcelable( - TelecomManager.EXTRA_HANDOVER_FROM_PHONE_ACCOUNT) : null; + TelecomManager.EXTRA_HANDOVER_FROM_PHONE_ACCOUNT, android.telecom.PhoneAccountHandle.class) : null; if (!isIncoming) { connection = onCreateOutgoingHandoverConnection(fromPhoneAccountHandle, request); } else { @@ -2313,6 +2650,36 @@ public abstract class ConnectionService extends Service { } } + private void onCallEndpointChanged(String callId, CallEndpoint callEndpoint) { + Log.i(this, "onCallEndpointChanged %s %s", callId, callEndpoint); + if (mConnectionById.containsKey(callId)) { + findConnectionForAction(callId, "onCallEndpointChanged").setCallEndpoint(callEndpoint); + } else { + findConferenceForAction(callId, "onCallEndpointChanged").setCallEndpoint(callEndpoint); + } + } + + private void onAvailableCallEndpointsChanged(String callId, + List<CallEndpoint> availableCallEndpoints) { + Log.i(this, "onAvailableCallEndpointsChanged %s", callId); + if (mConnectionById.containsKey(callId)) { + findConnectionForAction(callId, "onAvailableCallEndpointsChanged") + .setAvailableCallEndpoints(availableCallEndpoints); + } else { + findConferenceForAction(callId, "onAvailableCallEndpointsChanged") + .setAvailableCallEndpoints(availableCallEndpoints); + } + } + + private void onMuteStateChanged(String callId, boolean isMuted) { + Log.i(this, "onMuteStateChanged %s %s", callId, isMuted); + if (mConnectionById.containsKey(callId)) { + findConnectionForAction(callId, "onMuteStateChanged").setMuteState(isMuted); + } else { + findConferenceForAction(callId, "onMuteStateChanged").setMuteState(isMuted); + } + } + private void onUsingAlternativeUi(String callId, boolean isUsingAlternativeUi) { Log.i(this, "onUsingAlternativeUi %s %s", callId, isUsingAlternativeUi); if (mConnectionById.containsKey(callId)) { diff --git a/telecomm/java/android/telecom/ConnectionServiceAdapter.java b/telecomm/java/android/telecom/ConnectionServiceAdapter.java index f8a6cf03934a..a7105d349f26 100644 --- a/telecomm/java/android/telecom/ConnectionServiceAdapter.java +++ b/telecomm/java/android/telecom/ConnectionServiceAdapter.java @@ -16,10 +16,16 @@ package android.telecom; +import android.annotation.CallbackExecutor; +import android.annotation.NonNull; +import android.location.Location; import android.net.Uri; +import android.os.Binder; import android.os.Bundle; import android.os.IBinder.DeathRecipient; +import android.os.OutcomeReceiver; import android.os.RemoteException; +import android.os.ResultReceiver; import com.android.internal.telecom.IConnectionServiceAdapter; import com.android.internal.telecom.RemoteServiceCallback; @@ -29,6 +35,7 @@ import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executor; /** * Provides methods for IConnectionService implementations to interact with the system phone app. @@ -567,6 +574,41 @@ final class ConnectionServiceAdapter implements DeathRecipient { } } + /** + * Sets the call endpoint associated with a {@link Connection}. + * + * @param callId The unique ID of the call. + * @param endpoint The new call endpoint (see {@link CallEndpoint}). + * @param executor The executor of where the callback will execute. + * @param callback The callback to notify the result of the endpoint change. + */ + void requestCallEndpointChange(String callId, CallEndpoint endpoint, Executor executor, + OutcomeReceiver<Void, CallEndpointException> callback) { + Log.v(this, "requestCallEndpointChange"); + for (IConnectionServiceAdapter adapter : mAdapters) { + try { + adapter.requestCallEndpointChange(callId, endpoint, new ResultReceiver(null) { + @Override + protected void onReceiveResult(int resultCode, Bundle result) { + super.onReceiveResult(resultCode, result); + final long identity = Binder.clearCallingIdentity(); + try { + if (resultCode == CallEndpoint.ENDPOINT_OPERATION_SUCCESS) { + executor.execute(() -> callback.onResult(null)); + } else { + executor.execute(() -> callback.onError(result.getParcelable( + CallEndpointException.CHANGE_ERROR, + CallEndpointException.class))); + } + } finally { + Binder.restoreCallingIdentity(identity); + } + }}, Log.getExternalSession()); + } catch (RemoteException ignored) { + Log.d(this, "Remote exception calling requestCallEndpointChange"); + } + } + } /** * Informs Telecom of a connection level event. @@ -709,4 +751,45 @@ final class ConnectionServiceAdapter implements DeathRecipient { } } } + + /** + * Query location information. + * Only SIM call managers can call this method for Connections representing Emergency calls. + * If the previous request is not completed, the new request will be rejected. + * + * @param timeoutMillis long: Timeout in millis waiting for query response. + * @param provider String: the location provider name, This value cannot be null. + * @param executor The executor of where the callback will execute. + * @param callback The callback to notify the result of queryLocation. + */ + void queryLocation(String callId, long timeoutMillis, @NonNull String provider, + @NonNull @CallbackExecutor Executor executor, + @NonNull OutcomeReceiver<Location, QueryLocationException> callback) { + Log.v(this, "queryLocation: %s %d", callId, timeoutMillis); + for (IConnectionServiceAdapter adapter : mAdapters) { + try { + adapter.queryLocation(callId, timeoutMillis, provider, + new ResultReceiver(null) { + @Override + protected void onReceiveResult(int resultCode, Bundle result) { + super.onReceiveResult(resultCode, result); + + if (resultCode == 1 /* success */) { + executor.execute(() -> callback.onResult(result.getParcelable( + Connection.EXTRA_KEY_QUERY_LOCATION, Location.class))); + } else { + executor.execute(() -> callback.onError(result.getParcelable( + QueryLocationException.QUERY_LOCATION_ERROR, + QueryLocationException.class))); + } + } + }, + Log.getExternalSession()); + } catch (RemoteException e) { + Log.d(this, "queryLocation: Exception e : " + e); + executor.execute(() -> callback.onError(new QueryLocationException( + e.getMessage(), QueryLocationException.ERROR_SERVICE_UNAVAILABLE))); + } + } + } } diff --git a/telecomm/java/android/telecom/ConnectionServiceAdapterServant.java b/telecomm/java/android/telecom/ConnectionServiceAdapterServant.java index 6c1ea322e66e..8a59020fc580 100644 --- a/telecomm/java/android/telecom/ConnectionServiceAdapterServant.java +++ b/telecomm/java/android/telecom/ConnectionServiceAdapterServant.java @@ -21,6 +21,7 @@ import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.os.RemoteException; +import android.os.ResultReceiver; import android.telecom.Logging.Session; import com.android.internal.os.SomeArgs; @@ -77,6 +78,7 @@ final class ConnectionServiceAdapterServant { private static final int MSG_SET_CONFERENCE_STATE = 36; private static final int MSG_HANDLE_CREATE_CONFERENCE_COMPLETE = 37; private static final int MSG_SET_CALL_DIRECTION = 38; + private static final int MSG_QUERY_LOCATION = 39; private final IConnectionServiceAdapter mDelegate; @@ -372,6 +374,18 @@ final class ConnectionServiceAdapterServant { } finally { args.recycle(); } + break; + } + case MSG_QUERY_LOCATION: { + SomeArgs args = (SomeArgs) msg.obj; + try { + mDelegate.queryLocation((String) args.arg1, (long) args.arg2, + (String) args.arg3, (ResultReceiver) args.arg4, + (Session.Info) args.arg5); + } finally { + args.recycle(); + } + break; } } } @@ -692,6 +706,24 @@ final class ConnectionServiceAdapterServant { args.arg2 = sessionInfo; mHandler.obtainMessage(MSG_SET_CALL_DIRECTION, args).sendToTarget(); } + + @Override + public void requestCallEndpointChange(String callId, CallEndpoint endpoint, + ResultReceiver callback, Session.Info sessionInfo) { + // Do nothing + } + + @Override + public void queryLocation(String callId, long timeoutMillis, String provider, + ResultReceiver callback, Session.Info sessionInfo) { + SomeArgs args = SomeArgs.obtain(); + args.arg1 = callId; + args.arg2 = timeoutMillis; + args.arg3 = provider; + args.arg4 = callback; + args.arg5 = sessionInfo; + mHandler.obtainMessage(MSG_QUERY_LOCATION, args).sendToTarget(); + } }; public ConnectionServiceAdapterServant(IConnectionServiceAdapter delegate) { diff --git a/telecomm/java/android/telecom/DisconnectCause.java b/telecomm/java/android/telecom/DisconnectCause.java index b003f59d5e81..331caa1bad7a 100644 --- a/telecomm/java/android/telecom/DisconnectCause.java +++ b/telecomm/java/android/telecom/DisconnectCause.java @@ -43,8 +43,8 @@ public final class DisconnectCause implements Parcelable { /** Disconnected because of a local user-initiated action, such as hanging up. */ public static final int LOCAL = TelecomProtoEnums.LOCAL; // = 2 /** - * Disconnected because of a remote user-initiated action, such as the other party hanging up - * up. + * Disconnected because the remote party hung up an ongoing call, or because an outgoing call + * was not answered by the remote party. */ public static final int REMOTE = TelecomProtoEnums.REMOTE; // = 3 /** Disconnected because it has been canceled. */ diff --git a/telecomm/java/android/telecom/InCallAdapter.java b/telecomm/java/android/telecom/InCallAdapter.java index ab35affe9099..77701457484a 100755 --- a/telecomm/java/android/telecom/InCallAdapter.java +++ b/telecomm/java/android/telecom/InCallAdapter.java @@ -19,12 +19,16 @@ package android.telecom; import android.annotation.NonNull; import android.bluetooth.BluetoothDevice; import android.net.Uri; +import android.os.Binder; import android.os.Bundle; +import android.os.OutcomeReceiver; import android.os.RemoteException; +import android.os.ResultReceiver; import com.android.internal.telecom.IInCallAdapter; import java.util.List; +import java.util.concurrent.Executor; /** * Receives commands from {@link InCallService} implementations which should be executed by @@ -227,6 +231,39 @@ public final class InCallAdapter { } /** + * Request audio routing to a specific CallEndpoint.. See {@link CallEndpoint}. + * + * @param endpoint The call endpoint to use. + * @param executor The executor of where the callback will execute. + * @param callback The callback to notify the result of the endpoint change. + */ + public void requestCallEndpointChange(CallEndpoint endpoint, Executor executor, + OutcomeReceiver<Void, CallEndpointException> callback) { + try { + mAdapter.requestCallEndpointChange(endpoint, new ResultReceiver(null) { + @Override + protected void onReceiveResult(int resultCode, Bundle result) { + super.onReceiveResult(resultCode, result); + final long identity = Binder.clearCallingIdentity(); + try { + if (resultCode == CallEndpoint.ENDPOINT_OPERATION_SUCCESS) { + executor.execute(() -> callback.onResult(null)); + } else { + executor.execute(() -> callback.onError( + result.getParcelable(CallEndpointException.CHANGE_ERROR, + CallEndpointException.class))); + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } + }); + } catch (RemoteException e) { + Log.d(this, "Remote exception calling requestCallEndpointChange"); + } + } + + /** * Instructs Telecom to play a dual-tone multi-frequency signaling (DTMF) tone in a call. * * Any other currently playing DTMF tone in the specified call is immediately stopped. diff --git a/telecomm/java/android/telecom/InCallService.java b/telecomm/java/android/telecom/InCallService.java index 64a86db38396..13a045858ab1 100644 --- a/telecomm/java/android/telecom/InCallService.java +++ b/telecomm/java/android/telecom/InCallService.java @@ -16,6 +16,7 @@ package android.telecom; +import android.annotation.CallbackExecutor; import android.annotation.NonNull; import android.annotation.SdkConstant; import android.annotation.SystemApi; @@ -31,6 +32,7 @@ import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.os.Message; +import android.os.OutcomeReceiver; import android.view.Surface; import com.android.internal.os.SomeArgs; @@ -39,6 +41,8 @@ import com.android.internal.telecom.IInCallService; import java.util.Collections; import java.util.List; +import java.util.Objects; +import java.util.concurrent.Executor; /** * This service is implemented by an app that wishes to provide functionality for managing @@ -269,6 +273,11 @@ public abstract class InCallService extends Service { private static final int MSG_ON_RTT_INITIATION_FAILURE = 11; private static final int MSG_ON_HANDOVER_FAILED = 12; private static final int MSG_ON_HANDOVER_COMPLETE = 13; + private static final int MSG_ON_CALL_ENDPOINT_CHANGED = 14; + private static final int MSG_ON_AVAILABLE_CALL_ENDPOINTS_CHANGED = 15; + private static final int MSG_ON_MUTE_STATE_CHANGED = 16; + + private CallEndpoint mCallEndpoint; /** Default Handler used to consolidate binder method calls onto a single thread. */ private final Handler mHandler = new Handler(Looper.getMainLooper()) { @@ -350,6 +359,23 @@ public abstract class InCallService extends Service { mPhone.internalOnHandoverComplete(callId); break; } + case MSG_ON_CALL_ENDPOINT_CHANGED: { + CallEndpoint endpoint = (CallEndpoint) msg.obj; + if (!Objects.equals(mCallEndpoint, endpoint)) { + mCallEndpoint = endpoint; + InCallService.this.onCallEndpointChanged(mCallEndpoint); + } + break; + } + case MSG_ON_AVAILABLE_CALL_ENDPOINTS_CHANGED: { + InCallService.this.onAvailableCallEndpointsChanged( + (List<CallEndpoint>) msg.obj); + break; + } + case MSG_ON_MUTE_STATE_CHANGED: { + InCallService.this.onMuteStateChanged((boolean) msg.obj); + break; + } default: break; } @@ -392,6 +418,22 @@ public abstract class InCallService extends Service { } @Override + public void onCallEndpointChanged(CallEndpoint callEndpoint) { + mHandler.obtainMessage(MSG_ON_CALL_ENDPOINT_CHANGED, callEndpoint).sendToTarget(); + } + + @Override + public void onAvailableCallEndpointsChanged(List<CallEndpoint> availableEndpoints) { + mHandler.obtainMessage(MSG_ON_AVAILABLE_CALL_ENDPOINTS_CHANGED, availableEndpoints) + .sendToTarget(); + } + + @Override + public void onMuteStateChanged(boolean isMuted) { + mHandler.obtainMessage(MSG_ON_MUTE_STATE_CHANGED, isMuted).sendToTarget(); + } + + @Override public void bringToForeground(boolean showDialpad) { mHandler.obtainMessage(MSG_BRING_TO_FOREGROUND, showDialpad ? 1 : 0, 0).sendToTarget(); } @@ -559,7 +601,11 @@ public abstract class InCallService extends Service { * * @return An object encapsulating the audio state. Returns null if the service is not * fully initialized. + * @deprecated Use {@link #getCurrentCallEndpoint()}, + * {@link #onAvailableCallEndpointsChanged(List)} and + * {@link #onMuteStateChanged(boolean)} instead. */ + @Deprecated public final CallAudioState getCallAudioState() { return mPhone == null ? null : mPhone.getCallAudioState(); } @@ -581,7 +627,10 @@ public abstract class InCallService extends Service { * be change to the {@link #getCallAudioState()}. * * @param route The audio route to use. + * @deprecated Use {@link #requestCallEndpointChange(CallEndpoint, Executor, OutcomeReceiver)} + * instead. */ + @Deprecated public final void setAudioRoute(int route) { if (mPhone != null) { mPhone.setAudioRoute(route); @@ -596,7 +645,10 @@ public abstract class InCallService extends Service { * {@link CallAudioState#getSupportedBluetoothDevices()} * * @param bluetoothDevice The bluetooth device to connect to. + * @deprecated Use {@link #requestCallEndpointChange(CallEndpoint, Executor, OutcomeReceiver)} + * instead. */ + @Deprecated public final void requestBluetoothAudio(@NonNull BluetoothDevice bluetoothDevice) { if (mPhone != null) { mPhone.requestBluetoothAudio(bluetoothDevice.getAddress()); @@ -604,6 +656,34 @@ public abstract class InCallService extends Service { } /** + * Request audio routing to a specific CallEndpoint. Clients should not define their own + * CallEndpoint when requesting a change. Instead, the new endpoint should be one of the valid + * endpoints provided by {@link #onAvailableCallEndpointsChanged(List)}. + * When this request is honored, there will be change to the {@link #getCurrentCallEndpoint()}. + * + * @param endpoint The call endpoint to use. + * @param executor The executor of where the callback will execute. + * @param callback The callback to notify the result of the endpoint change. + */ + public final void requestCallEndpointChange(@NonNull CallEndpoint endpoint, + @NonNull @CallbackExecutor Executor executor, + @NonNull OutcomeReceiver<Void, CallEndpointException> callback) { + if (mPhone != null) { + mPhone.requestCallEndpointChange(endpoint, executor, callback); + } + } + + /** + * Obtains the current CallEndpoint. + * + * @return An object encapsulating the CallEndpoint. + */ + @NonNull + public final CallEndpoint getCurrentCallEndpoint() { + return mCallEndpoint; + } + + /** * Invoked when the {@code Phone} has been created. This is a signal to the in-call experience * to start displaying in-call information to the user. Each instance of {@code InCallService} * will have only one {@code Phone}, and this method will be called exactly once in the lifetime @@ -648,11 +728,39 @@ public abstract class InCallService extends Service { * Called when the audio state changes. * * @param audioState The new {@link CallAudioState}. + * @deprecated Use {@link #onCallEndpointChanged(CallEndpoint)}, + * {@link #onAvailableCallEndpointsChanged(List)} and + * {@link #onMuteStateChanged(boolean)} instead. */ + @Deprecated public void onCallAudioStateChanged(CallAudioState audioState) { } /** + * Called when the current CallEndpoint changes. + * + * @param callEndpoint The current CallEndpoint {@link CallEndpoint}. + */ + public void onCallEndpointChanged(@NonNull CallEndpoint callEndpoint) { + } + + /** + * Called when the available CallEndpoint changes. + * + * @param availableEndpoints The set of available CallEndpoint {@link CallEndpoint}. + */ + public void onAvailableCallEndpointsChanged(@NonNull List<CallEndpoint> availableEndpoints) { + } + + /** + * Called when the mute state changes. + * + * @param isMuted The current mute state. + */ + public void onMuteStateChanged(boolean isMuted) { + } + + /** * Called to bring the in-call screen to the foreground. The in-call experience should * respond immediately by coming to the foreground to inform the user of the state of * ongoing {@code Call}s. diff --git a/telecomm/java/android/telecom/Log.java b/telecomm/java/android/telecom/Log.java index 884dcf2dfbad..a34094ce6452 100644 --- a/telecomm/java/android/telecom/Log.java +++ b/telecomm/java/android/telecom/Log.java @@ -69,6 +69,7 @@ public class Log { private static final Object sSingletonSync = new Object(); private static EventManager sEventManager; private static SessionManager sSessionManager; + private static Object sLock = null; /** * Tracks whether user-activated extended logging is enabled. @@ -388,6 +389,19 @@ public class Log { } /** + * Sets the main telecom sync lock used within Telecom. This is used when building log messages + * so that we can identify places in the code where we are doing something outside of the + * Telecom lock. + * @param lock The lock. + */ + public static void setLock(Object lock) { + // Don't do lock monitoring on user builds. + if (!Build.IS_USER) { + sLock = lock; + } + } + + /** * If user enabled extended logging is enabled and the time limit has passed, disables the * extended logging. */ @@ -512,7 +526,10 @@ public class Log { args.length); msg = format + " (An error occurred while formatting the message.)"; } - return String.format(Locale.US, "%s: %s%s", prefix, msg, sessionPostfix); + // If a lock was set, check if this thread holds that lock and output an emoji that lets + // the developer know whether a log message came from within the Telecom lock or not. + String isLocked = sLock != null ? (Thread.holdsLock(sLock) ? "\uD83D\uDD12" : "❗") : ""; + return String.format(Locale.US, "%s: %s%s%s", prefix, msg, sessionPostfix, isLocked); } /** diff --git a/telecomm/java/android/telecom/Logging/EventManager.java b/telecomm/java/android/telecom/Logging/EventManager.java index 1342038c6477..a74c0bb99549 100644 --- a/telecomm/java/android/telecom/Logging/EventManager.java +++ b/telecomm/java/android/telecom/Logging/EventManager.java @@ -180,7 +180,7 @@ public class EventManager { } } - private final List<Event> mEvents = Collections.synchronizedList(new LinkedList<>()); + private final List<Event> mEvents = Collections.synchronizedList(new ArrayList<>()); private final Loggable mRecordEntry; public EventRecord(Loggable recordEntry) { @@ -197,7 +197,7 @@ public class EventManager { } public List<Event> getEvents() { - return new LinkedList<>(mEvents); + return new ArrayList<>(mEvents); } public List<EventTiming> extractEventTimings() { @@ -205,7 +205,7 @@ public class EventManager { return Collections.emptyList(); } - LinkedList<EventTiming> result = new LinkedList<>(); + ArrayList<EventTiming> result = new ArrayList<>(); Map<String, PendingResponse> pendingResponses = new HashMap<>(); synchronized (mEvents) { for (Event event : mEvents) { diff --git a/telecomm/java/android/telecom/ParcelableCall.java b/telecomm/java/android/telecom/ParcelableCall.java index f412a1825e2a..6a1318982e77 100644 --- a/telecomm/java/android/telecom/ParcelableCall.java +++ b/telecomm/java/android/telecom/ParcelableCall.java @@ -69,6 +69,7 @@ public final class ParcelableCall implements Parcelable { private int mCallerNumberVerificationStatus; private String mContactDisplayName; private String mActiveChildCallId; + private Uri mContactPhotoUri; public ParcelableCallBuilder setId(String id) { mId = id; @@ -224,6 +225,11 @@ public final class ParcelableCall implements Parcelable { return this; } + public ParcelableCallBuilder setContactPhotoUri(Uri contactPhotoUri) { + mContactPhotoUri = contactPhotoUri; + return this; + } + public ParcelableCall createParcelableCall() { return new ParcelableCall( mId, @@ -255,7 +261,8 @@ public final class ParcelableCall implements Parcelable { mCallDirection, mCallerNumberVerificationStatus, mContactDisplayName, - mActiveChildCallId); + mActiveChildCallId, + mContactPhotoUri); } public static ParcelableCallBuilder fromParcelableCall(ParcelableCall parcelableCall) { @@ -292,6 +299,7 @@ public final class ParcelableCall implements Parcelable { parcelableCall.mCallerNumberVerificationStatus; newBuilder.mContactDisplayName = parcelableCall.mContactDisplayName; newBuilder.mActiveChildCallId = parcelableCall.mActiveChildCallId; + newBuilder.mContactPhotoUri = parcelableCall.mContactPhotoUri; return newBuilder; } } @@ -327,6 +335,7 @@ public final class ParcelableCall implements Parcelable { private final int mCallerNumberVerificationStatus; private final String mContactDisplayName; private final String mActiveChildCallId; // Only valid for CDMA conferences + private final Uri mContactPhotoUri; public ParcelableCall( String id, @@ -358,7 +367,8 @@ public final class ParcelableCall implements Parcelable { int callDirection, int callerNumberVerificationStatus, String contactDisplayName, - String activeChildCallId + String activeChildCallId, + Uri contactPhotoUri ) { mId = id; mState = state; @@ -390,6 +400,7 @@ public final class ParcelableCall implements Parcelable { mCallerNumberVerificationStatus = callerNumberVerificationStatus; mContactDisplayName = contactDisplayName; mActiveChildCallId = activeChildCallId; + mContactPhotoUri = contactPhotoUri; } /** The unique ID of the call. */ @@ -607,6 +618,14 @@ public final class ParcelableCall implements Parcelable { } /** + * @return the caller photo URI. + */ + public @Nullable Uri getContactPhotoUri() { + return mContactPhotoUri; + } + + + /** * @return On a CDMA conference with two participants, returns the ID of the child call that's * currently active. */ @@ -655,6 +674,7 @@ public final class ParcelableCall implements Parcelable { int callerNumberVerificationStatus = source.readInt(); String contactDisplayName = source.readString(); String activeChildCallId = source.readString(); + Uri contactPhotoUri = source.readParcelable(classLoader, Uri.class); return new ParcelableCallBuilder() .setId(id) .setState(state) @@ -686,6 +706,7 @@ public final class ParcelableCall implements Parcelable { .setCallerNumberVerificationStatus(callerNumberVerificationStatus) .setContactDisplayName(contactDisplayName) .setActiveChildCallId(activeChildCallId) + .setContactPhotoUri(contactPhotoUri) .createParcelableCall(); } @@ -735,6 +756,7 @@ public final class ParcelableCall implements Parcelable { destination.writeInt(mCallerNumberVerificationStatus); destination.writeString(mContactDisplayName); destination.writeString(mActiveChildCallId); + destination.writeParcelable(mContactPhotoUri, 0); } @Override diff --git a/telecomm/java/android/telecom/ParcelableCallAnalytics.java b/telecomm/java/android/telecom/ParcelableCallAnalytics.java index b8ad9e2fbe6c..a69dfb0b255f 100644 --- a/telecomm/java/android/telecom/ParcelableCallAnalytics.java +++ b/telecomm/java/android/telecom/ParcelableCallAnalytics.java @@ -111,6 +111,8 @@ public class ParcelableCallAnalytics implements Parcelable { public static final int FILTERING_INITIATED = 106; public static final int FILTERING_COMPLETED = 107; public static final int FILTERING_TIMED_OUT = 108; + public static final int DND_CHECK_INITIATED = 109; + public static final int DND_CHECK_COMPLETED = 110; public static final int SKIP_RINGING = 200; public static final int SILENCE = 201; @@ -195,6 +197,7 @@ public class ParcelableCallAnalytics implements Parcelable { public static final int BLOCK_CHECK_FINISHED_TIMING = 9; public static final int FILTERING_COMPLETED_TIMING = 10; public static final int FILTERING_TIMED_OUT_TIMING = 11; + public static final int DND_PRE_CALL_PRE_CHECK_TIMING = 12; /** {@hide} */ public static final int START_CONNECTION_TO_REQUEST_DISCONNECT_TIMING = 12; @@ -359,7 +362,7 @@ public class ParcelableCallAnalytics implements Parcelable { eventTimings = new ArrayList<>(); in.readTypedList(eventTimings, EventTiming.CREATOR); isVideoCall = readByteAsBoolean(in); - videoEvents = new LinkedList<>(); + videoEvents = new ArrayList<>(); in.readTypedList(videoEvents, VideoEvent.CREATOR); callSource = in.readInt(); } diff --git a/telecomm/java/android/telecom/ParcelableConference.java b/telecomm/java/android/telecom/ParcelableConference.java index e57c833e930e..6dcfa6d56ef3 100644 --- a/telecomm/java/android/telecom/ParcelableConference.java +++ b/telecomm/java/android/telecom/ParcelableConference.java @@ -21,12 +21,12 @@ import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; +import com.android.internal.telecom.IVideoProvider; + import java.util.ArrayList; import java.util.Collections; import java.util.List; -import com.android.internal.telecom.IVideoProvider; - /** * A parcelable representation of a conference connection. * @hide @@ -287,6 +287,14 @@ public final class ParcelableConference implements Parcelable { return mCallDirection; } + public String getCallerDisplayName() { + return mCallerDisplayName; + } + + public int getCallerDisplayNamePresentation() { + return mCallerDisplayNamePresentation; + } + public static final @android.annotation.NonNull Parcelable.Creator<ParcelableConference> CREATOR = new Parcelable.Creator<ParcelableConference> () { @Override diff --git a/telecomm/java/android/telecom/Phone.java b/telecomm/java/android/telecom/Phone.java index bc0a14667307..95a8e16ace3d 100644 --- a/telecomm/java/android/telecom/Phone.java +++ b/telecomm/java/android/telecom/Phone.java @@ -16,11 +16,14 @@ package android.telecom; +import android.annotation.CallbackExecutor; +import android.annotation.NonNull; import android.annotation.SystemApi; import android.bluetooth.BluetoothDevice; import android.compat.annotation.UnsupportedAppUsage; import android.os.Build; import android.os.Bundle; +import android.os.OutcomeReceiver; import android.util.ArrayMap; import com.android.internal.annotations.GuardedBy; @@ -30,6 +33,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.Executor; /** * A unified virtual device providing a means of voice (and other) communication on a device. @@ -378,6 +382,21 @@ public final class Phone { } /** + * Request audio routing to a specific CallEndpoint. When this request is honored, there will + * be change to the {@link #getCurrentCallEndpoint()}. + * + * @param endpoint The call endpoint to use. + * @param executor The executor of where the callback will execute. + * @param callback The callback to notify the result of the endpoint change. + * @hide + */ + public void requestCallEndpointChange(@NonNull CallEndpoint endpoint, + @NonNull @CallbackExecutor Executor executor, + @NonNull OutcomeReceiver<Void, CallEndpointException> callback) { + mInCallAdapter.requestCallEndpointChange(endpoint, executor, callback); + } + + /** * Turns the proximity sensor on. When this request is made, the proximity sensor will * become active, and the touch screen and display will be turned off when the user's face * is detected to be in close proximity to the screen. This operation is a no-op on devices diff --git a/telecomm/java/android/telecom/PhoneAccount.java b/telecomm/java/android/telecom/PhoneAccount.java index 7a53447c1eee..94c737d61b0a 100644 --- a/telecomm/java/android/telecom/PhoneAccount.java +++ b/telecomm/java/android/telecom/PhoneAccount.java @@ -418,7 +418,34 @@ 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; + + /** + * 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. @@ -513,6 +540,11 @@ public final class PhoneAccount implements Parcelable { /** * Creates a builder with the specified {@link PhoneAccountHandle} and label. + * <p> + * Note: each CharSequence or String field is limited to 256 characters. This check is + * enforced when registering the PhoneAccount via + * {@link TelecomManager#registerPhoneAccount(PhoneAccount)} and will cause an + * {@link IllegalArgumentException} to be thrown if the character field limit is over 256. */ public Builder(PhoneAccountHandle accountHandle, CharSequence label) { this.mAccountHandle = accountHandle; @@ -543,6 +575,11 @@ public final class PhoneAccount implements Parcelable { /** * Sets the label. See {@link PhoneAccount#getLabel()}. + * <p> + * Note: Each CharSequence or String field is limited to 256 characters. This check is + * enforced when registering the PhoneAccount via + * {@link TelecomManager#registerPhoneAccount(PhoneAccount)} and will cause an + * {@link IllegalArgumentException} to be thrown if the character field limit is over 256. * * @param label The label of the phone account. * @return The builder. @@ -618,6 +655,11 @@ public final class PhoneAccount implements Parcelable { /** * Sets the short description. See {@link PhoneAccount#getShortDescription}. + * <p> + * Note: Each CharSequence or String field is limited to 256 characters. This check is + * enforced when registering the PhoneAccount via + * {@link TelecomManager#registerPhoneAccount(PhoneAccount)} and will cause an + * {@link IllegalArgumentException} to be thrown if the character field limit is over 256. * * @param value The short description. * @return The builder. @@ -672,6 +714,13 @@ public final class PhoneAccount implements Parcelable { * <p> * {@code PhoneAccount}s only support extra values of type: {@link String}, {@link Integer}, * and {@link Boolean}. Extras which are not of these types are ignored. + * <p> + * Note: Each Bundle (Key, Value) String field is limited to 256 characters. Additionally, + * the bundle is limited to 100 (Key, Value) pairs total. This check is + * enforced when registering the PhoneAccount via + * {@link TelecomManager#registerPhoneAccount(PhoneAccount)} and will cause an + * {@link IllegalArgumentException} to be thrown if the character field limit is over 256 + * or more than 100 (Key, Value) pairs are in the Bundle. * * @param extras * @return @@ -703,6 +752,11 @@ public final class PhoneAccount implements Parcelable { * <p> * Note: This is an API specific to the Telephony stack; the group Id will be ignored for * callers not holding the correct permission. + * <p> + * Additionally, each CharSequence or String field is limited to 256 characters. + * This check is enforced when registering the PhoneAccount via + * {@link TelecomManager#registerPhoneAccount(PhoneAccount)} and will cause an + * {@link IllegalArgumentException} to be thrown if the character field limit is over 256. * * @param groupId The group Id of the {@link PhoneAccount} that will replace any other * registered {@link PhoneAccount} in Telecom with the same Group Id. @@ -1173,6 +1227,12 @@ public final class PhoneAccount implements Parcelable { if (hasCapabilities(CAPABILITY_VOICE_CALLING_AVAILABLE)) { sb.append("Voice "); } + if (hasCapabilities(CAPABILITY_SUPPORTS_TRANSACTIONAL_OPERATIONS)) { + sb.append("TransactOps "); + } + if (hasCapabilities(CAPABILITY_SUPPORTS_CALL_STREAMING)) { + sb.append("Stream "); + } return sb.toString(); } diff --git a/telecomm/java/android/telecom/PhoneAccountHandle.java b/telecomm/java/android/telecom/PhoneAccountHandle.java index ec94f8a1829f..e5db8cfa0989 100644 --- a/telecomm/java/android/telecom/PhoneAccountHandle.java +++ b/telecomm/java/android/telecom/PhoneAccountHandle.java @@ -70,6 +70,12 @@ public final class PhoneAccountHandle implements Parcelable { * ID provided does not expose personally identifying information. A * {@link ConnectionService} should use an opaque token as the * {@link PhoneAccountHandle} identifier. + * <p> + * Note: Each String field is limited to 256 characters. This check is enforced when + * registering the PhoneAccount via + * {@link TelecomManager#registerPhoneAccount(PhoneAccount)} and will cause an + * {@link IllegalArgumentException} to be thrown if the character field limit is + * over 256. */ public PhoneAccountHandle( @NonNull ComponentName componentName, @@ -88,6 +94,13 @@ public final class PhoneAccountHandle implements Parcelable { * {@link ConnectionService} should use an opaque token as the * {@link PhoneAccountHandle} identifier. * @param userHandle The {@link UserHandle} associated with this {@link PhoneAccountHandle}. + * + * <p> + * Note: Each String field is limited to 256 characters. This check is enforced when + * registering the PhoneAccount via + * {@link TelecomManager#registerPhoneAccount(PhoneAccount)} and will cause an + * {@link IllegalArgumentException} to be thrown if the character field limit is + * over 256. */ public PhoneAccountHandle( @NonNull ComponentName componentName, diff --git a/telecomm/java/android/telecom/QueryLocationException.aidl b/telecomm/java/android/telecom/QueryLocationException.aidl new file mode 100644 index 000000000000..56ac4126cac4 --- /dev/null +++ b/telecomm/java/android/telecom/QueryLocationException.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 QueryLocationException;
\ No newline at end of file diff --git a/telecomm/java/android/telecom/QueryLocationException.java b/telecomm/java/android/telecom/QueryLocationException.java new file mode 100644 index 000000000000..fd90d1ec3572 --- /dev/null +++ b/telecomm/java/android/telecom/QueryLocationException.java @@ -0,0 +1,129 @@ +/* + * 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.OutcomeReceiver; +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; +import java.util.concurrent.Executor; + +/** + * This class represents a set of exceptions that can occur when requesting a + * {@link Connection#queryLocationForEmergency(long, String, Executor, OutcomeReceiver)} + */ +public final class QueryLocationException extends RuntimeException implements Parcelable { + /** @hide */ + public static final String QUERY_LOCATION_ERROR = "QueryLocationErrorKey"; + + /** + * The operation was not completed on time. + */ + public static final int ERROR_REQUEST_TIME_OUT = 1; + /** + * The operation was rejected due to an existing request. + */ + public static final int ERROR_PREVIOUS_REQUEST_EXISTS = 2; + /** + * The operation has failed because it is not permitted. + */ + public static final int ERROR_NOT_PERMITTED = 3; + /** + * The operation has failed due to a location query being requested for a non-emergency + * connection. + */ + public static final int ERROR_NOT_ALLOWED_FOR_NON_EMERGENCY_CONNECTIONS = 4; + /** + * The operation has failed due to the service is not available. + */ + public static final int ERROR_SERVICE_UNAVAILABLE = 5; + /** + * The operation has failed due to an unknown or unspecified error. + */ + public static final int ERROR_UNSPECIFIED = 6; + + private int mCode = ERROR_UNSPECIFIED; + 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 QueryLocationException objects for deserialized Parcels. + */ + public static final + @android.annotation.NonNull Parcelable.Creator<QueryLocationException> CREATOR = + new Parcelable.Creator<>() { + @Override + public QueryLocationException createFromParcel(Parcel source) { + return new QueryLocationException(source.readString8(), source.readInt()); + } + @Override + public QueryLocationException[] newArray(int size) { + return new QueryLocationException[size]; + } + }; + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({ERROR_REQUEST_TIME_OUT, + ERROR_PREVIOUS_REQUEST_EXISTS, + ERROR_NOT_PERMITTED, + ERROR_NOT_ALLOWED_FOR_NON_EMERGENCY_CONNECTIONS, + ERROR_SERVICE_UNAVAILABLE, + ERROR_UNSPECIFIED}) + public @interface QueryLocationErrorCode {} + public QueryLocationException(@Nullable String message) { + super(getMessage(message, ERROR_UNSPECIFIED)); + mMessage = message; + } + public QueryLocationException(@Nullable String message, @QueryLocationErrorCode int code) { + super(getMessage(message, code)); + mCode = code; + mMessage = message; + } + public QueryLocationException( + @Nullable String message, @QueryLocationErrorCode int code, @Nullable Throwable cause) { + super(getMessage(message, code), cause); + mCode = code; + mMessage = message; + } + public @QueryLocationErrorCode 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; + } + } +} diff --git a/telecomm/java/android/telecom/RemoteConnection.java b/telecomm/java/android/telecom/RemoteConnection.java index 7a6fddb6f029..8b2b51e29c91 100644 --- a/telecomm/java/android/telecom/RemoteConnection.java +++ b/telecomm/java/android/telecom/RemoteConnection.java @@ -36,12 +36,10 @@ import com.android.internal.telecom.IVideoCallback; import com.android.internal.telecom.IVideoProvider; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; -import java.util.stream.Collectors; /** * A connection provided to a {@link ConnectionService} by another {@code ConnectionService} diff --git a/telecomm/java/android/telecom/RemoteConnectionService.java b/telecomm/java/android/telecom/RemoteConnectionService.java index efe35d21c003..2fc6a22261b6 100644 --- a/telecomm/java/android/telecom/RemoteConnectionService.java +++ b/telecomm/java/android/telecom/RemoteConnectionService.java @@ -21,6 +21,7 @@ import android.os.Bundle; import android.os.IBinder; import android.os.IBinder.DeathRecipient; import android.os.RemoteException; +import android.os.ResultReceiver; import android.telecom.Logging.Session; import com.android.internal.telecom.IConnectionService; @@ -510,6 +511,18 @@ final class RemoteConnectionService { public void setCallDirection(String callId, int direction, Session.Info sessionInfo) { // Do nothing } + + @Override + public void requestCallEndpointChange(String callId, CallEndpoint endpoint, + ResultReceiver callback, Session.Info sessionInfo) { + // Do nothing + } + + @Override + public void queryLocation(String callId, long timeoutMillis, String provider, + ResultReceiver callback, Session.Info sessionInfo) { + // Do nothing + } }; private final ConnectionServiceAdapterServant mServant = diff --git a/telecomm/java/android/telecom/StatusHints.java b/telecomm/java/android/telecom/StatusHints.java index 2faecc2e3468..5f0c8d729e74 100644 --- a/telecomm/java/android/telecom/StatusHints.java +++ b/telecomm/java/android/telecom/StatusHints.java @@ -16,14 +16,19 @@ package android.telecom; +import android.annotation.Nullable; import android.annotation.SystemApi; import android.content.ComponentName; import android.content.Context; import android.graphics.drawable.Drawable; import android.graphics.drawable.Icon; +import android.os.Binder; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; +import android.os.UserHandle; + +import com.android.internal.annotations.VisibleForTesting; import java.util.Objects; @@ -33,7 +38,7 @@ import java.util.Objects; public final class StatusHints implements Parcelable { private final CharSequence mLabel; - private final Icon mIcon; + private Icon mIcon; private final Bundle mExtras; /** @@ -48,11 +53,31 @@ public final class StatusHints implements Parcelable { public StatusHints(CharSequence label, Icon icon, Bundle extras) { mLabel = label; - mIcon = icon; + mIcon = validateAccountIconUserBoundary(icon, Binder.getCallingUserHandle()); mExtras = extras; } /** + * @param icon + * @hide + */ + @VisibleForTesting + public StatusHints(@Nullable Icon icon) { + mLabel = null; + mExtras = null; + mIcon = icon; + } + + /** + * + * @param icon + * @hide + */ + public void setIcon(@Nullable Icon icon) { + mIcon = icon; + } + + /** * @return A package used to load the icon. * * @hide @@ -112,6 +137,30 @@ public final class StatusHints implements Parcelable { return 0; } + /** + * Validates the StatusHints image icon to see if it's not in the calling user space. + * Invalidates the icon if so, otherwise returns back the original icon. + * + * @param icon + * @return icon (validated) + * @hide + */ + public static Icon validateAccountIconUserBoundary(Icon icon, UserHandle callingUserHandle) { + // Refer to Icon#getUriString for context. The URI string is invalid for icons of + // incompatible types. + if (icon != null && (icon.getType() == Icon.TYPE_URI + || icon.getType() == Icon.TYPE_URI_ADAPTIVE_BITMAP)) { + String encodedUser = icon.getUri().getEncodedUserInfo(); + // If there is no encoded user, the URI is calling into the calling user space + if (encodedUser != null) { + int userId = Integer.parseInt(encodedUser); + // Do not try to save the icon if the user id isn't in the calling user space. + if (userId != callingUserHandle.getIdentifier()) return null; + } + } + return icon; + } + @Override public void writeToParcel(Parcel out, int flags) { out.writeCharSequence(mLabel); diff --git a/telecomm/java/android/telecom/StreamingCall.aidl b/telecomm/java/android/telecom/StreamingCall.aidl new file mode 100644 index 000000000000..d2866589a72a --- /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 000000000000..3319fc117b4d --- /dev/null +++ b/telecomm/java/android/telecom/StreamingCall.java @@ -0,0 +1,191 @@ +/* + * 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.SystemApi; +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. + * + * @hide + */ +@SystemApi +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; + + /** + * The ID associated with this call. This is the same value as {@link CallControl#getCallId()}. + * @hide + */ + public static final String EXTRA_CALL_ID = "android.telecom.extra.CALL_ID"; + + /** + * @hide + */ + private StreamingCall(@NonNull Parcel in) { + mComponentName = in.readParcelable(ComponentName.class.getClassLoader()); + mDisplayName = in.readCharSequence(); + 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.writeCharSequence(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 CharSequence mDisplayName; + private final Uri mAddress; + private final Bundle mExtras; + @StreamingCallState + private int mState; + private StreamingCallAdapter mAdapter = null; + + public StreamingCall(@NonNull ComponentName componentName, @NonNull CharSequence 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 CharSequence 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 requestStreamingState(@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 000000000000..54a3e247015c --- /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#requestStreamingState(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) { + } + } +} diff --git a/telecomm/java/android/telecom/TelecomManager.java b/telecomm/java/android/telecom/TelecomManager.java index e3f7c1688a49..a72f7806d3ea 100644 --- a/telecomm/java/android/telecom/TelecomManager.java +++ b/telecomm/java/android/telecom/TelecomManager.java @@ -15,8 +15,10 @@ package android.telecom; 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; @@ -37,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; @@ -48,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; @@ -739,10 +744,6 @@ public class TelecomManager { * state of calls in the self-managed {@link ConnectionService}. An example use-case is * exposing these calls to an automotive device via its companion app. * <p> - * This meta-data can only be set for an {@link InCallService} which also sets - * {@link #METADATA_IN_CALL_SERVICE_UI}. Only the default phone/dialer app, or a car-mode - * {@link InCallService} can see self-managed calls. - * <p> * See also {@link Connection#PROPERTY_SELF_MANAGED}. */ public static final String METADATA_INCLUDE_SELF_MANAGED_CALLS = @@ -950,6 +951,23 @@ public class TelecomManager { public static final String EXTRA_CALL_SOURCE = "android.telecom.extra.CALL_SOURCE"; /** + * Intent action to trigger "switch to managed profile" dialog for call in SystemUI + * + * @hide + */ + public static final String ACTION_SHOW_SWITCH_TO_WORK_PROFILE_FOR_CALL_DIALOG = + "android.telecom.action.SHOW_SWITCH_TO_WORK_PROFILE_FOR_CALL_DIALOG"; + + /** + * Extra specifying the managed profile user id. + * This is used with {@link TelecomManager#ACTION_SHOW_SWITCH_TO_WORK_PROFILE_FOR_CALL_DIALOG} + * + * @hide + */ + public static final String EXTRA_MANAGED_PROFILE_USER_ID = + "android.telecom.extra.MANAGED_PROFILE_USER_ID"; + + /** * Indicating the call is initiated via emergency dialer's shortcut button. * * @hide @@ -1059,6 +1077,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 */ @@ -1183,7 +1209,7 @@ public class TelecomManager { if (service != null) { try { return service.getSimCallManager( - SubscriptionManager.getDefaultSubscriptionId()); + SubscriptionManager.getDefaultSubscriptionId(), mContext.getPackageName()); } catch (RemoteException e) { Log.e(TAG, "Error calling ITelecomService#getSimCallManager"); } @@ -1205,7 +1231,7 @@ public class TelecomManager { ITelecomService service = getTelecomService(); if (service != null) { try { - return service.getSimCallManager(subscriptionId); + return service.getSimCallManager(subscriptionId, mContext.getPackageName()); } catch (RemoteException e) { Log.e(TAG, "Error calling ITelecomService#getSimCallManager"); } @@ -1229,7 +1255,7 @@ public class TelecomManager { ITelecomService service = getTelecomService(); if (service != null) { try { - return service.getSimCallManagerForUser(userId); + return service.getSimCallManagerForUser(userId, mContext.getPackageName()); } catch (RemoteException e) { Log.e(TAG, "Error calling ITelecomService#getSimCallManagerForUser"); } @@ -1504,7 +1530,7 @@ public class TelecomManager { ITelecomService service = getTelecomService(); if (service != null) { try { - service.registerPhoneAccount(account); + service.registerPhoneAccount(account, mContext.getPackageName()); } catch (RemoteException e) { Log.e(TAG, "Error calling ITelecomService#registerPhoneAccount", e); } @@ -1520,7 +1546,7 @@ public class TelecomManager { ITelecomService service = getTelecomService(); if (service != null) { try { - service.unregisterPhoneAccount(accountHandle); + service.unregisterPhoneAccount(accountHandle, mContext.getPackageName()); } catch (RemoteException e) { Log.e(TAG, "Error calling ITelecomService#unregisterPhoneAccount", e); } @@ -1601,7 +1627,7 @@ public class TelecomManager { ITelecomService service = getTelecomService(); if (service != null) { try { - return service.getDefaultDialerPackage(); + return service.getDefaultDialerPackage(mContext.getPackageName()); } catch (RemoteException e) { Log.e(TAG, "RemoteException attempting to get the default dialer package name.", e); } @@ -1675,7 +1701,7 @@ public class TelecomManager { ITelecomService service = getTelecomService(); if (service != null) { try { - return service.getSystemDialerPackage(); + return service.getSystemDialerPackage(mContext.getPackageName()); } catch (RemoteException e) { Log.e(TAG, "RemoteException attempting to get the system dialer package name.", e); } @@ -1854,7 +1880,7 @@ public class TelecomManager { ITelecomService service = getTelecomService(); if (service != null) { try { - return service.getCallStateUsingPackage(mContext.getPackageName(), + return service.getCallStateUsingPackage(mContext.getOpPackageName(), mContext.getAttributionTag()); } catch (RemoteException e) { Log.d(TAG, "RemoteException calling getCallState().", e); @@ -2071,7 +2097,10 @@ public class TelecomManager { * For a self-managed {@link ConnectionService}, a {@link SecurityException} will be thrown if * the {@link PhoneAccount} has {@link PhoneAccount#CAPABILITY_SELF_MANAGED} and the calling app * does not have {@link android.Manifest.permission#MANAGE_OWN_CALLS}. - * + * <p> + * <p> + * <b>Note</b>: {@link android.app.Notification.CallStyle} notifications should be posted after + * the call is added to Telecom in order for the notification to be non-dismissible. * @param phoneAccount A {@link PhoneAccountHandle} registered with * {@link #registerPhoneAccount}. * @param extras A bundle that will be passed through to @@ -2088,7 +2117,8 @@ public class TelecomManager { "acceptHandover for API > O-MR1"); return; } - service.addNewIncomingCall(phoneAccount, extras == null ? new Bundle() : extras); + service.addNewIncomingCall(phoneAccount, extras == null ? new Bundle() : extras, + mContext.getPackageName()); } catch (RemoteException e) { Log.e(TAG, "RemoteException adding a new incoming call: " + phoneAccount, e); } @@ -2130,7 +2160,8 @@ public class TelecomManager { if (service != null) { try { service.addNewIncomingConference( - phoneAccount, extras == null ? new Bundle() : extras); + phoneAccount, extras == null ? new Bundle() : extras, + mContext.getPackageName()); } catch (RemoteException e) { Log.e(TAG, "RemoteException adding a new incoming conference: " + phoneAccount, e); } @@ -2317,7 +2348,10 @@ public class TelecomManager { * {@link PhoneAccount} with the {@link PhoneAccount#CAPABILITY_PLACE_EMERGENCY_CALLS} * capability, depending on external factors, such as network conditions and Modem/SIM status. * </p> - * + * <p> + * <p> + * <b>Note</b>: {@link android.app.Notification.CallStyle} notifications should be posted after + * the call is placed in order for the notification to be non-dismissible. * @param address The address to make the call to. * @param extras Bundle of extras to use with the call. */ @@ -2427,7 +2461,11 @@ public class TelecomManager { Intent result = null; if (service != null) { try { - result = service.createManageBlockedNumbersIntent(); + result = service.createManageBlockedNumbersIntent(mContext.getPackageName()); + if (result != null) { + result.prepareToEnterProcess(LOCAL_FLAG_FROM_SYSTEM, + mContext.getAttributionSource()); + } } catch (RemoteException e) { Log.e(TAG, "Error calling ITelecomService#createManageBlockedNumbersIntent", e); } @@ -2449,7 +2487,12 @@ public class TelecomManager { ITelecomService service = getTelecomService(); if (service != null) { try { - return service.createLaunchEmergencyDialerIntent(number); + Intent result = service.createLaunchEmergencyDialerIntent(number); + if (result != null) { + result.prepareToEnterProcess(LOCAL_FLAG_FROM_SYSTEM, + mContext.getAttributionSource()); + } + return result; } catch (RemoteException e) { Log.e(TAG, "Error createLaunchEmergencyDialerIntent", e); } @@ -2584,7 +2627,7 @@ public class TelecomManager { ITelecomService service = getTelecomService(); if (service != null) { try { - service.acceptHandover(srcAddr, videoState, destAcct); + service.acceptHandover(srcAddr, videoState, destAcct, mContext.getPackageName()); } catch (RemoteException e) { Log.e(TAG, "RemoteException acceptHandover: " + e); } @@ -2641,6 +2684,117 @@ public class TelecomManager { } /** + * Add a call to the Android system service Telecom. This allows the system to start tracking an + * incoming or outgoing call with the specified {@link CallAttributes}. Once a call is added, + * a {@link android.app.Notification.CallStyle} notification should be posted and when the + * call is ready to be disconnected, use {@link CallControl#disconnect(DisconnectCause, + * Executor, OutcomeReceiver)} which is provided by the + * {@code pendingControl#onResult(CallControl)}. + * <p> + * <p> + * <p> + * <b>Call Lifecycle</b>: Your app is given foreground execution priority as long as you have a + * valid call and are posting a {@link android.app.Notification.CallStyle} notification. + * When your application is given foreground execution priority, your app is treated as a + * foreground service. Foreground execution priority will prevent the + * {@link android.app.ActivityManager} from killing your application when it is placed the + * background. Foreground execution priority is removed from your app when all of your app's + * calls terminate or your app no longer posts a valid notification. + * <p> + * <p> + * <p> + * <b>Note</b>: 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> + * <p> + * <p> + * <b>Usage example:</b> + * <pre> + * // Its up to your app on how you want to wrap the objects. One such implementation can be: + * class MyVoipCall { + * ... + * public CallControlCallEventCallback handshakes = new CallControlCallback() { + * ... + * } + * + * public CallEventCallback events = new CallEventCallback() { + * ... + * } + * + * public MyVoipCall(String id){ + * ... + * } + * } + * + * MyVoipCall myFirstOutgoingCall = new MyVoipCall("1"); + * + * telecomManager.addCall(callAttributes, + * Runnable::run, + * new OutcomeReceiver() { + * public void onResult(CallControl callControl) { + * // The call has been added successfully. For demonstration + * // purposes, the call is disconnected immediately ... + * callControl.disconnect( + * new DisconnectCause(DisconnectCause.LOCAL) ) + * } + * }, + * myFirstOutgoingCall.handshakes, + * myFirstOutgoingCall.events); + * </pre> + * + * @param callAttributes attributes of the new call (incoming or outgoing, address, etc.) + * @param executor execution context to run {@link CallControlCallback} updates on + * @param pendingControl Receives the result of addCall transaction. Upon success, a + * CallControl object is provided which can be used to do things like + * disconnect the call that was added. + * @param handshakes callback that receives <b>actionable</b> updates that originate from + * Telecom. + * @param events callback that receives <b>non</b>-actionable updates that originate + * from Telecom. + */ + @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 CallControlCallback handshakes, + @NonNull CallEventCallback events) { + Objects.requireNonNull(callAttributes); + Objects.requireNonNull(executor); + Objects.requireNonNull(pendingControl); + Objects.requireNonNull(handshakes); + Objects.requireNonNull(events); + + 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, handshakes, events); + + // 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. * @param callingPackageProxy The original package that called this before it was trampolined. diff --git a/telecomm/java/com/android/internal/telecom/ClientTransactionalServiceRepository.java b/telecomm/java/com/android/internal/telecom/ClientTransactionalServiceRepository.java new file mode 100644 index 000000000000..2eebbdb35fbb --- /dev/null +++ b/telecomm/java/com/android/internal/telecom/ClientTransactionalServiceRepository.java @@ -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; + } + +} diff --git a/telecomm/java/com/android/internal/telecom/ClientTransactionalServiceWrapper.java b/telecomm/java/com/android/internal/telecom/ClientTransactionalServiceWrapper.java new file mode 100644 index 000000000000..71e9184b7c54 --- /dev/null +++ b/telecomm/java/com/android/internal/telecom/ClientTransactionalServiceWrapper.java @@ -0,0 +1,330 @@ +/* + * 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.Bundle; +import android.os.OutcomeReceiver; +import android.os.ResultReceiver; +import android.telecom.CallAttributes; +import android.telecom.CallControl; +import android.telecom.CallControlCallback; +import android.telecom.CallEndpoint; +import android.telecom.CallEventCallback; +import android.telecom.CallException; +import android.telecom.DisconnectCause; +import android.telecom.PhoneAccountHandle; +import android.text.TextUtils; +import android.util.Log; + +import java.util.List; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executor; +import java.util.function.Consumer; + +/** + * wraps {@link CallControlCallback}, {@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 handshakes that overrides the CallControlCallback + * @param events that overrides the CallStateCallback + * @return the callId of the newly created call + */ + public String trackCall(CallAttributes callAttributes, Executor executor, + OutcomeReceiver<CallControl, CallException> pendingControl, + CallControlCallback handshakes, + CallEventCallback events) { + // 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, handshakes, events)); + + 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_DISCONNECT = "onDisconnect"; + private static final String ON_STREAMING_STARTED = "onStreamingStarted"; + private static final String ON_REQ_ENDPOINT_CHANGE = "onRequestEndpointChange"; + private static final String ON_AVAILABLE_CALL_ENDPOINTS = "onAvailableCallEndpointsChanged"; + private static final String ON_MUTE_STATE_CHANGED = "onMuteStateChanged"; + private static final String ON_CALL_STREAMING_FAILED = "onCallStreamingFailed"; + private static final String ON_EVENT = "onEvent"; + + private void handleCallEventCallback(String action, String callId, + ResultReceiver ackResultReceiver, Object... args) { + 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 + CallControlCallback callback = call.getCallControlCallback(); + // 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_DISCONNECT: + callback.onDisconnect((DisconnectCause) args[0], + outcomeReceiverWrapper); + untrackCall(callId); + break; + case ON_ANSWER: + callback.onAnswer((int) args[0], outcomeReceiverWrapper); + break; + case ON_STREAMING_STARTED: + callback.onCallStreamingStarted(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, resultReceiver); + } + + @Override + public void onSetInactive(String callId, ResultReceiver resultReceiver) { + handleCallEventCallback(ON_SET_INACTIVE, callId, resultReceiver); + } + + @Override + public void onAnswer(String callId, int videoState, ResultReceiver resultReceiver) { + handleCallEventCallback(ON_ANSWER, callId, resultReceiver, videoState); + } + + @Override + public void onDisconnect(String callId, DisconnectCause cause, + ResultReceiver resultReceiver) { + handleCallEventCallback(ON_DISCONNECT, callId, resultReceiver, cause); + } + + @Override + public void onCallEndpointChanged(String callId, CallEndpoint endpoint) { + handleEventCallback(callId, ON_REQ_ENDPOINT_CHANGE, endpoint); + } + + @Override + public void onAvailableCallEndpointsChanged(String callId, List<CallEndpoint> endpoints) { + handleEventCallback(callId, ON_AVAILABLE_CALL_ENDPOINTS, endpoints); + } + + @Override + public void onMuteStateChanged(String callId, boolean isMuted) { + handleEventCallback(callId, ON_MUTE_STATE_CHANGED, isMuted); + } + + public void handleEventCallback(String callId, String action, Object arg) { + Log.d(TAG, TextUtils.formatSimple("hEC: [%s], callId=[%s]", action, callId)); + // lookup the callEventCallback associated with the particular call + TransactionalCall call = mCallIdToTransactionalCall.get(callId); + if (call != null) { + CallEventCallback callback = call.getCallStateCallback(); + Executor executor = call.getExecutor(); + final long identity = Binder.clearCallingIdentity(); + try { + executor.execute(() -> { + switch (action) { + case ON_REQ_ENDPOINT_CHANGE: + callback.onCallEndpointChanged((CallEndpoint) arg); + break; + case ON_AVAILABLE_CALL_ENDPOINTS: + callback.onAvailableCallEndpointsChanged((List<CallEndpoint>) arg); + break; + case ON_MUTE_STATE_CHANGED: + callback.onMuteStateChanged((boolean) arg); + break; + case ON_CALL_STREAMING_FAILED: + callback.onCallStreamingFailed((int) arg /* reason */); + break; + } + }); + } finally { + Binder.restoreCallingIdentity(identity); + } + } + } + + @Override + public void removeCallFromTransactionalServiceWrapper(String callId) { + untrackCall(callId); + } + + @Override + public void onCallStreamingStarted(String callId, ResultReceiver resultReceiver) { + handleCallEventCallback(ON_STREAMING_STARTED, callId, resultReceiver); + } + + @Override + public void onCallStreamingFailed(String callId, int reason) { + Log.i(TAG, TextUtils.formatSimple("oCSF: id=[%s], reason=[%s]", callId, reason)); + handleEventCallback(callId, ON_CALL_STREAMING_FAILED, reason); + } + + @Override + public void onEvent(String callId, String event, Bundle extras) { + // lookup the callEventCallback associated with the particular call + TransactionalCall call = mCallIdToTransactionalCall.get(callId); + if (call != null) { + CallEventCallback callback = call.getCallStateCallback(); + Executor executor = call.getExecutor(); + final long identity = Binder.clearCallingIdentity(); + try { + executor.execute(() -> { + callback.onEvent(event, extras); + }); + } finally { + Binder.restoreCallingIdentity(identity); + } + } + } + }; +} diff --git a/telecomm/java/com/android/internal/telecom/ICallControl.aidl b/telecomm/java/com/android/internal/telecom/ICallControl.aidl new file mode 100644 index 000000000000..5e2c923e4c9c --- /dev/null +++ b/telecomm/java/com/android/internal/telecom/ICallControl.aidl @@ -0,0 +1,36 @@ +/* + * 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.Bundle; +import android.telecom.CallControl; +import android.telecom.CallEndpoint; +import android.telecom.DisconnectCause; +import android.os.ResultReceiver; + +/** + * {@hide} + */ +oneway interface ICallControl { + void setActive(String callId, in ResultReceiver callback); + void answer(int videoState, String callId, in ResultReceiver callback); + void setInactive(String callId, in ResultReceiver callback); + void disconnect(String callId, in DisconnectCause disconnectCause, in ResultReceiver callback); + void startCallStreaming(String callId, in ResultReceiver callback); + void requestCallEndpointChange(in CallEndpoint callEndpoint, in ResultReceiver callback); + void sendEvent(String callId, String event, in Bundle extras); +}
\ No newline at end of file diff --git a/telecomm/java/com/android/internal/telecom/ICallEventCallback.aidl b/telecomm/java/com/android/internal/telecom/ICallEventCallback.aidl new file mode 100644 index 000000000000..213cafbbf188 --- /dev/null +++ b/telecomm/java/com/android/internal/telecom/ICallEventCallback.aidl @@ -0,0 +1,52 @@ +/* + * 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.Bundle; +import android.telecom.CallControl; +import android.telecom.CallEndpoint; +import com.android.internal.telecom.ICallControl; +import android.os.ResultReceiver; +import android.telecom.CallAudioState; +import android.telecom.CallException; +import android.telecom.DisconnectCause; +import java.util.List; + +/** + * {@hide} + */ +oneway interface ICallEventCallback { + // publicly exposed. Client should override + void onAddCallControl(String callId, int resultCode, in ICallControl callControl, + in CallException exception); + // -- Call Event Actions / Call State Transitions + void onSetActive(String callId, in ResultReceiver callback); + void onSetInactive(String callId, in ResultReceiver callback); + void onAnswer(String callId, int videoState, in ResultReceiver callback); + void onDisconnect(String callId, in DisconnectCause cause, in ResultReceiver callback); + // -- Streaming related. Client registered call streaming capabilities should override + void onCallStreamingStarted(String callId, in ResultReceiver callback); + void onCallStreamingFailed(String callId, int reason); + // -- Audio related. + void onCallEndpointChanged(String callId, in CallEndpoint endpoint); + void onAvailableCallEndpointsChanged(String callId, in List<CallEndpoint> endpoint); + void onMuteStateChanged(String callId, boolean isMuted); + // -- Events + void onEvent(String callId, String event, in Bundle extras); + // hidden methods that help with cleanup + void removeCallFromTransactionalServiceWrapper(String callId); +}
\ No newline at end of file diff --git a/telecomm/java/com/android/internal/telecom/ICallStreamingService.aidl b/telecomm/java/com/android/internal/telecom/ICallStreamingService.aidl new file mode 100644 index 000000000000..6d53fd25bb45 --- /dev/null +++ b/telecomm/java/com/android/internal/telecom/ICallStreamingService.aidl @@ -0,0 +1,35 @@ +/* + * 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.StreamingCall; + +import com.android.internal.telecom.IStreamingCallAdapter; + +/** + * Internal remote interface for call streaming services. + * + * @see android.telecom.CallStreamingService + * + * {@hide} + */ +oneway interface ICallStreamingService { + void setStreamingCallAdapter(in IStreamingCallAdapter streamingCallAdapter); + void onCallStreamingStarted(in StreamingCall call); + void onCallStreamingStopped(); + void onCallStreamingStateChanged(int state); +}
\ No newline at end of file diff --git a/telecomm/java/com/android/internal/telecom/IConnectionService.aidl b/telecomm/java/com/android/internal/telecom/IConnectionService.aidl index d72f8aa82ddb..29617f21bf95 100644 --- a/telecomm/java/com/android/internal/telecom/IConnectionService.aidl +++ b/telecomm/java/com/android/internal/telecom/IConnectionService.aidl @@ -20,6 +20,7 @@ import android.net.Uri; import android.os.Bundle; import android.os.ParcelFileDescriptor; import android.telecom.CallAudioState; +import android.telecom.CallEndpoint; import android.telecom.Connection; import android.telecom.ConnectionRequest; import android.telecom.Logging.Session; @@ -98,6 +99,14 @@ oneway interface IConnectionService { void onCallAudioStateChanged(String activeCallId, in CallAudioState callAudioState, in Session.Info sessionInfo); + void onCallEndpointChanged(String activeCallId, in CallEndpoint callEndpoint, + in Session.Info sessionInfo); + + void onAvailableCallEndpointsChanged(String activeCallId, + in List<CallEndpoint> availableCallEndpoints, in Session.Info sessionInfo); + + void onMuteStateChanged(String activeCallId, boolean isMuted, in Session.Info sessionInfo); + void playDtmfTone(String callId, char digit, in Session.Info sessionInfo); void stopDtmfTone(String callId, in Session.Info sessionInfo); diff --git a/telecomm/java/com/android/internal/telecom/IConnectionServiceAdapter.aidl b/telecomm/java/com/android/internal/telecom/IConnectionServiceAdapter.aidl index 3fd7f949cfe6..8ac016155aa6 100644 --- a/telecomm/java/com/android/internal/telecom/IConnectionServiceAdapter.aidl +++ b/telecomm/java/com/android/internal/telecom/IConnectionServiceAdapter.aidl @@ -19,6 +19,8 @@ package com.android.internal.telecom; import android.app.PendingIntent; import android.net.Uri; import android.os.Bundle; +import android.os.ResultReceiver; +import android.telecom.CallEndpoint; import android.telecom.ConnectionRequest; import android.telecom.DisconnectCause; import android.telecom.Logging.Session; @@ -113,6 +115,9 @@ oneway interface IConnectionServiceAdapter { void setAudioRoute(String callId, int audioRoute, String bluetoothAddress, in Session.Info sessionInfo); + void requestCallEndpointChange(String callId, in CallEndpoint endpoint, + in ResultReceiver callback, in Session.Info sessionInfo); + void onConnectionEvent(String callId, String event, in Bundle extras, in Session.Info sessionInfo); @@ -134,4 +139,7 @@ oneway interface IConnectionServiceAdapter { void setConferenceState(String callId, boolean isConference, in Session.Info sessionInfo); void setCallDirection(String callId, int direction, in Session.Info sessionInfo); + + void queryLocation(String callId, long timeoutMillis, String provider, + in ResultReceiver callback, in Session.Info sessionInfo); } diff --git a/telecomm/java/com/android/internal/telecom/IInCallAdapter.aidl b/telecomm/java/com/android/internal/telecom/IInCallAdapter.aidl index edf1cf4cdb18..e381ce8c080f 100755 --- a/telecomm/java/com/android/internal/telecom/IInCallAdapter.aidl +++ b/telecomm/java/com/android/internal/telecom/IInCallAdapter.aidl @@ -18,7 +18,9 @@ package com.android.internal.telecom; import android.net.Uri; import android.os.Bundle; +import android.os.ResultReceiver; import android.telecom.PhoneAccountHandle; +import android.telecom.CallEndpoint; /** * Internal remote callback interface for in-call services. @@ -50,6 +52,8 @@ oneway interface IInCallAdapter { void setAudioRoute(int route, String bluetoothAddress); + void requestCallEndpointChange(in CallEndpoint endpoint, in ResultReceiver callback); + void enterBackgroundAudioProcessing(String callId); void exitBackgroundAudioProcessing(String callId, boolean shouldRing); diff --git a/telecomm/java/com/android/internal/telecom/IInCallService.aidl b/telecomm/java/com/android/internal/telecom/IInCallService.aidl index b9563fa7bb18..bac295a774be 100644 --- a/telecomm/java/com/android/internal/telecom/IInCallService.aidl +++ b/telecomm/java/com/android/internal/telecom/IInCallService.aidl @@ -19,6 +19,7 @@ package com.android.internal.telecom; import android.app.PendingIntent; import android.os.Bundle; import android.telecom.CallAudioState; +import android.telecom.CallEndpoint; import android.telecom.ParcelableCall; import com.android.internal.telecom.IInCallAdapter; @@ -43,6 +44,12 @@ oneway interface IInCallService { void onCallAudioStateChanged(in CallAudioState callAudioState); + void onCallEndpointChanged(in CallEndpoint callEndpoint); + + void onAvailableCallEndpointsChanged(in List<CallEndpoint> availableCallEndpoints); + + void onMuteStateChanged(boolean isMuted); + void bringToForeground(boolean showDialpad); void onCanAddCallChanged(boolean canAddCall); diff --git a/telecomm/java/com/android/internal/telecom/IStreamingCallAdapter.aidl b/telecomm/java/com/android/internal/telecom/IStreamingCallAdapter.aidl new file mode 100644 index 000000000000..51424a66d0df --- /dev/null +++ b/telecomm/java/com/android/internal/telecom/IStreamingCallAdapter.aidl @@ -0,0 +1,28 @@ +/* + * 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; + +/** + * Internal remote callback interface for call streaming services. + * + * @see android.telecom.StreamingCallAdapter + * + * {@hide} + */ +oneway interface IStreamingCallAdapter { + void setStreamingState(int state); +}
\ No newline at end of file diff --git a/telecomm/java/com/android/internal/telecom/ITelecomService.aidl b/telecomm/java/com/android/internal/telecom/ITelecomService.aidl index 74b5545e75de..fdcb9749c38e 100644 --- a/telecomm/java/com/android/internal/telecom/ITelecomService.aidl +++ b/telecomm/java/com/android/internal/telecom/ITelecomService.aidl @@ -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 @@ -107,22 +109,22 @@ interface ITelecomService { /** * @see TelecomServiceImpl#getSimCallManager */ - PhoneAccountHandle getSimCallManager(int subId); + PhoneAccountHandle getSimCallManager(int subId, String callingPackage); /** * @see TelecomServiceImpl#getSimCallManagerForUser */ - PhoneAccountHandle getSimCallManagerForUser(int userId); + PhoneAccountHandle getSimCallManagerForUser(int userId, String callingPackage); /** * @see TelecomServiceImpl#registerPhoneAccount */ - void registerPhoneAccount(in PhoneAccount metadata); + void registerPhoneAccount(in PhoneAccount metadata, String callingPackage); /** * @see TelecomServiceImpl#unregisterPhoneAccount */ - void unregisterPhoneAccount(in PhoneAccountHandle account); + void unregisterPhoneAccount(in PhoneAccountHandle account, String callingPackage); /** * @see TelecomServiceImpl#clearAccounts @@ -155,7 +157,7 @@ interface ITelecomService { /** * @see TelecomServiceImpl#getDefaultDialerPackage */ - String getDefaultDialerPackage(); + String getDefaultDialerPackage(String callingPackage); /** * @see TelecomServiceImpl#getDefaultDialerPackage @@ -165,7 +167,7 @@ interface ITelecomService { /** * @see TelecomServiceImpl#getSystemDialerPackage */ - String getSystemDialerPackage(); + String getSystemDialerPackage(String callingPackage); /** * @see TelecomServiceImpl#dumpCallAnalytics @@ -263,12 +265,15 @@ interface ITelecomService { /** * @see TelecomServiceImpl#addNewIncomingCall */ - void addNewIncomingCall(in PhoneAccountHandle phoneAccount, in Bundle extras); + void addNewIncomingCall(in PhoneAccountHandle phoneAccount, in Bundle extras, + String callingPackage); /** * @see TelecomServiceImpl#addNewIncomingConference */ - void addNewIncomingConference(in PhoneAccountHandle phoneAccount, in Bundle extras); + void addNewIncomingConference(in PhoneAccountHandle phoneAccount, in Bundle extras, + String callingPackage); + /** * @see TelecomServiceImpl#addNewUnknownCall @@ -304,7 +309,7 @@ interface ITelecomService { /** * @see TelecomServiceImpl#createManageBlockedNumbersIntent **/ - Intent createManageBlockedNumbersIntent(); + Intent createManageBlockedNumbersIntent(String callingPackage); /** * @see TelecomServiceImpl#createLaunchEmergencyDialerIntent @@ -331,7 +336,8 @@ interface ITelecomService { /** * @see TelecomServiceImpl#acceptHandover */ - void acceptHandover(in Uri srcAddr, int videoState, in PhoneAccountHandle destAcct); + void acceptHandover(in Uri srcAddr, int videoState, in PhoneAccountHandle destAcct, + String callingPackage); /** * @see TelecomServiceImpl#setTestEmergencyPhoneAccountPackageNameFilter @@ -387,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); } diff --git a/telecomm/java/com/android/internal/telecom/TransactionalCall.java b/telecomm/java/com/android/internal/telecom/TransactionalCall.java new file mode 100644 index 000000000000..75f9d35470db --- /dev/null +++ b/telecomm/java/com/android/internal/telecom/TransactionalCall.java @@ -0,0 +1,85 @@ +/* + * 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.CallControlCallback; +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 CallControlCallback mCallControlCallback; + private final CallEventCallback mCallStateCallback; + private CallControl mCallControl; + + public TransactionalCall(String callId, CallAttributes callAttributes, + Executor executor, OutcomeReceiver<CallControl, CallException> pendingControl, + CallControlCallback callControlCallback, + CallEventCallback callStateCallback) { + mCallId = callId; + mCallAttributes = callAttributes; + mExecutor = executor; + mPendingControl = pendingControl; + mCallControlCallback = callControlCallback; + mCallStateCallback = callStateCallback; + } + + + 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 CallControlCallback getCallControlCallback() { + return mCallControlCallback; + } + + public CallEventCallback getCallStateCallback() { + return mCallStateCallback; + } +} |