diff options
author | Justin Klaassen <justinklaassen@google.com> | 2017-09-15 17:58:39 -0400 |
---|---|---|
committer | Justin Klaassen <justinklaassen@google.com> | 2017-09-15 17:58:39 -0400 |
commit | 10d07c88d69cc64f73a069163e7ea5ba2519a099 (patch) | |
tree | 8dbd149eb350320a29c3d10e7ad3201de1c5cbee /android/telecom | |
parent | 677516fb6b6f207d373984757d3d9450474b6b00 (diff) | |
download | android-28-10d07c88d69cc64f73a069163e7ea5ba2519a099.tar.gz |
Import Android SDK Platform PI [4335822]
/google/data/ro/projects/android/fetch_artifact \
--bid 4335822 \
--target sdk_phone_armv7-win_sdk \
sdk-repo-linux-sources-4335822.zip
AndroidVersion.ApiLevel has been modified to appear as 28
Change-Id: Ic8f04be005a71c2b9abeaac754d8da8d6f9a2c32
Diffstat (limited to 'android/telecom')
47 files changed, 24101 insertions, 0 deletions
diff --git a/android/telecom/AudioState.java b/android/telecom/AudioState.java new file mode 100644 index 00000000..33013ac0 --- /dev/null +++ b/android/telecom/AudioState.java @@ -0,0 +1,187 @@ +/* + * Copyright (C) 2014 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.SystemApi; +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.Locale; + +/** + * Encapsulates the telecom audio state, including the current audio routing, supported audio + * routing and mute. + * @deprecated - use {@link CallAudioState} instead. + * @hide + */ +@Deprecated +@SystemApi +public class AudioState implements Parcelable { + /** Direct the audio stream through the device's earpiece. */ + public static final int ROUTE_EARPIECE = 0x00000001; + + /** Direct the audio stream through Bluetooth. */ + public static final int ROUTE_BLUETOOTH = 0x00000002; + + /** Direct the audio stream through a wired headset. */ + public static final int ROUTE_WIRED_HEADSET = 0x00000004; + + /** Direct the audio stream through the device's speakerphone. */ + public static final int ROUTE_SPEAKER = 0x00000008; + + /** + * Direct the audio stream through the device's earpiece or wired headset if one is + * connected. + */ + public static final int ROUTE_WIRED_OR_EARPIECE = ROUTE_EARPIECE | ROUTE_WIRED_HEADSET; + + /** Bit mask of all possible audio routes. */ + private static final int ROUTE_ALL = ROUTE_EARPIECE | ROUTE_BLUETOOTH | ROUTE_WIRED_HEADSET | + ROUTE_SPEAKER; + + private final boolean isMuted; + private final int route; + private final int supportedRouteMask; + + public AudioState(boolean muted, int route, int supportedRouteMask) { + this.isMuted = muted; + this.route = route; + this.supportedRouteMask = supportedRouteMask; + } + + public AudioState(AudioState state) { + isMuted = state.isMuted(); + route = state.getRoute(); + supportedRouteMask = state.getSupportedRouteMask(); + } + + public AudioState(CallAudioState state) { + isMuted = state.isMuted(); + route = state.getRoute(); + supportedRouteMask = state.getSupportedRouteMask(); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (!(obj instanceof AudioState)) { + return false; + } + AudioState state = (AudioState) obj; + return isMuted() == state.isMuted() && getRoute() == state.getRoute() && + getSupportedRouteMask() == state.getSupportedRouteMask(); + } + + @Override + public String toString() { + return String.format(Locale.US, + "[AudioState isMuted: %b, route: %s, supportedRouteMask: %s]", + isMuted, + audioRouteToString(route), + audioRouteToString(supportedRouteMask)); + } + + public static String audioRouteToString(int route) { + if (route == 0 || (route & ~ROUTE_ALL) != 0x0) { + return "UNKNOWN"; + } + + StringBuffer buffer = new StringBuffer(); + if ((route & ROUTE_EARPIECE) == ROUTE_EARPIECE) { + listAppend(buffer, "EARPIECE"); + } + if ((route & ROUTE_BLUETOOTH) == ROUTE_BLUETOOTH) { + listAppend(buffer, "BLUETOOTH"); + } + if ((route & ROUTE_WIRED_HEADSET) == ROUTE_WIRED_HEADSET) { + listAppend(buffer, "WIRED_HEADSET"); + } + if ((route & ROUTE_SPEAKER) == ROUTE_SPEAKER) { + listAppend(buffer, "SPEAKER"); + } + + return buffer.toString(); + } + + private static void listAppend(StringBuffer buffer, String str) { + if (buffer.length() > 0) { + buffer.append(", "); + } + buffer.append(str); + } + + /** + * Responsible for creating AudioState objects for deserialized Parcels. + */ + public static final Parcelable.Creator<AudioState> CREATOR = + new Parcelable.Creator<AudioState> () { + + @Override + public AudioState createFromParcel(Parcel source) { + boolean isMuted = source.readByte() == 0 ? false : true; + int route = source.readInt(); + int supportedRouteMask = source.readInt(); + return new AudioState(isMuted, route, supportedRouteMask); + } + + @Override + public AudioState[] newArray(int size) { + return new AudioState[size]; + } + }; + + /** + * {@inheritDoc} + */ + @Override + public int describeContents() { + return 0; + } + + /** + * Writes AudioState object into a serializeable Parcel. + */ + @Override + public void writeToParcel(Parcel destination, int flags) { + destination.writeByte((byte) (isMuted ? 1 : 0)); + destination.writeInt(route); + destination.writeInt(supportedRouteMask); + } + + /** + * @return {@code true} if the call is muted, false otherwise. + */ + public boolean isMuted() { + return isMuted; + } + + /** + * @return The current audio route being used. + */ + public int getRoute() { + return route; + } + + /** + * @return Bit mask of all routes supported by this call. + */ + public int getSupportedRouteMask() { + return supportedRouteMask; + } +} diff --git a/android/telecom/AuthenticatorService.java b/android/telecom/AuthenticatorService.java new file mode 100644 index 00000000..1e43c715 --- /dev/null +++ b/android/telecom/AuthenticatorService.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2015 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.accounts.AbstractAccountAuthenticator; +import android.accounts.Account; +import android.accounts.AccountAuthenticatorResponse; +import android.accounts.NetworkErrorException; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.os.IBinder; + +/** + * A generic stub account authenticator service often used for sync adapters that do not directly + * involve accounts. + * + * @hide + */ +public class AuthenticatorService extends Service { + private static Authenticator mAuthenticator; + + @Override + public void onCreate() { + mAuthenticator = new Authenticator(this); + } + + @Override + public IBinder onBind(Intent intent) { + return mAuthenticator.getIBinder(); + } + + /** + * Stub account authenticator. All methods either return null or throw an exception. + */ + public class Authenticator extends AbstractAccountAuthenticator { + public Authenticator(Context context) { + super(context); + } + + @Override + public Bundle editProperties(AccountAuthenticatorResponse accountAuthenticatorResponse, + String s) { + throw new UnsupportedOperationException(); + } + + @Override + public Bundle addAccount(AccountAuthenticatorResponse accountAuthenticatorResponse, + String s, String s2, String[] strings, Bundle bundle) + throws NetworkErrorException { + return null; + } + + @Override + public Bundle confirmCredentials(AccountAuthenticatorResponse accountAuthenticatorResponse, + Account account, Bundle bundle) + throws NetworkErrorException { + return null; + } + + @Override + public Bundle getAuthToken(AccountAuthenticatorResponse accountAuthenticatorResponse, + Account account, String s, Bundle bundle) + throws NetworkErrorException { + throw new UnsupportedOperationException(); + } + + @Override + public String getAuthTokenLabel(String s) { + throw new UnsupportedOperationException(); + } + + @Override + public Bundle updateCredentials(AccountAuthenticatorResponse accountAuthenticatorResponse, + Account account, String s, Bundle bundle) + throws NetworkErrorException { + throw new UnsupportedOperationException(); + } + + @Override + public Bundle hasFeatures(AccountAuthenticatorResponse accountAuthenticatorResponse, + Account account, String[] strings) + throws NetworkErrorException { + throw new UnsupportedOperationException(); + } + } +} diff --git a/android/telecom/Call.java b/android/telecom/Call.java new file mode 100644 index 00000000..e13bd619 --- /dev/null +++ b/android/telecom/Call.java @@ -0,0 +1,2092 @@ +/* + * Copyright (C) 2014 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.annotation.SystemApi; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.os.ParcelFileDescriptor; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.lang.String; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * Represents an ongoing phone call that the in-call app should present to the user. + */ +public final class Call { + /** + * The state of a {@code Call} when newly created. + */ + public static final int STATE_NEW = 0; + + /** + * The state of an outgoing {@code Call} when dialing the remote number, but not yet connected. + */ + public static final int STATE_DIALING = 1; + + /** + * The state of an incoming {@code Call} when ringing locally, but not yet connected. + */ + public static final int STATE_RINGING = 2; + + /** + * The state of a {@code Call} when in a holding state. + */ + public static final int STATE_HOLDING = 3; + + /** + * The state of a {@code Call} when actively supporting conversation. + */ + public static final int STATE_ACTIVE = 4; + + /** + * The state of a {@code Call} when no further voice or other communication is being + * transmitted, the remote side has been or will inevitably be informed that the {@code Call} + * is no longer active, and the local data transport has or inevitably will release resources + * associated with this {@code Call}. + */ + public static final int STATE_DISCONNECTED = 7; + + /** + * The state of an outgoing {@code Call} when waiting on user to select a + * {@link PhoneAccount} through which to place the call. + */ + public static final int STATE_SELECT_PHONE_ACCOUNT = 8; + + /** + * @hide + * @deprecated use STATE_SELECT_PHONE_ACCOUNT. + */ + @Deprecated + @SystemApi + public static final int STATE_PRE_DIAL_WAIT = STATE_SELECT_PHONE_ACCOUNT; + + /** + * The initial state of an outgoing {@code Call}. + * Common transitions are to {@link #STATE_DIALING} state for a successful call or + * {@link #STATE_DISCONNECTED} if it failed. + */ + public static final int STATE_CONNECTING = 9; + + /** + * The state of a {@code Call} when the user has initiated a disconnection of the call, but the + * call has not yet been disconnected by the underlying {@code ConnectionService}. The next + * state of the call is (potentially) {@link #STATE_DISCONNECTED}. + */ + public static final int STATE_DISCONNECTING = 10; + + /** + * The state of an external call which is in the process of being pulled from a remote device to + * the local device. + * <p> + * A call can only be in this state if the {@link Details#PROPERTY_IS_EXTERNAL_CALL} property + * and {@link Details#CAPABILITY_CAN_PULL_CALL} capability are set on the call. + * <p> + * An {@link InCallService} will only see this state if it has the + * {@link TelecomManager#METADATA_INCLUDE_EXTERNAL_CALLS} metadata set to {@code true} in its + * manifest. + */ + public static final int STATE_PULLING_CALL = 11; + + /** + * The key to retrieve the optional {@code PhoneAccount}s Telecom can bundle with its Call + * extras. Used to pass the phone accounts to display on the front end to the user in order to + * select phone accounts to (for example) place a call. + */ + public static final String AVAILABLE_PHONE_ACCOUNTS = "selectPhoneAccountAccounts"; + + /** + * Extra key used to indicate the time (in milliseconds since midnight, January 1, 1970 UTC) + * when the last outgoing emergency call was made. This is used to identify potential emergency + * callbacks. + */ + public static final String EXTRA_LAST_EMERGENCY_CALLBACK_TIME_MILLIS = + "android.telecom.extra.LAST_EMERGENCY_CALLBACK_TIME_MILLIS"; + + /** + * Call event sent from a {@link Call} via {@link #sendCallEvent(String, Bundle)} to inform + * Telecom that the user has requested that the current {@link Call} should be handed over + * to another {@link ConnectionService}. + * <p> + * The caller must specify the {@link #EXTRA_HANDOVER_PHONE_ACCOUNT_HANDLE} to indicate to + * Telecom which {@link PhoneAccountHandle} the {@link Call} should be handed over to. + * @hide + */ + public static final String EVENT_REQUEST_HANDOVER = + "android.telecom.event.REQUEST_HANDOVER"; + + /** + * Extra key used with the {@link #EVENT_REQUEST_HANDOVER} call event. Specifies the + * {@link PhoneAccountHandle} to which a call should be handed over to. + * @hide + */ + public static final String EXTRA_HANDOVER_PHONE_ACCOUNT_HANDLE = + "android.telecom.extra.HANDOVER_PHONE_ACCOUNT_HANDLE"; + + /** + * Integer extra key used with the {@link #EVENT_REQUEST_HANDOVER} call event. Specifies the + * video state of the call when it is handed over to the new {@link PhoneAccount}. + * <p> + * Valid values: {@link VideoProfile#STATE_AUDIO_ONLY}, + * {@link VideoProfile#STATE_BIDIRECTIONAL}, {@link VideoProfile#STATE_RX_ENABLED}, and + * {@link VideoProfile#STATE_TX_ENABLED}. + * @hide + */ + public static final String EXTRA_HANDOVER_VIDEO_STATE = + "android.telecom.extra.HANDOVER_VIDEO_STATE"; + + /** + * Extra key used with the {@link #EVENT_REQUEST_HANDOVER} call event. Used by the + * {@link InCallService} initiating a handover to provide a {@link Bundle} with extra + * information to the handover {@link ConnectionService} specified by + * {@link #EXTRA_HANDOVER_PHONE_ACCOUNT_HANDLE}. + * <p> + * This {@link Bundle} is not interpreted by Telecom, but passed as-is to the + * {@link ConnectionService} via the request extras when + * {@link ConnectionService#onCreateOutgoingConnection(PhoneAccountHandle, ConnectionRequest)} + * is called to initate the handover. + * @hide + */ + public static final String EXTRA_HANDOVER_EXTRAS = "android.telecom.extra.HANDOVER_EXTRAS"; + + /** + * Call event sent from Telecom to the handover {@link ConnectionService} via + * {@link Connection#onCallEvent(String, Bundle)} to inform a {@link Connection} that a handover + * to the {@link ConnectionService} has completed successfully. + * <p> + * A handover is initiated with the {@link #EVENT_REQUEST_HANDOVER} call event. + * @hide + */ + public static final String EVENT_HANDOVER_COMPLETE = + "android.telecom.event.HANDOVER_COMPLETE"; + + /** + * Call event sent from Telecom to the handover destination {@link ConnectionService} via + * {@link Connection#onCallEvent(String, Bundle)} to inform the handover destination that the + * source connection has disconnected. The {@link Bundle} parameter for the call event will be + * {@code null}. + * <p> + * A handover is initiated with the {@link #EVENT_REQUEST_HANDOVER} call event. + * @hide + */ + public static final String EVENT_HANDOVER_SOURCE_DISCONNECTED = + "android.telecom.event.HANDOVER_SOURCE_DISCONNECTED"; + + /** + * Call event sent from Telecom to the handover {@link ConnectionService} via + * {@link Connection#onCallEvent(String, Bundle)} to inform a {@link Connection} that a handover + * to the {@link ConnectionService} has failed. + * <p> + * A handover is initiated with the {@link #EVENT_REQUEST_HANDOVER} call event. + * @hide + */ + public static final String EVENT_HANDOVER_FAILED = + "android.telecom.event.HANDOVER_FAILED"; + + public static class Details { + + /** Call can currently be put on hold or unheld. */ + public static final int CAPABILITY_HOLD = 0x00000001; + + /** Call supports the hold feature. */ + public static final int CAPABILITY_SUPPORT_HOLD = 0x00000002; + + /** + * Calls within a conference can be merged. A {@link ConnectionService} has the option to + * add a {@link Conference} call before the child {@link Connection}s are merged. This is how + * CDMA-based {@link Connection}s are implemented. For these unmerged {@link Conference}s, this + * capability allows a merge button to be shown while the conference call is in the foreground + * of the in-call UI. + * <p> + * This is only intended for use by a {@link Conference}. + */ + public static final int CAPABILITY_MERGE_CONFERENCE = 0x00000004; + + /** + * Calls within a conference can be swapped between foreground and background. + * See {@link #CAPABILITY_MERGE_CONFERENCE} for additional information. + * <p> + * This is only intended for use by a {@link Conference}. + */ + public static final int CAPABILITY_SWAP_CONFERENCE = 0x00000008; + + /** + * @hide + */ + public static final int CAPABILITY_UNUSED_1 = 0x00000010; + + /** Call supports responding via text option. */ + public static final int CAPABILITY_RESPOND_VIA_TEXT = 0x00000020; + + /** Call can be muted. */ + public static final int CAPABILITY_MUTE = 0x00000040; + + /** + * Call supports conference call management. This capability only applies to {@link Conference} + * calls which can have {@link Connection}s as children. + */ + public static final int CAPABILITY_MANAGE_CONFERENCE = 0x00000080; + + /** + * Local device supports receiving video. + */ + public static final int CAPABILITY_SUPPORTS_VT_LOCAL_RX = 0x00000100; + + /** + * Local device supports transmitting video. + */ + public static final int CAPABILITY_SUPPORTS_VT_LOCAL_TX = 0x00000200; + + /** + * Local device supports bidirectional video calling. + */ + public static final int CAPABILITY_SUPPORTS_VT_LOCAL_BIDIRECTIONAL = + CAPABILITY_SUPPORTS_VT_LOCAL_RX | CAPABILITY_SUPPORTS_VT_LOCAL_TX; + + /** + * Remote device supports receiving video. + */ + public static final int CAPABILITY_SUPPORTS_VT_REMOTE_RX = 0x00000400; + + /** + * Remote device supports transmitting video. + */ + public static final int CAPABILITY_SUPPORTS_VT_REMOTE_TX = 0x00000800; + + /** + * Remote device supports bidirectional video calling. + */ + public static final int CAPABILITY_SUPPORTS_VT_REMOTE_BIDIRECTIONAL = + CAPABILITY_SUPPORTS_VT_REMOTE_RX | CAPABILITY_SUPPORTS_VT_REMOTE_TX; + + /** + * Call is able to be separated from its parent {@code Conference}, if any. + */ + public static final int CAPABILITY_SEPARATE_FROM_CONFERENCE = 0x00001000; + + /** + * Call is able to be individually disconnected when in a {@code Conference}. + */ + public static final int CAPABILITY_DISCONNECT_FROM_CONFERENCE = 0x00002000; + + /** + * Speed up audio setup for MT call. + * @hide + */ + public static final int CAPABILITY_SPEED_UP_MT_AUDIO = 0x00040000; + + /** + * Call can be upgraded to a video call. + * @hide + */ + public static final int CAPABILITY_CAN_UPGRADE_TO_VIDEO = 0x00080000; + + /** + * For video calls, indicates whether the outgoing video for the call can be paused using + * the {@link android.telecom.VideoProfile#STATE_PAUSED} VideoState. + */ + public static final int CAPABILITY_CAN_PAUSE_VIDEO = 0x00100000; + + /** + * Call sends responses through connection. + * @hide + */ + public static final int CAPABILITY_CAN_SEND_RESPONSE_VIA_CONNECTION = 0x00200000; + + /** + * When set, prevents a video {@code Call} from being downgraded to an audio-only call. + * <p> + * Should be set when the VideoState has the {@link VideoProfile#STATE_TX_ENABLED} or + * {@link VideoProfile#STATE_RX_ENABLED} bits set to indicate that the connection cannot be + * downgraded from a video call back to a VideoState of + * {@link VideoProfile#STATE_AUDIO_ONLY}. + * <p> + * Intuitively, a call which can be downgraded to audio should also have local and remote + * video + * capabilities (see {@link #CAPABILITY_SUPPORTS_VT_LOCAL_BIDIRECTIONAL} and + * {@link #CAPABILITY_SUPPORTS_VT_REMOTE_BIDIRECTIONAL}). + */ + public static final int CAPABILITY_CANNOT_DOWNGRADE_VIDEO_TO_AUDIO = 0x00400000; + + /** + * When set for an external call, indicates that this {@code Call} can be pulled from a + * remote device to the current device. + * <p> + * Should only be set on a {@code Call} where {@link #PROPERTY_IS_EXTERNAL_CALL} is set. + * <p> + * An {@link InCallService} will only see calls with this capability if it has the + * {@link TelecomManager#METADATA_INCLUDE_EXTERNAL_CALLS} metadata set to {@code true} + * in its manifest. + * <p> + * See {@link Connection#CAPABILITY_CAN_PULL_CALL} and + * {@link Connection#PROPERTY_IS_EXTERNAL_CALL}. + */ + public static final int CAPABILITY_CAN_PULL_CALL = 0x00800000; + + //****************************************************************************************** + // Next CAPABILITY value: 0x01000000 + //****************************************************************************************** + + /** + * Whether the call is currently a conference. + */ + public static final int PROPERTY_CONFERENCE = 0x00000001; + + /** + * Whether the call is a generic conference, where we do not know the precise state of + * participants in the conference (eg. on CDMA). + */ + public static final int PROPERTY_GENERIC_CONFERENCE = 0x00000002; + + /** + * Whether the call is made while the device is in emergency callback mode. + */ + public static final int PROPERTY_EMERGENCY_CALLBACK_MODE = 0x00000004; + + /** + * Connection is using WIFI. + */ + public static final int PROPERTY_WIFI = 0x00000008; + + /** + * Call is using high definition audio. + */ + public static final int PROPERTY_HIGH_DEF_AUDIO = 0x00000010; + + /** + * Whether the call is associated with the work profile. + */ + public static final int PROPERTY_ENTERPRISE_CALL = 0x00000020; + + /** + * When set, indicates that this {@code Call} does not actually exist locally for the + * {@link ConnectionService}. + * <p> + * Consider, for example, a scenario where a user has two phones with the same phone number. + * When a user places a call on one device, the telephony stack can represent that call on + * the other device by adding it to the {@link ConnectionService} with the + * {@link Connection#PROPERTY_IS_EXTERNAL_CALL} property set. + * <p> + * An {@link InCallService} will only see calls with this property if it has the + * {@link TelecomManager#METADATA_INCLUDE_EXTERNAL_CALLS} metadata set to {@code true} + * in its manifest. + * <p> + * See {@link Connection#PROPERTY_IS_EXTERNAL_CALL}. + */ + public static final int PROPERTY_IS_EXTERNAL_CALL = 0x00000040; + + /** + * Indicates that the call has CDMA Enhanced Voice Privacy enabled. + */ + public static final int PROPERTY_HAS_CDMA_VOICE_PRIVACY = 0x00000080; + + /** + * Indicates that the call is from a self-managed {@link ConnectionService}. + * <p> + * See also {@link Connection#PROPERTY_SELF_MANAGED} + */ + public static final int PROPERTY_SELF_MANAGED = 0x00000100; + + //****************************************************************************************** + // Next PROPERTY value: 0x00000200 + //****************************************************************************************** + + private final String mTelecomCallId; + private final Uri mHandle; + private final int mHandlePresentation; + private final String mCallerDisplayName; + private final int mCallerDisplayNamePresentation; + private final PhoneAccountHandle mAccountHandle; + private final int mCallCapabilities; + private final int mCallProperties; + private final int mSupportedAudioRoutes = CallAudioState.ROUTE_ALL; + private final DisconnectCause mDisconnectCause; + private final long mConnectTimeMillis; + private final GatewayInfo mGatewayInfo; + private final int mVideoState; + private final StatusHints mStatusHints; + private final Bundle mExtras; + private final Bundle mIntentExtras; + private final long mCreationTimeMillis; + + /** + * Whether the supplied capabilities supports the specified capability. + * + * @param capabilities A bit field of capabilities. + * @param capability The capability to check capabilities for. + * @return Whether the specified capability is supported. + */ + public static boolean can(int capabilities, int capability) { + return (capabilities & capability) == capability; + } + + /** + * Whether the capabilities of this {@code Details} supports the specified capability. + * + * @param capability The capability to check capabilities for. + * @return Whether the specified capability is supported. + */ + public boolean can(int capability) { + return can(mCallCapabilities, capability); + } + + /** + * Render a set of capability bits ({@code CAPABILITY_*}) as a human readable string. + * + * @param capabilities A capability bit field. + * @return A human readable string representation. + */ + public static String capabilitiesToString(int capabilities) { + StringBuilder builder = new StringBuilder(); + builder.append("[Capabilities:"); + if (can(capabilities, CAPABILITY_HOLD)) { + builder.append(" CAPABILITY_HOLD"); + } + if (can(capabilities, CAPABILITY_SUPPORT_HOLD)) { + builder.append(" CAPABILITY_SUPPORT_HOLD"); + } + if (can(capabilities, CAPABILITY_MERGE_CONFERENCE)) { + builder.append(" CAPABILITY_MERGE_CONFERENCE"); + } + if (can(capabilities, CAPABILITY_SWAP_CONFERENCE)) { + builder.append(" CAPABILITY_SWAP_CONFERENCE"); + } + if (can(capabilities, CAPABILITY_RESPOND_VIA_TEXT)) { + builder.append(" CAPABILITY_RESPOND_VIA_TEXT"); + } + if (can(capabilities, CAPABILITY_MUTE)) { + builder.append(" CAPABILITY_MUTE"); + } + if (can(capabilities, CAPABILITY_MANAGE_CONFERENCE)) { + builder.append(" CAPABILITY_MANAGE_CONFERENCE"); + } + if (can(capabilities, CAPABILITY_SUPPORTS_VT_LOCAL_RX)) { + builder.append(" CAPABILITY_SUPPORTS_VT_LOCAL_RX"); + } + if (can(capabilities, CAPABILITY_SUPPORTS_VT_LOCAL_TX)) { + builder.append(" CAPABILITY_SUPPORTS_VT_LOCAL_TX"); + } + if (can(capabilities, CAPABILITY_SUPPORTS_VT_LOCAL_BIDIRECTIONAL)) { + builder.append(" CAPABILITY_SUPPORTS_VT_LOCAL_BIDIRECTIONAL"); + } + if (can(capabilities, CAPABILITY_SUPPORTS_VT_REMOTE_RX)) { + builder.append(" CAPABILITY_SUPPORTS_VT_REMOTE_RX"); + } + if (can(capabilities, CAPABILITY_SUPPORTS_VT_REMOTE_TX)) { + builder.append(" CAPABILITY_SUPPORTS_VT_REMOTE_TX"); + } + if (can(capabilities, CAPABILITY_CANNOT_DOWNGRADE_VIDEO_TO_AUDIO)) { + builder.append(" CAPABILITY_CANNOT_DOWNGRADE_VIDEO_TO_AUDIO"); + } + if (can(capabilities, CAPABILITY_SUPPORTS_VT_REMOTE_BIDIRECTIONAL)) { + builder.append(" CAPABILITY_SUPPORTS_VT_REMOTE_BIDIRECTIONAL"); + } + if (can(capabilities, CAPABILITY_SPEED_UP_MT_AUDIO)) { + builder.append(" CAPABILITY_SPEED_UP_MT_AUDIO"); + } + if (can(capabilities, CAPABILITY_CAN_UPGRADE_TO_VIDEO)) { + builder.append(" CAPABILITY_CAN_UPGRADE_TO_VIDEO"); + } + if (can(capabilities, CAPABILITY_CAN_PAUSE_VIDEO)) { + builder.append(" CAPABILITY_CAN_PAUSE_VIDEO"); + } + if (can(capabilities, CAPABILITY_CAN_PULL_CALL)) { + builder.append(" CAPABILITY_CAN_PULL_CALL"); + } + builder.append("]"); + return builder.toString(); + } + + /** + * Whether the supplied properties includes the specified property. + * + * @param properties A bit field of properties. + * @param property The property to check properties for. + * @return Whether the specified property is supported. + */ + public static boolean hasProperty(int properties, int property) { + return (properties & property) == property; + } + + /** + * Whether the properties of this {@code Details} includes the specified property. + * + * @param property The property to check properties for. + * @return Whether the specified property is supported. + */ + public boolean hasProperty(int property) { + return hasProperty(mCallProperties, property); + } + + /** + * Render a set of property bits ({@code PROPERTY_*}) as a human readable string. + * + * @param properties A property bit field. + * @return A human readable string representation. + */ + public static String propertiesToString(int properties) { + StringBuilder builder = new StringBuilder(); + builder.append("[Properties:"); + if (hasProperty(properties, PROPERTY_CONFERENCE)) { + builder.append(" PROPERTY_CONFERENCE"); + } + if (hasProperty(properties, PROPERTY_GENERIC_CONFERENCE)) { + builder.append(" PROPERTY_GENERIC_CONFERENCE"); + } + if (hasProperty(properties, PROPERTY_WIFI)) { + builder.append(" PROPERTY_WIFI"); + } + if (hasProperty(properties, PROPERTY_HIGH_DEF_AUDIO)) { + builder.append(" PROPERTY_HIGH_DEF_AUDIO"); + } + if (hasProperty(properties, PROPERTY_EMERGENCY_CALLBACK_MODE)) { + builder.append(" PROPERTY_EMERGENCY_CALLBACK_MODE"); + } + if (hasProperty(properties, PROPERTY_IS_EXTERNAL_CALL)) { + builder.append(" PROPERTY_IS_EXTERNAL_CALL"); + } + if(hasProperty(properties, PROPERTY_HAS_CDMA_VOICE_PRIVACY)) { + builder.append(" PROPERTY_HAS_CDMA_VOICE_PRIVACY"); + } + builder.append("]"); + return builder.toString(); + } + + /** {@hide} */ + public String getTelecomCallId() { + return mTelecomCallId; + } + + /** + * @return The handle (e.g., phone number) to which the {@code Call} is currently + * connected. + */ + public Uri getHandle() { + return mHandle; + } + + /** + * @return The presentation requirements for the handle. See + * {@link TelecomManager} for valid values. + */ + public int getHandlePresentation() { + return mHandlePresentation; + } + + /** + * @return The display name for the caller. + */ + public String getCallerDisplayName() { + return mCallerDisplayName; + } + + /** + * @return The presentation requirements for the caller display name. See + * {@link TelecomManager} for valid values. + */ + public int getCallerDisplayNamePresentation() { + return mCallerDisplayNamePresentation; + } + + /** + * @return The {@code PhoneAccountHandle} whereby the {@code Call} is currently being + * routed. + */ + public PhoneAccountHandle getAccountHandle() { + return mAccountHandle; + } + + /** + * @return A bitmask of the capabilities of the {@code Call}, as defined by the various + * {@code CAPABILITY_*} constants in this class. + */ + public int getCallCapabilities() { + return mCallCapabilities; + } + + /** + * @return A bitmask of the properties of the {@code Call}, as defined by the various + * {@code PROPERTY_*} constants in this class. + */ + public int getCallProperties() { + return mCallProperties; + } + + /** + * @return a bitmask of the audio routes available for the call. + * + * @hide + */ + public int getSupportedAudioRoutes() { + return mSupportedAudioRoutes; + } + + /** + * @return For a {@link #STATE_DISCONNECTED} {@code Call}, the disconnect cause expressed + * by {@link android.telecom.DisconnectCause}. + */ + public DisconnectCause getDisconnectCause() { + return mDisconnectCause; + } + + /** + * Returns the time the {@link Call} connected (i.e. became active). This information is + * updated periodically, but user interfaces should not rely on this to display the "call + * time clock". For the time when the call was first added to Telecom, see + * {@link #getCreationTimeMillis()}. + * + * @return The time the {@link Call} connected in milliseconds since the epoch. + */ + public final long getConnectTimeMillis() { + return mConnectTimeMillis; + } + + /** + * @return Information about any calling gateway the {@code Call} may be using. + */ + public GatewayInfo getGatewayInfo() { + return mGatewayInfo; + } + + /** + * @return The video state of the {@code Call}. + */ + public int getVideoState() { + return mVideoState; + } + + /** + * @return The current {@link android.telecom.StatusHints}, or {@code null} if none + * have been set. + */ + public StatusHints getStatusHints() { + return mStatusHints; + } + + /** + * @return The extras associated with this call. + */ + public Bundle getExtras() { + return mExtras; + } + + /** + * @return The extras used with the original intent to place this call. + */ + public Bundle getIntentExtras() { + return mIntentExtras; + } + + /** + * Returns the time when the call was first created and added to Telecom. This is the same + * time that is logged as the start time in the Call Log (see + * {@link android.provider.CallLog.Calls#DATE}). To determine when the call was connected + * (became active), see {@link #getConnectTimeMillis()}. + * + * @return The creation time of the call, in millis since the epoch. + */ + public long getCreationTimeMillis() { + return mCreationTimeMillis; + } + + @Override + public boolean equals(Object o) { + if (o instanceof Details) { + Details d = (Details) o; + return + Objects.equals(mHandle, d.mHandle) && + Objects.equals(mHandlePresentation, d.mHandlePresentation) && + Objects.equals(mCallerDisplayName, d.mCallerDisplayName) && + Objects.equals(mCallerDisplayNamePresentation, + d.mCallerDisplayNamePresentation) && + Objects.equals(mAccountHandle, d.mAccountHandle) && + Objects.equals(mCallCapabilities, d.mCallCapabilities) && + Objects.equals(mCallProperties, d.mCallProperties) && + Objects.equals(mDisconnectCause, d.mDisconnectCause) && + Objects.equals(mConnectTimeMillis, d.mConnectTimeMillis) && + Objects.equals(mGatewayInfo, d.mGatewayInfo) && + Objects.equals(mVideoState, d.mVideoState) && + Objects.equals(mStatusHints, d.mStatusHints) && + areBundlesEqual(mExtras, d.mExtras) && + areBundlesEqual(mIntentExtras, d.mIntentExtras) && + Objects.equals(mCreationTimeMillis, d.mCreationTimeMillis); + } + return false; + } + + @Override + public int hashCode() { + return Objects.hash(mHandle, + mHandlePresentation, + mCallerDisplayName, + mCallerDisplayNamePresentation, + mAccountHandle, + mCallCapabilities, + mCallProperties, + mDisconnectCause, + mConnectTimeMillis, + mGatewayInfo, + mVideoState, + mStatusHints, + mExtras, + mIntentExtras, + mCreationTimeMillis); + } + + /** {@hide} */ + public Details( + String telecomCallId, + Uri handle, + int handlePresentation, + String callerDisplayName, + int callerDisplayNamePresentation, + PhoneAccountHandle accountHandle, + int capabilities, + int properties, + DisconnectCause disconnectCause, + long connectTimeMillis, + GatewayInfo gatewayInfo, + int videoState, + StatusHints statusHints, + Bundle extras, + Bundle intentExtras, + long creationTimeMillis) { + mTelecomCallId = telecomCallId; + mHandle = handle; + mHandlePresentation = handlePresentation; + mCallerDisplayName = callerDisplayName; + mCallerDisplayNamePresentation = callerDisplayNamePresentation; + mAccountHandle = accountHandle; + mCallCapabilities = capabilities; + mCallProperties = properties; + mDisconnectCause = disconnectCause; + mConnectTimeMillis = connectTimeMillis; + mGatewayInfo = gatewayInfo; + mVideoState = videoState; + mStatusHints = statusHints; + mExtras = extras; + mIntentExtras = intentExtras; + mCreationTimeMillis = creationTimeMillis; + } + + /** {@hide} */ + public static Details createFromParcelableCall(ParcelableCall parcelableCall) { + return new Details( + parcelableCall.getId(), + parcelableCall.getHandle(), + parcelableCall.getHandlePresentation(), + parcelableCall.getCallerDisplayName(), + parcelableCall.getCallerDisplayNamePresentation(), + parcelableCall.getAccountHandle(), + parcelableCall.getCapabilities(), + parcelableCall.getProperties(), + parcelableCall.getDisconnectCause(), + parcelableCall.getConnectTimeMillis(), + parcelableCall.getGatewayInfo(), + parcelableCall.getVideoState(), + parcelableCall.getStatusHints(), + parcelableCall.getExtras(), + parcelableCall.getIntentExtras(), + parcelableCall.getCreationTimeMillis()); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("[pa: "); + sb.append(mAccountHandle); + sb.append(", hdl: "); + sb.append(Log.pii(mHandle)); + sb.append(", caps: "); + sb.append(capabilitiesToString(mCallCapabilities)); + sb.append(", props: "); + sb.append(propertiesToString(mCallProperties)); + sb.append("]"); + return sb.toString(); + } + } + + /** + * Defines callbacks which inform the {@link InCallService} of changes to a {@link Call}. + * These callbacks can originate from the Telecom framework, or a {@link ConnectionService} + * implementation. + * <p> + * You can handle these callbacks by extending the {@link Callback} class and overriding the + * callbacks that your {@link InCallService} is interested in. The callback methods include the + * {@link Call} for which the callback applies, allowing reuse of a single instance of your + * {@link Callback} implementation, if desired. + * <p> + * Use {@link Call#registerCallback(Callback)} to register your callback(s). Ensure + * {@link Call#unregisterCallback(Callback)} is called when you no longer require callbacks + * (typically in {@link InCallService#onCallRemoved(Call)}). + * Note: Callbacks which occur before you call {@link Call#registerCallback(Callback)} will not + * reach your implementation of {@link Callback}, so it is important to register your callback + * as soon as your {@link InCallService} is notified of a new call via + * {@link InCallService#onCallAdded(Call)}. + */ + public static abstract class Callback { + /** + * Invoked when the state of this {@code Call} has changed. See {@link #getState()}. + * + * @param call The {@code Call} invoking this method. + * @param state The new state of the {@code Call}. + */ + public void onStateChanged(Call call, int state) {} + + /** + * Invoked when the parent of this {@code Call} has changed. See {@link #getParent()}. + * + * @param call The {@code Call} invoking this method. + * @param parent The new parent of the {@code Call}. + */ + public void onParentChanged(Call call, Call parent) {} + + /** + * Invoked when the children of this {@code Call} have changed. See {@link #getChildren()}. + * + * @param call The {@code Call} invoking this method. + * @param children The new children of the {@code Call}. + */ + public void onChildrenChanged(Call call, List<Call> children) {} + + /** + * Invoked when the details of this {@code Call} have changed. See {@link #getDetails()}. + * + * @param call The {@code Call} invoking this method. + * @param details A {@code Details} object describing the {@code Call}. + */ + public void onDetailsChanged(Call call, Details details) {} + + /** + * Invoked when the text messages that can be used as responses to the incoming + * {@code Call} are loaded from the relevant database. + * See {@link #getCannedTextResponses()}. + * + * @param call The {@code Call} invoking this method. + * @param cannedTextResponses The text messages useable as responses. + */ + public void onCannedTextResponsesLoaded(Call call, List<String> cannedTextResponses) {} + + /** + * Invoked when the post-dial sequence in the outgoing {@code Call} has reached a pause + * character. This causes the post-dial signals to stop pending user confirmation. An + * implementation should present this choice to the user and invoke + * {@link #postDialContinue(boolean)} when the user makes the choice. + * + * @param call The {@code Call} invoking this method. + * @param remainingPostDialSequence The post-dial characters that remain to be sent. + */ + public void onPostDialWait(Call call, String remainingPostDialSequence) {} + + /** + * Invoked when the {@code Call.VideoCall} of the {@code Call} has changed. + * + * @param call The {@code Call} invoking this method. + * @param videoCall The {@code Call.VideoCall} associated with the {@code Call}. + */ + public void onVideoCallChanged(Call call, InCallService.VideoCall videoCall) {} + + /** + * Invoked when the {@code Call} is destroyed. Clients should refrain from cleaning + * up their UI for the {@code Call} in response to state transitions. Specifically, + * clients should not assume that a {@link #onStateChanged(Call, int)} with a state of + * {@link #STATE_DISCONNECTED} is the final notification the {@code Call} will send. Rather, + * clients should wait for this method to be invoked. + * + * @param call The {@code Call} being destroyed. + */ + public void onCallDestroyed(Call call) {} + + /** + * Invoked upon changes to the set of {@code Call}s with which this {@code Call} can be + * conferenced. + * + * @param call The {@code Call} being updated. + * @param conferenceableCalls The {@code Call}s with which this {@code Call} can be + * conferenced. + */ + public void onConferenceableCallsChanged(Call call, List<Call> conferenceableCalls) {} + + /** + * Invoked when a {@link Call} receives an event from its associated {@link Connection}. + * <p> + * Where possible, the Call should make an attempt to handle {@link Connection} events which + * are part of the {@code android.telecom.*} namespace. The Call should ignore any events + * it does not wish to handle. Unexpected events should be handled gracefully, as it is + * possible that a {@link ConnectionService} has defined its own Connection events which a + * Call is not aware of. + * <p> + * See {@link Connection#sendConnectionEvent(String, Bundle)}. + * + * @param call The {@code Call} receiving the event. + * @param event The event. + * @param extras Extras associated with the connection event. + */ + public void onConnectionEvent(Call call, String event, Bundle extras) {} + + /** + * Invoked when the RTT mode changes for this call. + * @param call The call whose RTT mode has changed. + * @param mode the new RTT mode, one of + * {@link RttCall#RTT_MODE_FULL}, {@link RttCall#RTT_MODE_HCO}, + * or {@link RttCall#RTT_MODE_VCO} + */ + public void onRttModeChanged(Call call, int mode) {} + + /** + * Invoked when the call's RTT status changes, either from off to on or from on to off. + * @param call The call whose RTT status has changed. + * @param enabled whether RTT is now enabled or disabled + * @param rttCall the {@link RttCall} object to use for reading and writing if RTT is now + * on, null otherwise. + */ + public void onRttStatusChanged(Call call, boolean enabled, RttCall rttCall) {} + + /** + * Invoked when the remote end of the connection has requested that an RTT communication + * channel be opened. A response to this should be sent via {@link #respondToRttRequest} + * with the same ID that this method is invoked with. + * @param call The call which the RTT request was placed on + * @param id The ID of the request. + */ + public void onRttRequest(Call call, int id) {} + + /** + * Invoked when the RTT session failed to initiate for some reason, including rejection + * by the remote party. + * @param call The call which the RTT initiation failure occurred on. + * @param reason One of the status codes defined in + * {@link android.telecom.Connection.RttModifyStatus}, with the exception of + * {@link android.telecom.Connection.RttModifyStatus#SESSION_MODIFY_REQUEST_SUCCESS}. + */ + public void onRttInitiationFailure(Call call, int reason) {} + } + + /** + * A class that holds the state that describes the state of the RTT channel to the remote + * party, if it is active. + */ + public static final class RttCall { + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({RTT_MODE_INVALID, RTT_MODE_FULL, RTT_MODE_HCO, RTT_MODE_VCO}) + public @interface RttAudioMode {} + + /** + * For metrics use. Default value in the proto. + * @hide + */ + public static final int RTT_MODE_INVALID = 0; + + /** + * Indicates that there should be a bidirectional audio stream between the two parties + * on the call. + */ + public static final int RTT_MODE_FULL = 1; + + /** + * Indicates that the local user should be able to hear the audio stream from the remote + * user, but not vice versa. Equivalent to muting the microphone. + */ + public static final int RTT_MODE_HCO = 2; + + /** + * Indicates that the remote user should be able to hear the audio stream from the local + * user, but not vice versa. Equivalent to setting the volume to zero. + */ + public static final int RTT_MODE_VCO = 3; + + private static final int READ_BUFFER_SIZE = 1000; + + private InputStreamReader mReceiveStream; + private OutputStreamWriter mTransmitStream; + private int mRttMode; + private final InCallAdapter mInCallAdapter; + private final String mTelecomCallId; + private char[] mReadBuffer = new char[READ_BUFFER_SIZE]; + + /** + * @hide + */ + public RttCall(String telecomCallId, InputStreamReader receiveStream, + OutputStreamWriter transmitStream, int mode, InCallAdapter inCallAdapter) { + mTelecomCallId = telecomCallId; + mReceiveStream = receiveStream; + mTransmitStream = transmitStream; + mRttMode = mode; + mInCallAdapter = inCallAdapter; + } + + /** + * Returns the current RTT audio mode. + * @return Current RTT audio mode. One of {@link #RTT_MODE_FULL}, {@link #RTT_MODE_VCO}, or + * {@link #RTT_MODE_HCO}. + */ + public int getRttAudioMode() { + return mRttMode; + } + + /** + * Sets the RTT audio mode. The requested mode change will be communicated through + * {@link Callback#onRttModeChanged(Call, int)}. + * @param mode The desired RTT audio mode, one of {@link #RTT_MODE_FULL}, + * {@link #RTT_MODE_VCO}, or {@link #RTT_MODE_HCO}. + */ + public void setRttMode(@RttAudioMode int mode) { + mInCallAdapter.setRttMode(mTelecomCallId, mode); + } + + /** + * Writes the string {@param input} into the outgoing text stream for this RTT call. Since + * RTT transmits text in real-time, this method should be called once for each character + * the user enters into the device. + * + * This method is not thread-safe -- calling it from multiple threads simultaneously may + * lead to interleaved text. + * @param input The message to send to the remote user. + */ + public void write(String input) throws IOException { + mTransmitStream.write(input); + mTransmitStream.flush(); + } + + /** + * Reads a string from the remote user, blocking if there is no data available. Returns + * {@code null} if the RTT conversation has been terminated and there is no further data + * to read. + * + * This method is not thread-safe -- calling it from multiple threads simultaneously may + * lead to interleaved text. + * @return A string containing text sent by the remote user, or {@code null} if the + * conversation has been terminated or if there was an error while reading. + */ + public String read() { + try { + int numRead = mReceiveStream.read(mReadBuffer, 0, READ_BUFFER_SIZE); + if (numRead < 0) { + return null; + } + return new String(mReadBuffer, 0, numRead); + } catch (IOException e) { + Log.w(this, "Exception encountered when reading from InputStreamReader: %s", e); + return null; + } + } + + /** + * Non-blocking version of {@link #read()}. Returns {@code null} if there is nothing to + * be read. + * @return A string containing text entered by the user, or {@code null} if the user has + * not entered any new text yet. + */ + public String readImmediately() throws IOException { + if (mReceiveStream.ready()) { + int numRead = mReceiveStream.read(mReadBuffer, 0, READ_BUFFER_SIZE); + if (numRead < 0) { + return null; + } + return new String(mReadBuffer, 0, numRead); + } else { + return null; + } + } + } + + /** + * @deprecated Use {@code Call.Callback} instead. + * @hide + */ + @Deprecated + @SystemApi + public static abstract class Listener extends Callback { } + + private final Phone mPhone; + private final String mTelecomCallId; + private final InCallAdapter mInCallAdapter; + private final List<String> mChildrenIds = new ArrayList<>(); + private final List<Call> mChildren = new ArrayList<>(); + private final List<Call> mUnmodifiableChildren = Collections.unmodifiableList(mChildren); + private final List<CallbackRecord<Callback>> mCallbackRecords = new CopyOnWriteArrayList<>(); + private final List<Call> mConferenceableCalls = new ArrayList<>(); + private final List<Call> mUnmodifiableConferenceableCalls = + Collections.unmodifiableList(mConferenceableCalls); + + private boolean mChildrenCached; + private String mParentId = null; + private int mState; + private List<String> mCannedTextResponses = null; + private String mCallingPackage; + private int mTargetSdkVersion; + private String mRemainingPostDialSequence; + private VideoCallImpl mVideoCallImpl; + private RttCall mRttCall; + private Details mDetails; + private Bundle mExtras; + + /** + * Obtains the post-dial sequence remaining to be emitted by this {@code Call}, if any. + * + * @return The remaining post-dial sequence, or {@code null} if there is no post-dial sequence + * remaining or this {@code Call} is not in a post-dial state. + */ + public String getRemainingPostDialSequence() { + return mRemainingPostDialSequence; + } + + /** + * Instructs this {@link #STATE_RINGING} {@code Call} to answer. + * @param videoState The video state in which to answer the call. + */ + public void answer(int videoState) { + mInCallAdapter.answerCall(mTelecomCallId, videoState); + } + + /** + * Instructs this {@link #STATE_RINGING} {@code Call} to reject. + * + * @param rejectWithMessage Whether to reject with a text message. + * @param textMessage An optional text message with which to respond. + */ + public void reject(boolean rejectWithMessage, String textMessage) { + mInCallAdapter.rejectCall(mTelecomCallId, rejectWithMessage, textMessage); + } + + /** + * Instructs this {@code Call} to disconnect. + */ + public void disconnect() { + mInCallAdapter.disconnectCall(mTelecomCallId); + } + + /** + * Instructs this {@code Call} to go on hold. + */ + public void hold() { + mInCallAdapter.holdCall(mTelecomCallId); + } + + /** + * Instructs this {@link #STATE_HOLDING} call to release from hold. + */ + public void unhold() { + mInCallAdapter.unholdCall(mTelecomCallId); + } + + /** + * Instructs this {@code Call} to play a dual-tone multi-frequency signaling (DTMF) tone. + * + * Any other currently playing DTMF tone in the specified call is immediately stopped. + * + * @param digit A character representing the DTMF digit for which to play the tone. This + * value must be one of {@code '0'} through {@code '9'}, {@code '*'} or {@code '#'}. + */ + public void playDtmfTone(char digit) { + mInCallAdapter.playDtmfTone(mTelecomCallId, digit); + } + + /** + * Instructs this {@code Call} to stop any dual-tone multi-frequency signaling (DTMF) tone + * currently playing. + * + * DTMF tones are played by calling {@link #playDtmfTone(char)}. If no DTMF tone is + * currently playing, this method will do nothing. + */ + public void stopDtmfTone() { + mInCallAdapter.stopDtmfTone(mTelecomCallId); + } + + /** + * Instructs this {@code Call} to continue playing a post-dial DTMF string. + * + * A post-dial DTMF string is a string of digits entered after a phone number, when dialed, + * that are immediately sent as DTMF tones to the recipient as soon as the connection is made. + * + * If the DTMF string contains a {@link TelecomManager#DTMF_CHARACTER_PAUSE} symbol, this + * {@code Call} will temporarily pause playing the tones for a pre-defined period of time. + * + * If the DTMF string contains a {@link TelecomManager#DTMF_CHARACTER_WAIT} symbol, this + * {@code Call} will pause playing the tones and notify callbacks via + * {@link Callback#onPostDialWait(Call, String)}. At this point, the in-call app + * should display to the user an indication of this state and an affordance to continue + * the postdial sequence. When the user decides to continue the postdial sequence, the in-call + * app should invoke the {@link #postDialContinue(boolean)} method. + * + * @param proceed Whether or not to continue with the post-dial sequence. + */ + public void postDialContinue(boolean proceed) { + mInCallAdapter.postDialContinue(mTelecomCallId, proceed); + } + + /** + * Notifies this {@code Call} that an account has been selected and to proceed with placing + * an outgoing call. Optionally sets this account as the default account. + */ + public void phoneAccountSelected(PhoneAccountHandle accountHandle, boolean setDefault) { + mInCallAdapter.phoneAccountSelected(mTelecomCallId, accountHandle, setDefault); + + } + + /** + * Instructs this {@code Call} to enter a conference. + * + * @param callToConferenceWith The other call with which to conference. + */ + public void conference(Call callToConferenceWith) { + if (callToConferenceWith != null) { + mInCallAdapter.conference(mTelecomCallId, callToConferenceWith.mTelecomCallId); + } + } + + /** + * Instructs this {@code Call} to split from any conference call with which it may be + * connected. + */ + public void splitFromConference() { + mInCallAdapter.splitFromConference(mTelecomCallId); + } + + /** + * Merges the calls within this conference. See {@link Details#CAPABILITY_MERGE_CONFERENCE}. + */ + public void mergeConference() { + mInCallAdapter.mergeConference(mTelecomCallId); + } + + /** + * Swaps the calls within this conference. See {@link Details#CAPABILITY_SWAP_CONFERENCE}. + */ + public void swapConference() { + mInCallAdapter.swapConference(mTelecomCallId); + } + + /** + * Initiates a request to the {@link ConnectionService} to pull an external call to the local + * device. + * <p> + * Calls to this method are ignored if the call does not have the + * {@link Call.Details#PROPERTY_IS_EXTERNAL_CALL} property set. + * <p> + * An {@link InCallService} will only see calls which support this method if it has the + * {@link TelecomManager#METADATA_INCLUDE_EXTERNAL_CALLS} metadata set to {@code true} + * in its manifest. + */ + public void pullExternalCall() { + // If this isn't an external call, ignore the request. + if (!mDetails.hasProperty(Details.PROPERTY_IS_EXTERNAL_CALL)) { + return; + } + + mInCallAdapter.pullExternalCall(mTelecomCallId); + } + + /** + * Sends a {@code Call} event from this {@code Call} to the associated {@link Connection} in + * the {@link ConnectionService}. + * <p> + * Call events are used to communicate point in time information from an {@link InCallService} + * to a {@link ConnectionService}. A {@link ConnectionService} implementation could define + * events which enable the {@link InCallService}, for example, toggle a unique feature of the + * {@link ConnectionService}. + * <p> + * A {@link ConnectionService} can communicate to the {@link InCallService} using + * {@link Connection#sendConnectionEvent(String, Bundle)}. + * <p> + * Events are exposed to {@link ConnectionService} implementations via + * {@link android.telecom.Connection#onCallEvent(String, Bundle)}. + * <p> + * No assumptions should be made as to how a {@link ConnectionService} will handle these events. + * The {@link InCallService} must assume that the {@link ConnectionService} could chose to + * ignore some events altogether. + * <p> + * Events should be fully qualified (e.g., {@code com.example.event.MY_EVENT}) to avoid + * conflicts between {@link InCallService} implementations. Further, {@link InCallService} + * implementations shall not re-purpose events in the {@code android.*} namespace, nor shall + * they define their own event types in this namespace. When defining a custom event type, + * ensure the contents of the extras {@link Bundle} is clearly defined. Extra keys for this + * bundle should be named similar to the event type (e.g. {@code com.example.extra.MY_EXTRA}). + * <p> + * When defining events and the associated extras, it is important to keep their behavior + * consistent when the associated {@link InCallService} is updated. Support for deprecated + * events/extras should me maintained to ensure backwards compatibility with older + * {@link ConnectionService} implementations which were built to support the older behavior. + * + * @param event The connection event. + * @param extras Bundle containing extra information associated with the event. + */ + public void sendCallEvent(String event, Bundle extras) { + mInCallAdapter.sendCallEvent(mTelecomCallId, event, extras); + } + + /** + * Sends an RTT upgrade request to the remote end of the connection. Success is not + * guaranteed, and notification of success will be via the + * {@link Callback#onRttStatusChanged(Call, boolean, RttCall)} callback. + */ + public void sendRttRequest() { + mInCallAdapter.sendRttRequest(mTelecomCallId); + } + + /** + * Responds to an RTT request received via the {@link Callback#onRttRequest(Call, int)} )} + * callback. + * The ID used here should be the same as the ID that was received via the callback. + * @param id The request ID received via {@link Callback#onRttRequest(Call, int)} + * @param accept {@code true} if the RTT request should be accepted, {@code false} otherwise. + */ + public void respondToRttRequest(int id, boolean accept) { + mInCallAdapter.respondToRttRequest(mTelecomCallId, id, accept); + } + + /** + * Terminate the RTT session on this call. The resulting state change will be notified via + * the {@link Callback#onRttStatusChanged(Call, boolean, RttCall)} callback. + */ + public void stopRtt() { + mInCallAdapter.stopRtt(mTelecomCallId); + } + + /** + * Adds some extras to this {@link Call}. Existing keys are replaced and new ones are + * added. + * <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. + * + * @param extras The extras to add. + */ + public final void putExtras(Bundle extras) { + if (extras == null) { + return; + } + + if (mExtras == null) { + mExtras = new Bundle(); + } + mExtras.putAll(extras); + mInCallAdapter.putExtras(mTelecomCallId, extras); + } + + /** + * Adds a boolean extra to this {@link Call}. + * + * @param key The extra key. + * @param value The value. + * @hide + */ + public final void putExtra(String key, boolean value) { + if (mExtras == null) { + mExtras = new Bundle(); + } + mExtras.putBoolean(key, value); + mInCallAdapter.putExtra(mTelecomCallId, key, value); + } + + /** + * Adds an integer extra to this {@link Call}. + * + * @param key The extra key. + * @param value The value. + * @hide + */ + public final void putExtra(String key, int value) { + if (mExtras == null) { + mExtras = new Bundle(); + } + mExtras.putInt(key, value); + mInCallAdapter.putExtra(mTelecomCallId, key, value); + } + + /** + * Adds a string extra to this {@link Call}. + * + * @param key The extra key. + * @param value The value. + * @hide + */ + public final void putExtra(String key, String value) { + if (mExtras == null) { + mExtras = new Bundle(); + } + mExtras.putString(key, value); + mInCallAdapter.putExtra(mTelecomCallId, key, value); + } + + /** + * Removes extras from this {@link Call}. + * + * @param keys The keys of the extras to remove. + */ + public final void removeExtras(List<String> keys) { + if (mExtras != null) { + for (String key : keys) { + mExtras.remove(key); + } + if (mExtras.size() == 0) { + mExtras = null; + } + } + mInCallAdapter.removeExtras(mTelecomCallId, keys); + } + + /** + * Removes extras from this {@link Call}. + * + * @param keys The keys of the extras to remove. + */ + public final void removeExtras(String ... keys) { + removeExtras(Arrays.asList(keys)); + } + + /** + * Obtains the parent of this {@code Call} in a conference, if any. + * + * @return The parent {@code Call}, or {@code null} if this {@code Call} is not a + * child of any conference {@code Call}s. + */ + public Call getParent() { + if (mParentId != null) { + return mPhone.internalGetCallByTelecomId(mParentId); + } + return null; + } + + /** + * Obtains the children of this conference {@code Call}, if any. + * + * @return The children of this {@code Call} if this {@code Call} is a conference, or an empty + * {@code List} otherwise. + */ + public List<Call> getChildren() { + if (!mChildrenCached) { + mChildrenCached = true; + mChildren.clear(); + + for(String id : mChildrenIds) { + Call call = mPhone.internalGetCallByTelecomId(id); + if (call == null) { + // At least one child was still not found, so do not save true for "cached" + mChildrenCached = false; + } else { + mChildren.add(call); + } + } + } + + return mUnmodifiableChildren; + } + + /** + * Returns the list of {@code Call}s with which this {@code Call} is allowed to conference. + * + * @return The list of conferenceable {@code Call}s. + */ + public List<Call> getConferenceableCalls() { + return mUnmodifiableConferenceableCalls; + } + + /** + * Obtains the state of this {@code Call}. + * + * @return A state value, chosen from the {@code STATE_*} constants. + */ + public int getState() { + return mState; + } + + /** + * Obtains a list of canned, pre-configured message responses to present to the user as + * ways of rejecting this {@code Call} using via a text message. + * + * @see #reject(boolean, String) + * + * @return A list of canned text message responses. + */ + public List<String> getCannedTextResponses() { + return mCannedTextResponses; + } + + /** + * Obtains an object that can be used to display video from this {@code Call}. + * + * @return An {@code Call.VideoCall}. + */ + public InCallService.VideoCall getVideoCall() { + return mVideoCallImpl; + } + + /** + * Obtains an object containing call details. + * + * @return A {@link Details} object. Depending on the state of the {@code Call}, the + * result may be {@code null}. + */ + public Details getDetails() { + return mDetails; + } + + /** + * Returns this call's RttCall object. The {@link RttCall} instance is used to send and + * receive RTT text data, as well as to change the RTT mode. + * @return A {@link Call.RttCall}. {@code null} if there is no active RTT connection. + */ + public @Nullable RttCall getRttCall() { + return mRttCall; + } + + /** + * Returns whether this call has an active RTT connection. + * @return true if there is a connection, false otherwise. + */ + public boolean isRttActive() { + return mRttCall != null; + } + + /** + * Registers a callback to this {@code Call}. + * + * @param callback A {@code Callback}. + */ + public void registerCallback(Callback callback) { + registerCallback(callback, new Handler()); + } + + /** + * Registers a callback to this {@code Call}. + * + * @param callback A {@code Callback}. + * @param handler A handler which command and status changes will be delivered to. + */ + public void registerCallback(Callback callback, Handler handler) { + unregisterCallback(callback); + // Don't allow new callback registration if the call is already being destroyed. + if (callback != null && handler != null && mState != STATE_DISCONNECTED) { + mCallbackRecords.add(new CallbackRecord<Callback>(callback, handler)); + } + } + + /** + * Unregisters a callback from this {@code Call}. + * + * @param callback A {@code Callback}. + */ + public void unregisterCallback(Callback callback) { + // Don't allow callback deregistration if the call is already being destroyed. + if (callback != null && mState != STATE_DISCONNECTED) { + for (CallbackRecord<Callback> record : mCallbackRecords) { + if (record.getCallback() == callback) { + mCallbackRecords.remove(record); + break; + } + } + } + } + + @Override + public String toString() { + return new StringBuilder(). + append("Call [id: "). + append(mTelecomCallId). + append(", state: "). + append(stateToString(mState)). + append(", details: "). + append(mDetails). + append("]").toString(); + } + + /** + * @param state An integer value of a {@code STATE_*} constant. + * @return A string representation of the value. + */ + private static String stateToString(int state) { + switch (state) { + case STATE_NEW: + return "NEW"; + case STATE_RINGING: + return "RINGING"; + case STATE_DIALING: + return "DIALING"; + case STATE_ACTIVE: + return "ACTIVE"; + case STATE_HOLDING: + return "HOLDING"; + case STATE_DISCONNECTED: + return "DISCONNECTED"; + case STATE_CONNECTING: + return "CONNECTING"; + case STATE_DISCONNECTING: + return "DISCONNECTING"; + case STATE_SELECT_PHONE_ACCOUNT: + return "SELECT_PHONE_ACCOUNT"; + default: + Log.w(Call.class, "Unknown state %d", state); + return "UNKNOWN"; + } + } + + /** + * Adds a listener to this {@code Call}. + * + * @param listener A {@code Listener}. + * @deprecated Use {@link #registerCallback} instead. + * @hide + */ + @Deprecated + @SystemApi + public void addListener(Listener listener) { + registerCallback(listener); + } + + /** + * Removes a listener from this {@code Call}. + * + * @param listener A {@code Listener}. + * @deprecated Use {@link #unregisterCallback} instead. + * @hide + */ + @Deprecated + @SystemApi + public void removeListener(Listener listener) { + unregisterCallback(listener); + } + + /** {@hide} */ + Call(Phone phone, String telecomCallId, InCallAdapter inCallAdapter, String callingPackage, + int targetSdkVersion) { + mPhone = phone; + mTelecomCallId = telecomCallId; + mInCallAdapter = inCallAdapter; + mState = STATE_NEW; + mCallingPackage = callingPackage; + mTargetSdkVersion = targetSdkVersion; + } + + /** {@hide} */ + Call(Phone phone, String telecomCallId, InCallAdapter inCallAdapter, int state, + String callingPackage, int targetSdkVersion) { + mPhone = phone; + mTelecomCallId = telecomCallId; + mInCallAdapter = inCallAdapter; + mState = state; + mCallingPackage = callingPackage; + mTargetSdkVersion = targetSdkVersion; + } + + /** {@hide} */ + final String internalGetCallId() { + return mTelecomCallId; + } + + /** {@hide} */ + final void internalUpdate(ParcelableCall parcelableCall, Map<String, Call> callIdMap) { + + // First, we update the internal state as far as possible before firing any updates. + Details details = Details.createFromParcelableCall(parcelableCall); + boolean detailsChanged = !Objects.equals(mDetails, details); + if (detailsChanged) { + mDetails = details; + } + + boolean cannedTextResponsesChanged = false; + if (mCannedTextResponses == null && parcelableCall.getCannedSmsResponses() != null + && !parcelableCall.getCannedSmsResponses().isEmpty()) { + mCannedTextResponses = + Collections.unmodifiableList(parcelableCall.getCannedSmsResponses()); + cannedTextResponsesChanged = true; + } + + VideoCallImpl newVideoCallImpl = parcelableCall.getVideoCallImpl(mCallingPackage, + mTargetSdkVersion); + boolean videoCallChanged = parcelableCall.isVideoCallProviderChanged() && + !Objects.equals(mVideoCallImpl, newVideoCallImpl); + if (videoCallChanged) { + mVideoCallImpl = newVideoCallImpl; + } + if (mVideoCallImpl != null) { + mVideoCallImpl.setVideoState(getDetails().getVideoState()); + } + + int state = parcelableCall.getState(); + boolean stateChanged = mState != state; + if (stateChanged) { + mState = state; + } + + String parentId = parcelableCall.getParentCallId(); + boolean parentChanged = !Objects.equals(mParentId, parentId); + if (parentChanged) { + mParentId = parentId; + } + + List<String> childCallIds = parcelableCall.getChildCallIds(); + boolean childrenChanged = !Objects.equals(childCallIds, mChildrenIds); + if (childrenChanged) { + mChildrenIds.clear(); + mChildrenIds.addAll(parcelableCall.getChildCallIds()); + mChildrenCached = false; + } + + List<String> conferenceableCallIds = parcelableCall.getConferenceableCallIds(); + List<Call> conferenceableCalls = new ArrayList<Call>(conferenceableCallIds.size()); + for (String otherId : conferenceableCallIds) { + if (callIdMap.containsKey(otherId)) { + conferenceableCalls.add(callIdMap.get(otherId)); + } + } + + if (!Objects.equals(mConferenceableCalls, conferenceableCalls)) { + mConferenceableCalls.clear(); + mConferenceableCalls.addAll(conferenceableCalls); + fireConferenceableCallsChanged(); + } + + boolean isRttChanged = false; + boolean rttModeChanged = false; + if (parcelableCall.getParcelableRttCall() != null && parcelableCall.getIsRttCallChanged()) { + ParcelableRttCall parcelableRttCall = parcelableCall.getParcelableRttCall(); + InputStreamReader receiveStream = new InputStreamReader( + new ParcelFileDescriptor.AutoCloseInputStream( + parcelableRttCall.getReceiveStream()), + StandardCharsets.UTF_8); + OutputStreamWriter transmitStream = new OutputStreamWriter( + new ParcelFileDescriptor.AutoCloseOutputStream( + parcelableRttCall.getTransmitStream()), + StandardCharsets.UTF_8); + RttCall newRttCall = new Call.RttCall(mTelecomCallId, + receiveStream, transmitStream, parcelableRttCall.getRttMode(), mInCallAdapter); + if (mRttCall == null) { + isRttChanged = true; + } else if (mRttCall.getRttAudioMode() != newRttCall.getRttAudioMode()) { + rttModeChanged = true; + } + mRttCall = newRttCall; + } else if (mRttCall != null && parcelableCall.getParcelableRttCall() == null + && parcelableCall.getIsRttCallChanged()) { + isRttChanged = true; + mRttCall = null; + } + + // Now we fire updates, ensuring that any client who listens to any of these notifications + // gets the most up-to-date state. + + if (stateChanged) { + fireStateChanged(mState); + } + if (detailsChanged) { + fireDetailsChanged(mDetails); + } + if (cannedTextResponsesChanged) { + fireCannedTextResponsesLoaded(mCannedTextResponses); + } + if (videoCallChanged) { + fireVideoCallChanged(mVideoCallImpl); + } + if (parentChanged) { + fireParentChanged(getParent()); + } + if (childrenChanged) { + fireChildrenChanged(getChildren()); + } + if (isRttChanged) { + fireOnIsRttChanged(mRttCall != null, mRttCall); + } + if (rttModeChanged) { + fireOnRttModeChanged(mRttCall.getRttAudioMode()); + } + + // If we have transitioned to DISCONNECTED, that means we need to notify clients and + // remove ourselves from the Phone. Note that we do this after completing all state updates + // so a client can cleanly transition all their UI to the state appropriate for a + // DISCONNECTED Call while still relying on the existence of that Call in the Phone's list. + if (mState == STATE_DISCONNECTED) { + fireCallDestroyed(); + } + } + + /** {@hide} */ + final void internalSetPostDialWait(String remaining) { + mRemainingPostDialSequence = remaining; + firePostDialWait(mRemainingPostDialSequence); + } + + /** {@hide} */ + final void internalSetDisconnected() { + if (mState != Call.STATE_DISCONNECTED) { + mState = Call.STATE_DISCONNECTED; + fireStateChanged(mState); + fireCallDestroyed(); + } + } + + /** {@hide} */ + final void internalOnConnectionEvent(String event, Bundle extras) { + fireOnConnectionEvent(event, extras); + } + + /** {@hide} */ + final void internalOnRttUpgradeRequest(final int requestId) { + for (CallbackRecord<Callback> record : mCallbackRecords) { + final Call call = this; + final Callback callback = record.getCallback(); + record.getHandler().post(() -> callback.onRttRequest(call, requestId)); + } + } + + /** @hide */ + final void internalOnRttInitiationFailure(int reason) { + for (CallbackRecord<Callback> record : mCallbackRecords) { + final Call call = this; + final Callback callback = record.getCallback(); + record.getHandler().post(() -> callback.onRttInitiationFailure(call, reason)); + } + } + + private void fireStateChanged(final int newState) { + for (CallbackRecord<Callback> record : mCallbackRecords) { + final Call call = this; + final Callback callback = record.getCallback(); + record.getHandler().post(new Runnable() { + @Override + public void run() { + callback.onStateChanged(call, newState); + } + }); + } + } + + private void fireParentChanged(final Call newParent) { + for (CallbackRecord<Callback> record : mCallbackRecords) { + final Call call = this; + final Callback callback = record.getCallback(); + record.getHandler().post(new Runnable() { + @Override + public void run() { + callback.onParentChanged(call, newParent); + } + }); + } + } + + private void fireChildrenChanged(final List<Call> children) { + for (CallbackRecord<Callback> record : mCallbackRecords) { + final Call call = this; + final Callback callback = record.getCallback(); + record.getHandler().post(new Runnable() { + @Override + public void run() { + callback.onChildrenChanged(call, children); + } + }); + } + } + + private void fireDetailsChanged(final Details details) { + for (CallbackRecord<Callback> record : mCallbackRecords) { + final Call call = this; + final Callback callback = record.getCallback(); + record.getHandler().post(new Runnable() { + @Override + public void run() { + callback.onDetailsChanged(call, details); + } + }); + } + } + + private void fireCannedTextResponsesLoaded(final List<String> cannedTextResponses) { + for (CallbackRecord<Callback> record : mCallbackRecords) { + final Call call = this; + final Callback callback = record.getCallback(); + record.getHandler().post(new Runnable() { + @Override + public void run() { + callback.onCannedTextResponsesLoaded(call, cannedTextResponses); + } + }); + } + } + + private void fireVideoCallChanged(final InCallService.VideoCall videoCall) { + for (CallbackRecord<Callback> record : mCallbackRecords) { + final Call call = this; + final Callback callback = record.getCallback(); + record.getHandler().post(new Runnable() { + @Override + public void run() { + callback.onVideoCallChanged(call, videoCall); + } + }); + } + } + + private void firePostDialWait(final String remainingPostDialSequence) { + for (CallbackRecord<Callback> record : mCallbackRecords) { + final Call call = this; + final Callback callback = record.getCallback(); + record.getHandler().post(new Runnable() { + @Override + public void run() { + callback.onPostDialWait(call, remainingPostDialSequence); + } + }); + } + } + + private void fireCallDestroyed() { + /** + * To preserve the ordering of the Call's onCallDestroyed callback and Phone's + * onCallRemoved callback, we remove this call from the Phone's record + * only once all of the registered onCallDestroyed callbacks are executed. + * All the callbacks get removed from our records as a part of this operation + * since onCallDestroyed is the final callback. + */ + final Call call = this; + if (mCallbackRecords.isEmpty()) { + // No callbacks registered, remove the call from Phone's record. + mPhone.internalRemoveCall(call); + } + for (final CallbackRecord<Callback> record : mCallbackRecords) { + final Callback callback = record.getCallback(); + record.getHandler().post(new Runnable() { + @Override + public void run() { + boolean isFinalRemoval = false; + RuntimeException toThrow = null; + try { + callback.onCallDestroyed(call); + } catch (RuntimeException e) { + toThrow = e; + } + synchronized(Call.this) { + mCallbackRecords.remove(record); + if (mCallbackRecords.isEmpty()) { + isFinalRemoval = true; + } + } + if (isFinalRemoval) { + mPhone.internalRemoveCall(call); + } + if (toThrow != null) { + throw toThrow; + } + } + }); + } + } + + private void fireConferenceableCallsChanged() { + for (CallbackRecord<Callback> record : mCallbackRecords) { + final Call call = this; + final Callback callback = record.getCallback(); + record.getHandler().post(new Runnable() { + @Override + public void run() { + callback.onConferenceableCallsChanged(call, mUnmodifiableConferenceableCalls); + } + }); + } + } + + /** + * Notifies listeners of an incoming connection event. + * <p> + * Connection events are issued via {@link Connection#sendConnectionEvent(String, Bundle)}. + * + * @param event + * @param extras + */ + private void fireOnConnectionEvent(final String event, final Bundle extras) { + for (CallbackRecord<Callback> record : mCallbackRecords) { + final Call call = this; + final Callback callback = record.getCallback(); + record.getHandler().post(new Runnable() { + @Override + public void run() { + callback.onConnectionEvent(call, event, extras); + } + }); + } + } + + /** + * Notifies listeners of an RTT on/off change + * + * @param enabled True if RTT is now enabled, false otherwise + */ + private void fireOnIsRttChanged(final boolean enabled, final RttCall rttCall) { + for (CallbackRecord<Callback> record : mCallbackRecords) { + final Call call = this; + final Callback callback = record.getCallback(); + record.getHandler().post(() -> callback.onRttStatusChanged(call, enabled, rttCall)); + } + } + + /** + * Notifies listeners of a RTT mode change + * + * @param mode The new RTT mode + */ + private void fireOnRttModeChanged(final int mode) { + for (CallbackRecord<Callback> record : mCallbackRecords) { + final Call call = this; + final Callback callback = record.getCallback(); + record.getHandler().post(() -> callback.onRttModeChanged(call, mode)); + } + } + + /** + * Determines if two bundles are equal. + * + * @param bundle The original bundle. + * @param newBundle The bundle to compare with. + * @retrun {@code true} if the bundles are equal, {@code false} otherwise. + */ + private static boolean areBundlesEqual(Bundle bundle, Bundle newBundle) { + if (bundle == null || newBundle == null) { + return bundle == newBundle; + } + + if (bundle.size() != newBundle.size()) { + return false; + } + + for(String key : bundle.keySet()) { + if (key != null) { + final Object value = bundle.get(key); + final Object newValue = newBundle.get(key); + if (!Objects.equals(value, newValue)) { + return false; + } + } + } + return true; + } +} diff --git a/android/telecom/CallAudioState.java b/android/telecom/CallAudioState.java new file mode 100644 index 00000000..f601d8b5 --- /dev/null +++ b/android/telecom/CallAudioState.java @@ -0,0 +1,213 @@ +/* + * Copyright (C) 2014 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.Parcel; +import android.os.Parcelable; + +import java.util.Locale; + +/** + * Encapsulates the telecom audio state, including the current audio routing, supported audio + * routing and mute. + */ +public final class CallAudioState implements Parcelable { + /** Direct the audio stream through the device's earpiece. */ + public static final int ROUTE_EARPIECE = 0x00000001; + + /** Direct the audio stream through Bluetooth. */ + public static final int ROUTE_BLUETOOTH = 0x00000002; + + /** Direct the audio stream through a wired headset. */ + public static final int ROUTE_WIRED_HEADSET = 0x00000004; + + /** Direct the audio stream through the device's speakerphone. */ + public static final int ROUTE_SPEAKER = 0x00000008; + + /** + * Direct the audio stream through the device's earpiece or wired headset if one is + * connected. + */ + public static final int ROUTE_WIRED_OR_EARPIECE = ROUTE_EARPIECE | ROUTE_WIRED_HEADSET; + + /** + * Bit mask of all possible audio routes. + * + * @hide + **/ + public static final int ROUTE_ALL = ROUTE_EARPIECE | ROUTE_BLUETOOTH | ROUTE_WIRED_HEADSET | + ROUTE_SPEAKER; + + private final boolean isMuted; + private final int route; + private final int supportedRouteMask; + + /** + * Constructor for a {@link CallAudioState} object. + * + * @param muted {@code true} if the call is muted, {@code false} otherwise. + * @param route The current audio route being used. + * Allowed values: + * {@link #ROUTE_EARPIECE} + * {@link #ROUTE_BLUETOOTH} + * {@link #ROUTE_WIRED_HEADSET} + * {@link #ROUTE_SPEAKER} + * @param supportedRouteMask Bit mask of all routes supported by this call. This should be a + * bitwise combination of the following values: + * {@link #ROUTE_EARPIECE} + * {@link #ROUTE_BLUETOOTH} + * {@link #ROUTE_WIRED_HEADSET} + * {@link #ROUTE_SPEAKER} + */ + public CallAudioState(boolean muted, int route, int supportedRouteMask) { + this.isMuted = muted; + this.route = route; + this.supportedRouteMask = supportedRouteMask; + } + + /** @hide */ + public CallAudioState(CallAudioState state) { + isMuted = state.isMuted(); + route = state.getRoute(); + supportedRouteMask = state.getSupportedRouteMask(); + } + + /** @hide */ + @SuppressWarnings("deprecation") + public CallAudioState(AudioState state) { + isMuted = state.isMuted(); + route = state.getRoute(); + supportedRouteMask = state.getSupportedRouteMask(); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (!(obj instanceof CallAudioState)) { + return false; + } + CallAudioState state = (CallAudioState) obj; + return isMuted() == state.isMuted() && getRoute() == state.getRoute() && + getSupportedRouteMask() == state.getSupportedRouteMask(); + } + + @Override + public String toString() { + return String.format(Locale.US, + "[AudioState isMuted: %b, route: %s, supportedRouteMask: %s]", + isMuted, + audioRouteToString(route), + audioRouteToString(supportedRouteMask)); + } + + /** + * @return {@code true} if the call is muted, {@code false} otherwise. + */ + public boolean isMuted() { + return isMuted; + } + + /** + * @return The current audio route being used. + */ + public int getRoute() { + return route; + } + + /** + * @return Bit mask of all routes supported by this call. + */ + public int getSupportedRouteMask() { + return supportedRouteMask; + } + + /** + * Converts the provided audio route into a human readable string representation. + * + * @param route to convert into a string. + * + * @return String representation of the provided audio route. + */ + public static String audioRouteToString(int route) { + if (route == 0 || (route & ~ROUTE_ALL) != 0x0) { + return "UNKNOWN"; + } + + StringBuffer buffer = new StringBuffer(); + if ((route & ROUTE_EARPIECE) == ROUTE_EARPIECE) { + listAppend(buffer, "EARPIECE"); + } + if ((route & ROUTE_BLUETOOTH) == ROUTE_BLUETOOTH) { + listAppend(buffer, "BLUETOOTH"); + } + if ((route & ROUTE_WIRED_HEADSET) == ROUTE_WIRED_HEADSET) { + listAppend(buffer, "WIRED_HEADSET"); + } + if ((route & ROUTE_SPEAKER) == ROUTE_SPEAKER) { + listAppend(buffer, "SPEAKER"); + } + + return buffer.toString(); + } + + /** + * Responsible for creating AudioState objects for deserialized Parcels. + */ + public static final Parcelable.Creator<CallAudioState> CREATOR = + new Parcelable.Creator<CallAudioState> () { + + @Override + public CallAudioState createFromParcel(Parcel source) { + boolean isMuted = source.readByte() == 0 ? false : true; + int route = source.readInt(); + int supportedRouteMask = source.readInt(); + return new CallAudioState(isMuted, route, supportedRouteMask); + } + + @Override + public CallAudioState[] newArray(int size) { + return new CallAudioState[size]; + } + }; + + /** + * {@inheritDoc} + */ + @Override + public int describeContents() { + return 0; + } + + /** + * Writes AudioState object into a serializeable Parcel. + */ + @Override + public void writeToParcel(Parcel destination, int flags) { + destination.writeByte((byte) (isMuted ? 1 : 0)); + destination.writeInt(route); + destination.writeInt(supportedRouteMask); + } + + private static void listAppend(StringBuffer buffer, String str) { + if (buffer.length() > 0) { + buffer.append(", "); + } + buffer.append(str); + } +} diff --git a/android/telecom/CallScreeningService.java b/android/telecom/CallScreeningService.java new file mode 100644 index 00000000..f62b1709 --- /dev/null +++ b/android/telecom/CallScreeningService.java @@ -0,0 +1,239 @@ +/* + * Copyright (C) 2016 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.SdkConstant; +import android.app.Service; +import android.content.Intent; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.Message; +import android.os.RemoteException; + +import com.android.internal.os.SomeArgs; +import com.android.internal.telecom.ICallScreeningService; +import com.android.internal.telecom.ICallScreeningAdapter; + +/** + * This service can be implemented by the default dialer (see + * {@link TelecomManager#getDefaultDialerPackage()}) to allow or disallow incoming calls before + * they are shown to a user. + * <p> + * Below is an example manifest registration for a {@code CallScreeningService}. + * <pre> + * {@code + * <service android:name="your.package.YourCallScreeningServiceImplementation" + * android:permission="android.permission.BIND_SCREENING_SERVICE"> + * <intent-filter> + * <action android:name="android.telecom.CallScreeningService"/> + * </intent-filter> + * </service> + * } + * </pre> + */ +public abstract class CallScreeningService extends Service { + /** + * The {@link Intent} that must be declared as handled by the service. + */ + @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION) + public static final String SERVICE_INTERFACE = "android.telecom.CallScreeningService"; + + private static final int MSG_SCREEN_CALL = 1; + + private final Handler mHandler = new Handler(Looper.getMainLooper()) { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_SCREEN_CALL: + SomeArgs args = (SomeArgs) msg.obj; + try { + mCallScreeningAdapter = (ICallScreeningAdapter) args.arg1; + onScreenCall( + Call.Details.createFromParcelableCall((ParcelableCall) args.arg2)); + } finally { + args.recycle(); + } + break; + } + } + }; + + private final class CallScreeningBinder extends ICallScreeningService.Stub { + @Override + public void screenCall(ICallScreeningAdapter adapter, ParcelableCall call) { + Log.v(this, "screenCall"); + SomeArgs args = SomeArgs.obtain(); + args.arg1 = adapter; + args.arg2 = call; + mHandler.obtainMessage(MSG_SCREEN_CALL, args).sendToTarget(); + } + } + + private ICallScreeningAdapter mCallScreeningAdapter; + + /* + * Information about how to respond to an incoming call. + */ + public static class CallResponse { + private final boolean mShouldDisallowCall; + private final boolean mShouldRejectCall; + private final boolean mShouldSkipCallLog; + private final boolean mShouldSkipNotification; + + private CallResponse( + boolean shouldDisallowCall, + boolean shouldRejectCall, + boolean shouldSkipCallLog, + boolean shouldSkipNotification) { + if (!shouldDisallowCall + && (shouldRejectCall || shouldSkipCallLog || shouldSkipNotification)) { + throw new IllegalStateException("Invalid response state for allowed call."); + } + + mShouldDisallowCall = shouldDisallowCall; + mShouldRejectCall = shouldRejectCall; + mShouldSkipCallLog = shouldSkipCallLog; + mShouldSkipNotification = shouldSkipNotification; + } + + /* + * @return Whether the incoming call should be blocked. + */ + public boolean getDisallowCall() { + return mShouldDisallowCall; + } + + /* + * @return Whether the incoming call should be disconnected as if the user had manually + * rejected it. + */ + public boolean getRejectCall() { + return mShouldRejectCall; + } + + /* + * @return Whether the incoming call should not be displayed in the call log. + */ + public boolean getSkipCallLog() { + return mShouldSkipCallLog; + } + + /* + * @return Whether a missed call notification should not be shown for the incoming call. + */ + public boolean getSkipNotification() { + return mShouldSkipNotification; + } + + public static class Builder { + private boolean mShouldDisallowCall; + private boolean mShouldRejectCall; + private boolean mShouldSkipCallLog; + private boolean mShouldSkipNotification; + + /* + * Sets whether the incoming call should be blocked. + */ + public Builder setDisallowCall(boolean shouldDisallowCall) { + mShouldDisallowCall = shouldDisallowCall; + return this; + } + + /* + * Sets whether the incoming call should be disconnected as if the user had manually + * rejected it. This property should only be set to true if the call is disallowed. + */ + public Builder setRejectCall(boolean shouldRejectCall) { + mShouldRejectCall = shouldRejectCall; + return this; + } + + /* + * Sets whether the incoming call should not be displayed in the call log. This property + * should only be set to true if the call is disallowed. + */ + public Builder setSkipCallLog(boolean shouldSkipCallLog) { + mShouldSkipCallLog = shouldSkipCallLog; + return this; + } + + /* + * Sets whether a missed call notification should not be shown for the incoming call. + * This property should only be set to true if the call is disallowed. + */ + public Builder setSkipNotification(boolean shouldSkipNotification) { + mShouldSkipNotification = shouldSkipNotification; + return this; + } + + public CallResponse build() { + return new CallResponse( + mShouldDisallowCall, + mShouldRejectCall, + mShouldSkipCallLog, + mShouldSkipNotification); + } + } + } + + public CallScreeningService() { + } + + @Override + public IBinder onBind(Intent intent) { + Log.v(this, "onBind"); + return new CallScreeningBinder(); + } + + @Override + public boolean onUnbind(Intent intent) { + Log.v(this, "onUnbind"); + return false; + } + + /** + * Called when a new incoming call is added. + * {@link CallScreeningService#respondToCall(Call.Details, CallScreeningService.CallResponse)} + * should be called to allow or disallow the call. + * + * @param callDetails Information about a new incoming call, see {@link Call.Details}. + */ + public abstract void onScreenCall(Call.Details callDetails); + + /** + * Responds to the given call, either allowing it or disallowing it. + * + * @param callDetails The call to allow. + * @param response The {@link CallScreeningService.CallResponse} which contains information + * about how to respond to a call. + */ + public final void respondToCall(Call.Details callDetails, CallResponse response) { + try { + if (response.getDisallowCall()) { + mCallScreeningAdapter.disallowCall( + callDetails.getTelecomCallId(), + response.getRejectCall(), + !response.getSkipCallLog(), + !response.getSkipNotification()); + } else { + mCallScreeningAdapter.allowCall(callDetails.getTelecomCallId()); + } + } catch (RemoteException e) { + } + } +} diff --git a/android/telecom/CallbackRecord.java b/android/telecom/CallbackRecord.java new file mode 100644 index 00000000..1a81925d --- /dev/null +++ b/android/telecom/CallbackRecord.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2015 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.Handler; + + +/** + * This class is used to associate a generic callback of type T with a handler to which commands and + * status updates will be delivered to. + * + * @hide + */ +class CallbackRecord<T> { + private final T mCallback; + private final Handler mHandler; + + public CallbackRecord(T callback, Handler handler) { + mCallback = callback; + mHandler = handler; + } + + public T getCallback() { + return mCallback; + } + + public Handler getHandler() { + return mHandler; + } +} diff --git a/android/telecom/Conference.java b/android/telecom/Conference.java new file mode 100644 index 00000000..5fcff18a --- /dev/null +++ b/android/telecom/Conference.java @@ -0,0 +1,912 @@ +/* + * Copyright (C) 2014 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.annotation.Nullable; +import android.annotation.SystemApi; +import android.os.Bundle; +import android.os.SystemClock; +import android.telecom.Connection.VideoProvider; +import android.util.ArraySet; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.CopyOnWriteArraySet; + +/** + * Represents a conference call which can contain any number of {@link Connection} objects. + */ +public abstract class Conference extends Conferenceable { + + /** + * Used to indicate that the conference connection time is not specified. If not specified, + * Telecom will set the connect time. + */ + public static final long CONNECT_TIME_NOT_SPECIFIED = 0; + + /** @hide */ + public abstract static class Listener { + public void onStateChanged(Conference conference, int oldState, int newState) {} + public void onDisconnected(Conference conference, DisconnectCause disconnectCause) {} + public void onConnectionAdded(Conference conference, Connection connection) {} + public void onConnectionRemoved(Conference conference, Connection connection) {} + public void onConferenceableConnectionsChanged( + Conference conference, List<Connection> conferenceableConnections) {} + public void onDestroyed(Conference conference) {} + public void onConnectionCapabilitiesChanged( + Conference conference, int connectionCapabilities) {} + public void onConnectionPropertiesChanged( + Conference conference, int connectionProperties) {} + public void onVideoStateChanged(Conference c, int videoState) { } + public void onVideoProviderChanged(Conference c, Connection.VideoProvider videoProvider) {} + public void onStatusHintsChanged(Conference conference, StatusHints statusHints) {} + public void onExtrasChanged(Conference c, Bundle extras) {} + public void onExtrasRemoved(Conference c, List<String> keys) {} + } + + private final Set<Listener> mListeners = new CopyOnWriteArraySet<>(); + private final List<Connection> mChildConnections = new CopyOnWriteArrayList<>(); + private final List<Connection> mUnmodifiableChildConnections = + Collections.unmodifiableList(mChildConnections); + private final List<Connection> mConferenceableConnections = new ArrayList<>(); + private final List<Connection> mUnmodifiableConferenceableConnections = + Collections.unmodifiableList(mConferenceableConnections); + + private String mTelecomCallId; + private PhoneAccountHandle mPhoneAccount; + private CallAudioState mCallAudioState; + private int mState = Connection.STATE_NEW; + private DisconnectCause mDisconnectCause; + private int mConnectionCapabilities; + private int mConnectionProperties; + private String mDisconnectMessage; + private long mConnectTimeMillis = CONNECT_TIME_NOT_SPECIFIED; + private long mConnectElapsedTimeMillis = CONNECT_TIME_NOT_SPECIFIED; + private StatusHints mStatusHints; + private Bundle mExtras; + private Set<String> mPreviousExtraKeys; + private final Object mExtrasLock = new Object(); + + private final Connection.Listener mConnectionDeathListener = new Connection.Listener() { + @Override + public void onDestroyed(Connection c) { + if (mConferenceableConnections.remove(c)) { + fireOnConferenceableConnectionsChanged(); + } + } + }; + + /** + * Constructs a new Conference with a mandatory {@link PhoneAccountHandle} + * + * @param phoneAccount The {@code PhoneAccountHandle} associated with the conference. + */ + public Conference(PhoneAccountHandle phoneAccount) { + mPhoneAccount = phoneAccount; + } + + /** + * Returns the telecom internal call ID associated with this conference. + * + * @return The telecom call ID. + * @hide + */ + public final String getTelecomCallId() { + return mTelecomCallId; + } + + /** + * Sets the telecom internal call ID associated with this conference. + * + * @param telecomCallId The telecom call ID. + * @hide + */ + public final void setTelecomCallId(String telecomCallId) { + mTelecomCallId = telecomCallId; + } + + /** + * Returns the {@link PhoneAccountHandle} the conference call is being placed through. + * + * @return A {@code PhoneAccountHandle} object representing the PhoneAccount of the conference. + */ + public final PhoneAccountHandle getPhoneAccountHandle() { + return mPhoneAccount; + } + + /** + * Returns the list of connections currently associated with the conference call. + * + * @return A list of {@code Connection} objects which represent the children of the conference. + */ + public final List<Connection> getConnections() { + return mUnmodifiableChildConnections; + } + + /** + * Gets the state of the conference call. See {@link Connection} for valid values. + * + * @return A constant representing the state the conference call is currently in. + */ + public final int getState() { + return mState; + } + + /** + * Returns the capabilities of the conference. See {@code CAPABILITY_*} constants in class + * {@link Connection} for valid values. + * + * @return A bitmask of the capabilities of the conference call. + */ + public final int getConnectionCapabilities() { + return mConnectionCapabilities; + } + + /** + * Returns the properties of the conference. See {@code PROPERTY_*} constants in class + * {@link Connection} for valid values. + * + * @return A bitmask of the properties of the conference call. + */ + public final int getConnectionProperties() { + return mConnectionProperties; + } + + /** + * Whether the given capabilities support the specified capability. + * + * @param capabilities A capability bit field. + * @param capability The capability to check capabilities for. + * @return Whether the specified capability is supported. + * @hide + */ + public static boolean can(int capabilities, int capability) { + return (capabilities & capability) != 0; + } + + /** + * Whether the capabilities of this {@code Connection} supports the specified capability. + * + * @param capability The capability to check capabilities for. + * @return Whether the specified capability is supported. + * @hide + */ + public boolean can(int capability) { + return can(mConnectionCapabilities, capability); + } + + /** + * Removes the specified capability from the set of capabilities of this {@code Conference}. + * + * @param capability The capability to remove from the set. + * @hide + */ + public void removeCapability(int capability) { + int newCapabilities = mConnectionCapabilities; + newCapabilities &= ~capability; + + setConnectionCapabilities(newCapabilities); + } + + /** + * Adds the specified capability to the set of capabilities of this {@code Conference}. + * + * @param capability The capability to add to the set. + * @hide + */ + public void addCapability(int capability) { + int newCapabilities = mConnectionCapabilities; + newCapabilities |= capability; + + setConnectionCapabilities(newCapabilities); + } + + /** + * @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 #getCallAudioState()} instead. + * @hide + */ + @Deprecated + @SystemApi + public final AudioState getAudioState() { + return new AudioState(mCallAudioState); + } + + /** + * @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. + */ + public final CallAudioState getCallAudioState() { + return mCallAudioState; + } + + /** + * Returns VideoProvider of the primary call. This can be null. + */ + public VideoProvider getVideoProvider() { + return null; + } + + /** + * Returns video state of the primary call. + */ + public int getVideoState() { + return VideoProfile.STATE_AUDIO_ONLY; + } + + /** + * Notifies the {@link Conference} when the Conference and all it's {@link Connection}s should + * be disconnected. + */ + public void onDisconnect() {} + + /** + * Notifies the {@link Conference} when the specified {@link Connection} should be separated + * from the conference call. + * + * @param connection The connection to separate. + */ + public void onSeparate(Connection connection) {} + + /** + * Notifies the {@link Conference} when the specified {@link Connection} should merged with the + * conference call. + * + * @param connection The {@code Connection} to merge. + */ + public void onMerge(Connection connection) {} + + /** + * Notifies the {@link Conference} when it should be put on hold. + */ + public void onHold() {} + + /** + * Notifies the {@link Conference} when it should be moved from a held to active state. + */ + public void onUnhold() {} + + /** + * Notifies the {@link Conference} when the child calls should be merged. Only invoked if the + * conference contains the capability {@link Connection#CAPABILITY_MERGE_CONFERENCE}. + */ + public void onMerge() {} + + /** + * Notifies the {@link Conference} when the child calls should be swapped. Only invoked if the + * conference contains the capability {@link Connection#CAPABILITY_SWAP_CONFERENCE}. + */ + public void onSwap() {} + + /** + * Notifies the {@link Conference} of a request to play a DTMF tone. + * + * @param c A DTMF character. + */ + public void onPlayDtmfTone(char c) {} + + /** + * Notifies the {@link Conference} of a request to stop any currently playing DTMF tones. + */ + public void onStopDtmfTone() {} + + /** + * Notifies the {@link Conference} that the {@link #getAudioState()} property has a new value. + * + * @param state The new call audio state. + * @deprecated Use {@link #onCallAudioStateChanged(CallAudioState)} instead. + * @hide + */ + @SystemApi + @Deprecated + public void onAudioStateChanged(AudioState state) {} + + /** + * Notifies the {@link Conference} that the {@link #getCallAudioState()} property has a new + * value. + * + * @param state The new call audio state. + */ + public void onCallAudioStateChanged(CallAudioState state) {} + + /** + * Notifies the {@link Conference} that a {@link Connection} has been added to it. + * + * @param connection The newly added connection. + */ + public void onConnectionAdded(Connection connection) {} + + /** + * Sets state to be on hold. + */ + public final void setOnHold() { + setState(Connection.STATE_HOLDING); + } + + /** + * Sets state to be dialing. + */ + public final void setDialing() { + setState(Connection.STATE_DIALING); + } + + /** + * Sets state to be active. + */ + public final void setActive() { + setState(Connection.STATE_ACTIVE); + } + + /** + * Sets state to disconnected. + * + * @param disconnectCause The reason for the disconnection, as described by + * {@link android.telecom.DisconnectCause}. + */ + public final void setDisconnected(DisconnectCause disconnectCause) { + mDisconnectCause = disconnectCause;; + setState(Connection.STATE_DISCONNECTED); + for (Listener l : mListeners) { + l.onDisconnected(this, mDisconnectCause); + } + } + + /** + * @return The {@link DisconnectCause} for this connection. + */ + public final DisconnectCause getDisconnectCause() { + return mDisconnectCause; + } + + /** + * Sets the capabilities of a conference. See {@code CAPABILITY_*} constants of class + * {@link Connection} for valid values. + * + * @param connectionCapabilities A bitmask of the {@code Capabilities} of the conference call. + */ + public final void setConnectionCapabilities(int connectionCapabilities) { + if (connectionCapabilities != mConnectionCapabilities) { + mConnectionCapabilities = connectionCapabilities; + + for (Listener l : mListeners) { + l.onConnectionCapabilitiesChanged(this, mConnectionCapabilities); + } + } + } + + /** + * Sets the properties of a conference. See {@code PROPERTY_*} constants of class + * {@link Connection} for valid values. + * + * @param connectionProperties A bitmask of the {@code Properties} of the conference call. + */ + public final void setConnectionProperties(int connectionProperties) { + if (connectionProperties != mConnectionProperties) { + mConnectionProperties = connectionProperties; + + for (Listener l : mListeners) { + l.onConnectionPropertiesChanged(this, mConnectionProperties); + } + } + } + + /** + * Adds the specified connection as a child of this conference. + * + * @param connection The connection to add. + * @return True if the connection was successfully added. + */ + public final boolean addConnection(Connection connection) { + Log.d(this, "Connection=%s, connection=", connection); + if (connection != null && !mChildConnections.contains(connection)) { + if (connection.setConference(this)) { + mChildConnections.add(connection); + onConnectionAdded(connection); + for (Listener l : mListeners) { + l.onConnectionAdded(this, connection); + } + return true; + } + } + return false; + } + + /** + * Removes the specified connection as a child of this conference. + * + * @param connection The connection to remove. + */ + public final void removeConnection(Connection connection) { + Log.d(this, "removing %s from %s", connection, mChildConnections); + if (connection != null && mChildConnections.remove(connection)) { + connection.resetConference(); + for (Listener l : mListeners) { + l.onConnectionRemoved(this, connection); + } + } + } + + /** + * Sets the connections with which this connection can be conferenced. + * + * @param conferenceableConnections The set of connections this connection can conference with. + */ + public final void setConferenceableConnections(List<Connection> conferenceableConnections) { + clearConferenceableList(); + for (Connection c : conferenceableConnections) { + // If statement checks for duplicates in input. It makes it N^2 but we're dealing with a + // small amount of items here. + if (!mConferenceableConnections.contains(c)) { + c.addConnectionListener(mConnectionDeathListener); + mConferenceableConnections.add(c); + } + } + fireOnConferenceableConnectionsChanged(); + } + + /** + * Set the video state for the conference. + * Valid values: {@link VideoProfile#STATE_AUDIO_ONLY}, + * {@link VideoProfile#STATE_BIDIRECTIONAL}, + * {@link VideoProfile#STATE_TX_ENABLED}, + * {@link VideoProfile#STATE_RX_ENABLED}. + * + * @param videoState The new video state. + */ + public final void setVideoState(Connection c, int videoState) { + Log.d(this, "setVideoState Conference: %s Connection: %s VideoState: %s", + this, c, videoState); + for (Listener l : mListeners) { + l.onVideoStateChanged(this, videoState); + } + } + + /** + * Sets the video connection provider. + * + * @param videoProvider The video provider. + */ + public final void setVideoProvider(Connection c, Connection.VideoProvider videoProvider) { + Log.d(this, "setVideoProvider Conference: %s Connection: %s VideoState: %s", + this, c, videoProvider); + for (Listener l : mListeners) { + l.onVideoProviderChanged(this, videoProvider); + } + } + + private final void fireOnConferenceableConnectionsChanged() { + for (Listener l : mListeners) { + l.onConferenceableConnectionsChanged(this, getConferenceableConnections()); + } + } + + /** + * Returns the connections with which this connection can be conferenced. + */ + public final List<Connection> getConferenceableConnections() { + return mUnmodifiableConferenceableConnections; + } + + /** + * Tears down the conference object and any of its current connections. + */ + public final void destroy() { + Log.d(this, "destroying conference : %s", this); + // Tear down the children. + for (Connection connection : mChildConnections) { + Log.d(this, "removing connection %s", connection); + removeConnection(connection); + } + + // If not yet disconnected, set the conference call as disconnected first. + if (mState != Connection.STATE_DISCONNECTED) { + Log.d(this, "setting to disconnected"); + setDisconnected(new DisconnectCause(DisconnectCause.LOCAL)); + } + + // ...and notify. + for (Listener l : mListeners) { + l.onDestroyed(this); + } + } + + /** + * Add a listener to be notified of a state change. + * + * @param listener The new listener. + * @return This conference. + * @hide + */ + public final Conference addListener(Listener listener) { + mListeners.add(listener); + return this; + } + + /** + * Removes the specified listener. + * + * @param listener The listener to remove. + * @return This conference. + * @hide + */ + public final Conference removeListener(Listener listener) { + mListeners.remove(listener); + return this; + } + + /** + * Retrieves the primary connection associated with the conference. The primary connection is + * the connection from which the conference will retrieve its current state. + * + * @return The primary connection. + * @hide + */ + @SystemApi + public Connection getPrimaryConnection() { + if (mUnmodifiableChildConnections == null || mUnmodifiableChildConnections.isEmpty()) { + return null; + } + return mUnmodifiableChildConnections.get(0); + } + + /** + * @hide + * @deprecated Use {@link #setConnectionTime}. + */ + @Deprecated + @SystemApi + public final void setConnectTimeMillis(long connectTimeMillis) { + setConnectionTime(connectTimeMillis); + } + + /** + * Sets the connection start time of the {@code Conference}. Should be specified in wall-clock + * time returned by {@link System#currentTimeMillis()}. + * <p> + * When setting the connection time, you should always set the connection elapsed time via + * {@link #setConnectionElapsedTime(long)}. + * + * @param connectionTimeMillis The connection time, in milliseconds. + */ + public final void setConnectionTime(long connectionTimeMillis) { + mConnectTimeMillis = connectionTimeMillis; + } + + /** + * Sets the elapsed time since system boot when the {@link Conference} was connected. + * This is used to determine the duration of the {@link Conference}. + * <p> + * When setting the connection elapsed time, you should always set the connection time via + * {@link #setConnectionTime(long)}. + * + * @param connectionElapsedTime The connection time, as measured by + * {@link SystemClock#elapsedRealtime()}. + */ + public final void setConnectionElapsedTime(long connectionElapsedTime) { + mConnectElapsedTimeMillis = connectionElapsedTime; + } + + /** + * @hide + * @deprecated Use {@link #getConnectionTime}. + */ + @Deprecated + @SystemApi + public final long getConnectTimeMillis() { + return getConnectionTime(); + } + + /** + * Retrieves the connection start time of the {@code Conference}, if specified. A value of + * {@link #CONNECT_TIME_NOT_SPECIFIED} indicates that Telecom should determine the start time + * of the conference. + * + * @return The time at which the {@code Conference} was connected. + */ + public final long getConnectionTime() { + return mConnectTimeMillis; + } + + /** + * Retrieves the connection start time of the {@link Conference}, if specified. A value of + * {@link #CONNECT_TIME_NOT_SPECIFIED} indicates that Telecom should determine the start time + * of the conference. + * + * This is based on the value of {@link SystemClock#elapsedRealtime()} to ensure that it is not + * impacted by wall clock changes (user initiated, network initiated, time zone change, etc). + * + * @return The elapsed time at which the {@link Conference} was connected. + * @hide + */ + public final long getConnectElapsedTime() { + return mConnectElapsedTimeMillis; + } + + /** + * Inform this Conference that the state of its audio output has been changed externally. + * + * @param state The new audio state. + * @hide + */ + final void setCallAudioState(CallAudioState state) { + Log.d(this, "setCallAudioState %s", state); + mCallAudioState = state; + onAudioStateChanged(getAudioState()); + onCallAudioStateChanged(state); + } + + private void setState(int newState) { + if (newState != Connection.STATE_ACTIVE && + newState != Connection.STATE_HOLDING && + newState != Connection.STATE_DISCONNECTED) { + Log.w(this, "Unsupported state transition for Conference call.", + Connection.stateToString(newState)); + return; + } + + if (mState != newState) { + int oldState = mState; + mState = newState; + for (Listener l : mListeners) { + l.onStateChanged(this, oldState, newState); + } + } + } + + private final void clearConferenceableList() { + for (Connection c : mConferenceableConnections) { + c.removeConnectionListener(mConnectionDeathListener); + } + mConferenceableConnections.clear(); + } + + @Override + public String toString() { + return String.format(Locale.US, + "[State: %s,Capabilites: %s, VideoState: %s, VideoProvider: %s, ThisObject %s]", + Connection.stateToString(mState), + Call.Details.capabilitiesToString(mConnectionCapabilities), + getVideoState(), + getVideoProvider(), + super.toString()); + } + + /** + * Sets the label and icon status to display in the InCall UI. + * + * @param statusHints The status label and icon to set. + */ + public final void setStatusHints(StatusHints statusHints) { + mStatusHints = statusHints; + for (Listener l : mListeners) { + l.onStatusHintsChanged(this, statusHints); + } + } + + /** + * @return The status hints for this conference. + */ + public final StatusHints getStatusHints() { + return mStatusHints; + } + + /** + * Replaces all the extras associated with this {@code Conference}. + * <p> + * New or existing keys are replaced in the {@code Conference} extras. Keys which are no longer + * in the new extras, but were present the last time {@code setExtras} was called are removed. + * <p> + * Alternatively you may use the {@link #putExtras(Bundle)}, and + * {@link #removeExtras(String...)} methods to modify the extras. + * <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.extras.MY_EXTRA) to avoid conflicts. + * + * @param extras The extras associated with this {@code Conference}. + */ + public final void setExtras(@Nullable Bundle extras) { + // Keeping putExtras and removeExtras in the same lock so that this operation happens as a + // block instead of letting other threads put/remove while this method is running. + synchronized (mExtrasLock) { + // Add/replace any new or changed extras values. + putExtras(extras); + // If we have used "setExtras" in the past, compare the key set from the last invocation + // to the current one and remove any keys that went away. + if (mPreviousExtraKeys != null) { + List<String> toRemove = new ArrayList<String>(); + for (String oldKey : mPreviousExtraKeys) { + if (extras == null || !extras.containsKey(oldKey)) { + toRemove.add(oldKey); + } + } + + if (!toRemove.isEmpty()) { + removeExtras(toRemove); + } + } + + // Track the keys the last time set called setExtras. This way, the next time setExtras + // is called we can see if the caller has removed any extras values. + if (mPreviousExtraKeys == null) { + mPreviousExtraKeys = new ArraySet<String>(); + } + mPreviousExtraKeys.clear(); + if (extras != null) { + mPreviousExtraKeys.addAll(extras.keySet()); + } + } + } + + /** + * Adds some extras to this {@link Conference}. Existing keys are replaced and new ones are + * added. + * <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. + * + * @param extras The extras to add. + */ + public final void putExtras(@NonNull Bundle extras) { + if (extras == null) { + return; + } + + // Creating a Bundle clone so we don't have to synchronize on mExtrasLock while calling + // onExtrasChanged. + Bundle listenersBundle; + synchronized (mExtrasLock) { + if (mExtras == null) { + mExtras = new Bundle(); + } + mExtras.putAll(extras); + listenersBundle = new Bundle(mExtras); + } + + for (Listener l : mListeners) { + l.onExtrasChanged(this, new Bundle(listenersBundle)); + } + } + + /** + * Adds a boolean extra to this {@link Conference}. + * + * @param key The extra key. + * @param value The value. + * @hide + */ + public final void putExtra(String key, boolean value) { + Bundle newExtras = new Bundle(); + newExtras.putBoolean(key, value); + putExtras(newExtras); + } + + /** + * Adds an integer extra to this {@link Conference}. + * + * @param key The extra key. + * @param value The value. + * @hide + */ + public final void putExtra(String key, int value) { + Bundle newExtras = new Bundle(); + newExtras.putInt(key, value); + putExtras(newExtras); + } + + /** + * Adds a string extra to this {@link Conference}. + * + * @param key The extra key. + * @param value The value. + * @hide + */ + public final void putExtra(String key, String value) { + Bundle newExtras = new Bundle(); + newExtras.putString(key, value); + putExtras(newExtras); + } + + /** + * Removes extras from this {@link Conference}. + * + * @param keys The keys of the extras to remove. + */ + public final void removeExtras(List<String> keys) { + if (keys == null || keys.isEmpty()) { + return; + } + + synchronized (mExtrasLock) { + if (mExtras != null) { + for (String key : keys) { + mExtras.remove(key); + } + } + } + + List<String> unmodifiableKeys = Collections.unmodifiableList(keys); + for (Listener l : mListeners) { + l.onExtrasRemoved(this, unmodifiableKeys); + } + } + + /** + * Removes extras from this {@link Conference}. + * + * @param keys The keys of the extras to remove. + */ + public final void removeExtras(String ... keys) { + removeExtras(Arrays.asList(keys)); + } + + /** + * Returns the extras associated with this conference. + * <p> + * Extras should be updated using {@link #putExtras(Bundle)} and {@link #removeExtras(List)}. + * <p> + * Telecom or an {@link InCallService} can also update the extras via + * {@link android.telecom.Call#putExtras(Bundle)}, and + * {@link Call#removeExtras(List)}. + * <p> + * The conference is notified of changes to the extras made by Telecom or an + * {@link InCallService} by {@link #onExtrasChanged(Bundle)}. + * + * @return The extras associated with this connection. + */ + public final Bundle getExtras() { + return mExtras; + } + + /** + * Notifies this {@link Conference} of a change to the extras made outside the + * {@link ConnectionService}. + * <p> + * These extras changes can originate from Telecom itself, or from an {@link InCallService} via + * {@link android.telecom.Call#putExtras(Bundle)}, and + * {@link Call#removeExtras(List)}. + * + * @param extras The new extras bundle. + */ + public void onExtrasChanged(Bundle extras) {} + + /** + * Handles a change to extras received from Telecom. + * + * @param extras The new extras. + * @hide + */ + final void handleExtrasChanged(Bundle extras) { + Bundle b = null; + synchronized (mExtrasLock) { + mExtras = extras; + if (mExtras != null) { + b = new Bundle(mExtras); + } + } + onExtrasChanged(b); + } +} diff --git a/android/telecom/ConferenceParticipant.java b/android/telecom/ConferenceParticipant.java new file mode 100644 index 00000000..20b04ebe --- /dev/null +++ b/android/telecom/ConferenceParticipant.java @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2014 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.net.Uri; +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Parcelable representation of a participant's state in a conference call. + * @hide + */ +public class ConferenceParticipant implements Parcelable { + + /** + * The conference participant's handle (e.g., phone number). + */ + private final Uri mHandle; + + /** + * The display name for the participant. + */ + private final String mDisplayName; + + /** + * The endpoint Uri which uniquely identifies this conference participant. E.g. for an IMS + * conference call, this is the endpoint URI for the participant on the IMS conference server. + */ + private final Uri mEndpoint; + + /** + * The state of the participant in the conference. + * + * @see android.telecom.Connection + */ + private final int mState; + + /** + * Creates an instance of {@code ConferenceParticipant}. + * + * @param handle The conference participant's handle (e.g., phone number). + * @param displayName The display name for the participant. + * @param endpoint The enpoint Uri which uniquely identifies this conference participant. + * @param state The state of the participant in the conference. + */ + public ConferenceParticipant(Uri handle, String displayName, Uri endpoint, int state) { + mHandle = handle; + mDisplayName = displayName; + mEndpoint = endpoint; + mState = state; + } + + /** + * Responsible for creating {@code ConferenceParticipant} objects for deserialized Parcels. + */ + public static final Parcelable.Creator<ConferenceParticipant> CREATOR = + new Parcelable.Creator<ConferenceParticipant>() { + + @Override + public ConferenceParticipant createFromParcel(Parcel source) { + ClassLoader classLoader = ParcelableCall.class.getClassLoader(); + Uri handle = source.readParcelable(classLoader); + String displayName = source.readString(); + Uri endpoint = source.readParcelable(classLoader); + int state = source.readInt(); + return new ConferenceParticipant(handle, displayName, endpoint, state); + } + + @Override + public ConferenceParticipant[] newArray(int size) { + return new ConferenceParticipant[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + /** + * Writes the {@code ConferenceParticipant} to a parcel. + * + * @param dest The Parcel in which the object should be written. + * @param flags Additional flags about how the object should be written. + */ + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeParcelable(mHandle, 0); + dest.writeString(mDisplayName); + dest.writeParcelable(mEndpoint, 0); + dest.writeInt(mState); + } + + /** + * Builds a string representation of this instance. + * + * @return String representing the conference participant. + */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("[ConferenceParticipant Handle: "); + sb.append(Log.pii(mHandle)); + sb.append(" DisplayName: "); + sb.append(Log.pii(mDisplayName)); + sb.append(" Endpoint: "); + sb.append(Log.pii(mEndpoint)); + sb.append(" State: "); + sb.append(Connection.stateToString(mState)); + sb.append("]"); + return sb.toString(); + } + + /** + * The conference participant's handle (e.g., phone number). + */ + public Uri getHandle() { + return mHandle; + } + + /** + * The display name for the participant. + */ + public String getDisplayName() { + return mDisplayName; + } + + /** + * The enpoint Uri which uniquely identifies this conference participant. E.g. for an IMS + * conference call, this is the endpoint URI for the participant on the IMS conference server. + */ + public Uri getEndpoint() { + return mEndpoint; + } + + /** + * The state of the participant in the conference. + * + * @see android.telecom.Connection + */ + public int getState() { + return mState; + } +} diff --git a/android/telecom/Conferenceable.java b/android/telecom/Conferenceable.java new file mode 100644 index 00000000..bb6f2b8b --- /dev/null +++ b/android/telecom/Conferenceable.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2015 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; + +/** + * Interface used to identify entities with which another entity can participate in a conference + * call with. The {@link ConnectionService} implementation will only recognize + * {@link Conferenceable}s which are {@link Connection}s or {@link Conference}s. + */ +public abstract class Conferenceable { + Conferenceable() {} +} diff --git a/android/telecom/Connection.java b/android/telecom/Connection.java new file mode 100644 index 00000000..8ba934cc --- /dev/null +++ b/android/telecom/Connection.java @@ -0,0 +1,3120 @@ +/* + * Copyright (C) 2014 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 com.android.internal.os.SomeArgs; +import com.android.internal.telecom.IVideoCallback; +import com.android.internal.telecom.IVideoProvider; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.annotation.TestApi; +import android.app.Notification; +import android.content.Intent; +import android.hardware.camera2.CameraManager; +import android.net.Uri; +import android.os.Binder; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.Message; +import android.os.ParcelFileDescriptor; +import android.os.RemoteException; +import android.os.SystemClock; +import android.util.ArraySet; +import android.view.Surface; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +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; + +/** + * Represents a phone call or connection to a remote endpoint that carries voice and/or video + * traffic. + * <p> + * Implementations create a custom subclass of {@code Connection} and return it to the framework + * as the return value of + * {@link ConnectionService#onCreateIncomingConnection(PhoneAccountHandle, ConnectionRequest)} + * or + * {@link ConnectionService#onCreateOutgoingConnection(PhoneAccountHandle, ConnectionRequest)}. + * Implementations are then responsible for updating the state of the {@code Connection}, and + * must call {@link #destroy()} to signal to the framework that the {@code Connection} is no + * longer used and associated resources may be recovered. + * <p> + * Subclasses of {@code Connection} override the {@code on*} methods to provide the the + * {@link ConnectionService}'s implementation of calling functionality. The {@code on*} methods are + * called by Telecom to inform an instance of a {@code Connection} of actions specific to that + * {@code Connection} instance. + * <p> + * Basic call support requires overriding the following methods: {@link #onAnswer()}, + * {@link #onDisconnect()}, {@link #onReject()}, {@link #onAbort()} + * <p> + * Where a {@code Connection} has {@link #CAPABILITY_SUPPORT_HOLD}, the {@link #onHold()} and + * {@link #onUnhold()} methods should be overridden to provide hold support for the + * {@code Connection}. + * <p> + * Where a {@code Connection} supports a variation of video calling (e.g. the + * {@code CAPABILITY_SUPPORTS_VT_*} capability bits), {@link #onAnswer(int)} should be overridden + * to support answering a call as a video call. + * <p> + * Where a {@code Connection} has {@link #PROPERTY_IS_EXTERNAL_CALL} and + * {@link #CAPABILITY_CAN_PULL_CALL}, {@link #onPullExternalCall()} should be overridden to provide + * support for pulling the external call. + * <p> + * Where a {@code Connection} supports conference calling {@link #onSeparate()} should be + * overridden. + * <p> + * There are a number of other {@code on*} methods which a {@code Connection} can choose to + * implement, depending on whether it is concerned with the associated calls from Telecom. If, + * for example, call events from a {@link InCallService} are handled, + * {@link #onCallEvent(String, Bundle)} should be overridden. Another example is + * {@link #onExtrasChanged(Bundle)}, which should be overridden if the {@code Connection} wishes to + * make use of extra information provided via the {@link Call#putExtras(Bundle)} and + * {@link Call#removeExtras(String...)} methods. + */ +public abstract class Connection extends Conferenceable { + + /** + * The connection is initializing. This is generally the first state for a {@code Connection} + * returned by a {@link ConnectionService}. + */ + public static final int STATE_INITIALIZING = 0; + + /** + * The connection is new and not connected. + */ + public static final int STATE_NEW = 1; + + /** + * An incoming connection is in the ringing state. During this state, the user's ringer or + * vibration feature will be activated. + */ + public static final int STATE_RINGING = 2; + + /** + * An outgoing connection is in the dialing state. In this state the other party has not yet + * answered the call and the user traditionally hears a ringback tone. + */ + public static final int STATE_DIALING = 3; + + /** + * A connection is active. Both parties are connected to the call and can actively communicate. + */ + public static final int STATE_ACTIVE = 4; + + /** + * A connection is on hold. + */ + public static final int STATE_HOLDING = 5; + + /** + * A connection has been disconnected. This is the final state once the user has been + * disconnected from a call either locally, remotely or by an error in the service. + */ + public static final int STATE_DISCONNECTED = 6; + + /** + * The state of an external connection which is in the process of being pulled from a remote + * device to the local device. + * <p> + * A connection can only be in this state if the {@link #PROPERTY_IS_EXTERNAL_CALL} property and + * {@link #CAPABILITY_CAN_PULL_CALL} capability bits are set on the connection. + */ + public static final int STATE_PULLING_CALL = 7; + + /** + * Connection can currently be put on hold or unheld. This is distinct from + * {@link #CAPABILITY_SUPPORT_HOLD} in that although a connection may support 'hold' most times, + * it does not at the moment support the function. This can be true while the call is in the + * state {@link #STATE_DIALING}, for example. During this condition, an in-call UI may + * display a disabled 'hold' button. + */ + public static final int CAPABILITY_HOLD = 0x00000001; + + /** Connection supports the hold feature. */ + public static final int CAPABILITY_SUPPORT_HOLD = 0x00000002; + + /** + * Connections within a conference can be merged. A {@link ConnectionService} has the option to + * add a {@link Conference} before the child {@link Connection}s are merged. This is how + * CDMA-based {@link Connection}s are implemented. For these unmerged {@link Conference}s, this + * capability allows a merge button to be shown while the conference is in the foreground + * of the in-call UI. + * <p> + * This is only intended for use by a {@link Conference}. + */ + public static final int CAPABILITY_MERGE_CONFERENCE = 0x00000004; + + /** + * Connections within a conference can be swapped between foreground and background. + * See {@link #CAPABILITY_MERGE_CONFERENCE} for additional information. + * <p> + * This is only intended for use by a {@link Conference}. + */ + public static final int CAPABILITY_SWAP_CONFERENCE = 0x00000008; + + /** + * @hide + */ + public static final int CAPABILITY_UNUSED = 0x00000010; + + /** Connection supports responding via text option. */ + public static final int CAPABILITY_RESPOND_VIA_TEXT = 0x00000020; + + /** Connection can be muted. */ + public static final int CAPABILITY_MUTE = 0x00000040; + + /** + * Connection supports conference management. This capability only applies to + * {@link Conference}s which can have {@link Connection}s as children. + */ + public static final int CAPABILITY_MANAGE_CONFERENCE = 0x00000080; + + /** + * Local device supports receiving video. + */ + public static final int CAPABILITY_SUPPORTS_VT_LOCAL_RX = 0x00000100; + + /** + * Local device supports transmitting video. + */ + public static final int CAPABILITY_SUPPORTS_VT_LOCAL_TX = 0x00000200; + + /** + * Local device supports bidirectional video calling. + */ + public static final int CAPABILITY_SUPPORTS_VT_LOCAL_BIDIRECTIONAL = + CAPABILITY_SUPPORTS_VT_LOCAL_RX | CAPABILITY_SUPPORTS_VT_LOCAL_TX; + + /** + * Remote device supports receiving video. + */ + public static final int CAPABILITY_SUPPORTS_VT_REMOTE_RX = 0x00000400; + + /** + * Remote device supports transmitting video. + */ + public static final int CAPABILITY_SUPPORTS_VT_REMOTE_TX = 0x00000800; + + /** + * Remote device supports bidirectional video calling. + */ + public static final int CAPABILITY_SUPPORTS_VT_REMOTE_BIDIRECTIONAL = + CAPABILITY_SUPPORTS_VT_REMOTE_RX | CAPABILITY_SUPPORTS_VT_REMOTE_TX; + + /** + * Connection is able to be separated from its parent {@code Conference}, if any. + */ + public static final int CAPABILITY_SEPARATE_FROM_CONFERENCE = 0x00001000; + + /** + * Connection is able to be individually disconnected when in a {@code Conference}. + */ + public static final int CAPABILITY_DISCONNECT_FROM_CONFERENCE = 0x00002000; + + /** + * Un-used. + * @hide + */ + public static final int CAPABILITY_UNUSED_2 = 0x00004000; + + /** + * Un-used. + * @hide + */ + public static final int CAPABILITY_UNUSED_3 = 0x00008000; + + /** + * Un-used. + * @hide + */ + public static final int CAPABILITY_UNUSED_4 = 0x00010000; + + /** + * Un-used. + * @hide + */ + public static final int CAPABILITY_UNUSED_5 = 0x00020000; + + /** + * Speed up audio setup for MT call. + * @hide + */ + public static final int CAPABILITY_SPEED_UP_MT_AUDIO = 0x00040000; + + /** + * Call can be upgraded to a video call. + */ + public static final int CAPABILITY_CAN_UPGRADE_TO_VIDEO = 0x00080000; + + /** + * For video calls, indicates whether the outgoing video for the call can be paused using + * the {@link android.telecom.VideoProfile#STATE_PAUSED} VideoState. + */ + public static final int CAPABILITY_CAN_PAUSE_VIDEO = 0x00100000; + + /** + * For a conference, indicates the conference will not have child connections. + * <p> + * An example of a conference with child connections is a GSM conference call, where the radio + * retains connections to the individual participants of the conference. Another example is an + * IMS conference call where conference event package functionality is supported; in this case + * the conference server ensures the radio is aware of the participants in the conference, which + * are represented by child connections. + * <p> + * An example of a conference with no child connections is an IMS conference call with no + * conference event package support. Such a conference is represented by the radio as a single + * connection to the IMS conference server. + * <p> + * Indicating whether a conference has children or not is important to help user interfaces + * visually represent a conference. A conference with no children, for example, will have the + * conference connection shown in the list of calls on a Bluetooth device, where if the + * conference has children, only the children will be shown in the list of calls on a Bluetooth + * device. + * @hide + */ + public static final int CAPABILITY_CONFERENCE_HAS_NO_CHILDREN = 0x00200000; + + /** + * Indicates that the connection itself wants to handle any sort of reply response, rather than + * relying on SMS. + */ + public static final int CAPABILITY_CAN_SEND_RESPONSE_VIA_CONNECTION = 0x00400000; + + /** + * When set, prevents a video call from being downgraded to an audio-only call. + * <p> + * Should be set when the VideoState has the {@link VideoProfile#STATE_TX_ENABLED} or + * {@link VideoProfile#STATE_RX_ENABLED} bits set to indicate that the connection cannot be + * downgraded from a video call back to a VideoState of + * {@link VideoProfile#STATE_AUDIO_ONLY}. + * <p> + * Intuitively, a call which can be downgraded to audio should also have local and remote + * video + * capabilities (see {@link #CAPABILITY_SUPPORTS_VT_LOCAL_BIDIRECTIONAL} and + * {@link #CAPABILITY_SUPPORTS_VT_REMOTE_BIDIRECTIONAL}). + */ + public static final int CAPABILITY_CANNOT_DOWNGRADE_VIDEO_TO_AUDIO = 0x00800000; + + /** + * When set for an external connection, indicates that this {@code Connection} can be pulled + * from a remote device to the current device. + * <p> + * Should only be set on a {@code Connection} where {@link #PROPERTY_IS_EXTERNAL_CALL} + * is set. + */ + public static final int CAPABILITY_CAN_PULL_CALL = 0x01000000; + + //********************************************************************************************** + // Next CAPABILITY value: 0x02000000 + //********************************************************************************************** + + /** + * Indicates that the current device callback number should be shown. + * + * @hide + */ + public static final int PROPERTY_EMERGENCY_CALLBACK_MODE = 1<<0; + + /** + * Whether the call is a generic conference, where we do not know the precise state of + * participants in the conference (eg. on CDMA). + * + * @hide + */ + public static final int PROPERTY_GENERIC_CONFERENCE = 1<<1; + + /** + * Connection is using high definition audio. + * @hide + */ + public static final int PROPERTY_HIGH_DEF_AUDIO = 1<<2; + + /** + * Connection is using WIFI. + * @hide + */ + public static final int PROPERTY_WIFI = 1<<3; + + /** + * When set, indicates that the {@code Connection} does not actually exist locally for the + * {@link ConnectionService}. + * <p> + * Consider, for example, a scenario where a user has two devices with the same phone number. + * When a user places a call on one devices, the telephony stack can represent that call on the + * other device by adding is to the {@link ConnectionService} with the + * {@link #PROPERTY_IS_EXTERNAL_CALL} capability set. + * <p> + * An {@link ConnectionService} should not assume that all {@link InCallService}s will handle + * external connections. Only those {@link InCallService}s which have the + * {@link TelecomManager#METADATA_INCLUDE_EXTERNAL_CALLS} metadata set to {@code true} in its + * manifest will see external connections. + */ + public static final int PROPERTY_IS_EXTERNAL_CALL = 1<<4; + + /** + * Indicates that the connection has CDMA Enhanced Voice Privacy enabled. + */ + public static final int PROPERTY_HAS_CDMA_VOICE_PRIVACY = 1<<5; + + /** + * Indicates that the connection represents a downgraded IMS conference. + * @hide + */ + public static final int PROPERTY_IS_DOWNGRADED_CONFERENCE = 1<<6; + + /** + * Set by the framework to indicate that the {@link Connection} originated from a self-managed + * {@link ConnectionService}. + * <p> + * See {@link PhoneAccount#CAPABILITY_SELF_MANAGED}. + */ + public static final int PROPERTY_SELF_MANAGED = 1<<7; + + /** + * Set by the framework to indicate that a connection has an active RTT session associated with + * it. + * @hide + */ + @TestApi + public static final int PROPERTY_IS_RTT = 1 << 8; + + //********************************************************************************************** + // Next PROPERTY value: 1<<9 + //********************************************************************************************** + + /** + * Connection extra key used to store the last forwarded number associated with the current + * connection. Used to communicate to the user interface that the connection was forwarded via + * the specified number. + */ + public static final String EXTRA_LAST_FORWARDED_NUMBER = + "android.telecom.extra.LAST_FORWARDED_NUMBER"; + + /** + * Connection extra key used to store a child number associated with the current connection. + * Used to communicate to the user interface that the connection was received via + * a child address (i.e. phone number) associated with the {@link PhoneAccount}'s primary + * address. + */ + public static final String EXTRA_CHILD_ADDRESS = "android.telecom.extra.CHILD_ADDRESS"; + + /** + * Connection extra key used to store the subject for an incoming call. The user interface can + * query this extra and display its contents for incoming calls. Will only be used if the + * {@link PhoneAccount} supports the capability {@link PhoneAccount#CAPABILITY_CALL_SUBJECT}. + */ + public static final String EXTRA_CALL_SUBJECT = "android.telecom.extra.CALL_SUBJECT"; + + /** + * Boolean connection extra key set on a {@link Connection} in + * {@link Connection#STATE_RINGING} state to indicate that answering the call will cause the + * current active foreground call to be dropped. + */ + public static final String EXTRA_ANSWERING_DROPS_FG_CALL = + "android.telecom.extra.ANSWERING_DROPS_FG_CALL"; + + /** + * String connection extra key set on a {@link Connection} in {@link Connection#STATE_RINGING} + * state to indicate the name of the third-party app which is responsible for the current + * foreground call. + * <p> + * Used when {@link #EXTRA_ANSWERING_DROPS_FG_CALL} is true to ensure that the default Phone app + * is able to inform the user that answering the new incoming call will cause a call owned by + * another app to be dropped when the incoming call is answered. + */ + public static final String EXTRA_ANSWERING_DROPS_FG_CALL_APP_NAME = + "android.telecom.extra.ANSWERING_DROPS_FG_CALL_APP_NAME"; + + /** + * Boolean connection extra key on a {@link Connection} which indicates that adding an + * additional call is disallowed. + * @hide + */ + public static final String EXTRA_DISABLE_ADD_CALL = + "android.telecom.extra.DISABLE_ADD_CALL"; + + /** + * String connection extra key on a {@link Connection} or {@link Conference} which contains the + * original Connection ID associated with the connection. Used in + * {@link RemoteConnectionService} to track the Connection ID which was originally assigned to a + * connection/conference added via + * {@link ConnectionService#addExistingConnection(PhoneAccountHandle, Connection)} and + * {@link ConnectionService#addConference(Conference)} APIs. This is important to pass to + * Telecom for when it deals with RemoteConnections. When the ConnectionManager wraps the + * {@link RemoteConnection} and {@link RemoteConference} and adds it to Telecom, there needs to + * be a way to ensure that we don't add the connection again as a duplicate. + * <p> + * For example, the TelephonyCS calls addExistingConnection for a Connection with ID + * {@code TelephonyCS@1}. The ConnectionManager learns of this via + * {@link ConnectionService#onRemoteExistingConnectionAdded(RemoteConnection)}, and wraps this + * in a new {@link Connection} which it adds to Telecom via + * {@link ConnectionService#addExistingConnection(PhoneAccountHandle, Connection)}. As part of + * this process, the wrapped RemoteConnection gets assigned a new ID (e.g. {@code ConnMan@1}). + * The TelephonyCS will ALSO try to add the existing connection to Telecom, except with the + * ID it originally referred to the connection as. Thus Telecom needs to know that the + * Connection with ID {@code ConnMan@1} is really the same as {@code TelephonyCS@1}. + * @hide + */ + public static final String EXTRA_ORIGINAL_CONNECTION_ID = + "android.telecom.extra.ORIGINAL_CONNECTION_ID"; + + /** + * Connection event used to inform Telecom that it should play the on hold tone. This is used + * to play a tone when the peer puts the current call on hold. Sent to Telecom via + * {@link #sendConnectionEvent(String, Bundle)}. + * @hide + */ + public static final String EVENT_ON_HOLD_TONE_START = + "android.telecom.event.ON_HOLD_TONE_START"; + + /** + * Connection event used to inform Telecom that it should stop the on hold tone. This is used + * to stop a tone when the peer puts the current call on hold. Sent to Telecom via + * {@link #sendConnectionEvent(String, Bundle)}. + * @hide + */ + public static final String EVENT_ON_HOLD_TONE_END = + "android.telecom.event.ON_HOLD_TONE_END"; + + /** + * Connection event used to inform {@link InCallService}s when pulling of an external call has + * failed. The user interface should inform the user of the error. + * <p> + * Expected to be used by the {@link ConnectionService} when the {@link Call#pullExternalCall()} + * API is called on a {@link Call} with the properties + * {@link Call.Details#PROPERTY_IS_EXTERNAL_CALL} and + * {@link Call.Details#CAPABILITY_CAN_PULL_CALL}, but the {@link ConnectionService} could not + * pull the external call due to an error condition. + * <p> + * Sent via {@link #sendConnectionEvent(String, Bundle)}. The {@link Bundle} parameter is + * expected to be null when this connection event is used. + */ + public static final String EVENT_CALL_PULL_FAILED = "android.telecom.event.CALL_PULL_FAILED"; + + /** + * Connection event used to inform {@link InCallService}s when the merging of two calls has + * failed. The User Interface should use this message to inform the user of the error. + * <p> + * Sent via {@link #sendConnectionEvent(String, Bundle)}. The {@link Bundle} parameter is + * expected to be null when this connection event is used. + */ + public static final String EVENT_CALL_MERGE_FAILED = "android.telecom.event.CALL_MERGE_FAILED"; + + /** + * Connection event used to inform {@link InCallService}s when the process of merging a + * Connection into a conference has begun. + * <p> + * Sent via {@link #sendConnectionEvent(String, Bundle)}. The {@link Bundle} parameter is + * expected to be null when this connection event is used. + * @hide + */ + public static final String EVENT_MERGE_START = "android.telecom.event.MERGE_START"; + + /** + * Connection event used to inform {@link InCallService}s when the process of merging a + * Connection into a conference has completed. + * <p> + * Sent via {@link #sendConnectionEvent(String, Bundle)}. The {@link Bundle} parameter is + * expected to be null when this connection event is used. + * @hide + */ + public static final String EVENT_MERGE_COMPLETE = "android.telecom.event.MERGE_COMPLETE"; + + /** + * Connection event used to inform {@link InCallService}s when a call has been put on hold by + * the remote party. + * <p> + * This is different than the {@link Connection#STATE_HOLDING} state which indicates that the + * call is being held locally on the device. When a capable {@link ConnectionService} receives + * signalling to indicate that the remote party has put the call on hold, it can send this + * connection event. + * @hide + */ + public static final String EVENT_CALL_REMOTELY_HELD = + "android.telecom.event.CALL_REMOTELY_HELD"; + + /** + * Connection event used to inform {@link InCallService}s when a call which was remotely held + * (see {@link #EVENT_CALL_REMOTELY_HELD}) has been un-held by the remote party. + * <p> + * This is different than the {@link Connection#STATE_HOLDING} state which indicates that the + * call is being held locally on the device. When a capable {@link ConnectionService} receives + * signalling to indicate that the remote party has taken the call off hold, it can send this + * connection event. + * @hide + */ + public static final String EVENT_CALL_REMOTELY_UNHELD = + "android.telecom.event.CALL_REMOTELY_UNHELD"; + + /** + * Connection event used to inform an {@link InCallService} which initiated a call handover via + * {@link Call#EVENT_REQUEST_HANDOVER} that the handover from this {@link Connection} has + * successfully completed. + * @hide + */ + public static final String EVENT_HANDOVER_COMPLETE = + "android.telecom.event.HANDOVER_COMPLETE"; + + /** + * Connection event used to inform an {@link InCallService} which initiated a call handover via + * {@link Call#EVENT_REQUEST_HANDOVER} that the handover from this {@link Connection} has failed + * to complete. + * @hide + */ + public static final String EVENT_HANDOVER_FAILED = + "android.telecom.event.HANDOVER_FAILED"; + + // Flag controlling whether PII is emitted into the logs + private static final boolean PII_DEBUG = Log.isLoggable(android.util.Log.DEBUG); + + /** + * Whether the given capabilities support the specified capability. + * + * @param capabilities A capability bit field. + * @param capability The capability to check capabilities for. + * @return Whether the specified capability is supported. + * @hide + */ + public static boolean can(int capabilities, int capability) { + return (capabilities & capability) == capability; + } + + /** + * Whether the capabilities of this {@code Connection} supports the specified capability. + * + * @param capability The capability to check capabilities for. + * @return Whether the specified capability is supported. + * @hide + */ + public boolean can(int capability) { + return can(mConnectionCapabilities, capability); + } + + /** + * Removes the specified capability from the set of capabilities of this {@code Connection}. + * + * @param capability The capability to remove from the set. + * @hide + */ + public void removeCapability(int capability) { + mConnectionCapabilities &= ~capability; + } + + /** + * Adds the specified capability to the set of capabilities of this {@code Connection}. + * + * @param capability The capability to add to the set. + * @hide + */ + public void addCapability(int capability) { + mConnectionCapabilities |= capability; + } + + /** + * Renders a set of capability bits ({@code CAPABILITY_*}) as a human readable string. + * + * @param capabilities A capability bit field. + * @return A human readable string representation. + */ + public static String capabilitiesToString(int capabilities) { + return capabilitiesToStringInternal(capabilities, true /* isLong */); + } + + /** + * Renders a set of capability bits ({@code CAPABILITY_*}) as a *short* human readable + * string. + * + * @param capabilities A capability bit field. + * @return A human readable string representation. + * @hide + */ + public static String capabilitiesToStringShort(int capabilities) { + return capabilitiesToStringInternal(capabilities, false /* isLong */); + } + + private static String capabilitiesToStringInternal(int capabilities, boolean isLong) { + StringBuilder builder = new StringBuilder(); + builder.append("["); + if (isLong) { + builder.append("Capabilities:"); + } + + if (can(capabilities, CAPABILITY_HOLD)) { + builder.append(isLong ? " CAPABILITY_HOLD" : " hld"); + } + if (can(capabilities, CAPABILITY_SUPPORT_HOLD)) { + builder.append(isLong ? " CAPABILITY_SUPPORT_HOLD" : " sup_hld"); + } + if (can(capabilities, CAPABILITY_MERGE_CONFERENCE)) { + builder.append(isLong ? " CAPABILITY_MERGE_CONFERENCE" : " mrg_cnf"); + } + if (can(capabilities, CAPABILITY_SWAP_CONFERENCE)) { + builder.append(isLong ? " CAPABILITY_SWAP_CONFERENCE" : " swp_cnf"); + } + if (can(capabilities, CAPABILITY_RESPOND_VIA_TEXT)) { + builder.append(isLong ? " CAPABILITY_RESPOND_VIA_TEXT" : " txt"); + } + if (can(capabilities, CAPABILITY_MUTE)) { + builder.append(isLong ? " CAPABILITY_MUTE" : " mut"); + } + if (can(capabilities, CAPABILITY_MANAGE_CONFERENCE)) { + builder.append(isLong ? " CAPABILITY_MANAGE_CONFERENCE" : " mng_cnf"); + } + if (can(capabilities, CAPABILITY_SUPPORTS_VT_LOCAL_RX)) { + builder.append(isLong ? " CAPABILITY_SUPPORTS_VT_LOCAL_RX" : " VTlrx"); + } + if (can(capabilities, CAPABILITY_SUPPORTS_VT_LOCAL_TX)) { + builder.append(isLong ? " CAPABILITY_SUPPORTS_VT_LOCAL_TX" : " VTltx"); + } + if (can(capabilities, CAPABILITY_SUPPORTS_VT_LOCAL_BIDIRECTIONAL)) { + builder.append(isLong ? " CAPABILITY_SUPPORTS_VT_LOCAL_BIDIRECTIONAL" : " VTlbi"); + } + if (can(capabilities, CAPABILITY_SUPPORTS_VT_REMOTE_RX)) { + builder.append(isLong ? " CAPABILITY_SUPPORTS_VT_REMOTE_RX" : " VTrrx"); + } + if (can(capabilities, CAPABILITY_SUPPORTS_VT_REMOTE_TX)) { + builder.append(isLong ? " CAPABILITY_SUPPORTS_VT_REMOTE_TX" : " VTrtx"); + } + if (can(capabilities, CAPABILITY_SUPPORTS_VT_REMOTE_BIDIRECTIONAL)) { + builder.append(isLong ? " CAPABILITY_SUPPORTS_VT_REMOTE_BIDIRECTIONAL" : " VTrbi"); + } + if (can(capabilities, CAPABILITY_CANNOT_DOWNGRADE_VIDEO_TO_AUDIO)) { + builder.append(isLong ? " CAPABILITY_CANNOT_DOWNGRADE_VIDEO_TO_AUDIO" : " !v2a"); + } + if (can(capabilities, CAPABILITY_SPEED_UP_MT_AUDIO)) { + builder.append(isLong ? " CAPABILITY_SPEED_UP_MT_AUDIO" : " spd_aud"); + } + if (can(capabilities, CAPABILITY_CAN_UPGRADE_TO_VIDEO)) { + builder.append(isLong ? " CAPABILITY_CAN_UPGRADE_TO_VIDEO" : " a2v"); + } + if (can(capabilities, CAPABILITY_CAN_PAUSE_VIDEO)) { + builder.append(isLong ? " CAPABILITY_CAN_PAUSE_VIDEO" : " paus_VT"); + } + if (can(capabilities, CAPABILITY_CONFERENCE_HAS_NO_CHILDREN)) { + builder.append(isLong ? " CAPABILITY_SINGLE_PARTY_CONFERENCE" : " 1p_cnf"); + } + if (can(capabilities, CAPABILITY_CAN_SEND_RESPONSE_VIA_CONNECTION)) { + builder.append(isLong ? " CAPABILITY_CAN_SEND_RESPONSE_VIA_CONNECTION" : " rsp_by_con"); + } + if (can(capabilities, CAPABILITY_CAN_PULL_CALL)) { + builder.append(isLong ? " CAPABILITY_CAN_PULL_CALL" : " pull"); + } + + builder.append("]"); + return builder.toString(); + } + + /** + * Renders a set of property bits ({@code PROPERTY_*}) as a human readable string. + * + * @param properties A property bit field. + * @return A human readable string representation. + */ + public static String propertiesToString(int properties) { + return propertiesToStringInternal(properties, true /* isLong */); + } + + /** + * Renders a set of property bits ({@code PROPERTY_*}) as a *short* human readable string. + * + * @param properties A property bit field. + * @return A human readable string representation. + * @hide + */ + public static String propertiesToStringShort(int properties) { + return propertiesToStringInternal(properties, false /* isLong */); + } + + private static String propertiesToStringInternal(int properties, boolean isLong) { + StringBuilder builder = new StringBuilder(); + builder.append("["); + if (isLong) { + builder.append("Properties:"); + } + + if (can(properties, PROPERTY_SELF_MANAGED)) { + builder.append(isLong ? " PROPERTY_SELF_MANAGED" : " self_mng"); + } + + if (can(properties, PROPERTY_EMERGENCY_CALLBACK_MODE)) { + builder.append(isLong ? " PROPERTY_EMERGENCY_CALLBACK_MODE" : " ecbm"); + } + + if (can(properties, PROPERTY_HIGH_DEF_AUDIO)) { + builder.append(isLong ? " PROPERTY_HIGH_DEF_AUDIO" : " HD"); + } + + if (can(properties, PROPERTY_WIFI)) { + builder.append(isLong ? " PROPERTY_WIFI" : " wifi"); + } + + if (can(properties, PROPERTY_GENERIC_CONFERENCE)) { + builder.append(isLong ? " PROPERTY_GENERIC_CONFERENCE" : " gen_conf"); + } + + if (can(properties, PROPERTY_IS_EXTERNAL_CALL)) { + builder.append(isLong ? " PROPERTY_IS_EXTERNAL_CALL" : " xtrnl"); + } + + if (can(properties, PROPERTY_HAS_CDMA_VOICE_PRIVACY)) { + builder.append(isLong ? " PROPERTY_HAS_CDMA_VOICE_PRIVACY" : " priv"); + } + + builder.append("]"); + return builder.toString(); + } + + /** @hide */ + public abstract static class Listener { + public void onStateChanged(Connection c, int state) {} + public void onAddressChanged(Connection c, Uri newAddress, int presentation) {} + public void onCallerDisplayNameChanged( + Connection c, String callerDisplayName, int presentation) {} + public void onVideoStateChanged(Connection c, int videoState) {} + public void onDisconnected(Connection c, DisconnectCause disconnectCause) {} + public void onPostDialWait(Connection c, String remaining) {} + public void onPostDialChar(Connection c, char nextChar) {} + public void onRingbackRequested(Connection c, boolean ringback) {} + public void onDestroyed(Connection c) {} + public void onConnectionCapabilitiesChanged(Connection c, int capabilities) {} + public void onConnectionPropertiesChanged(Connection c, int properties) {} + public void onSupportedAudioRoutesChanged(Connection c, int supportedAudioRoutes) {} + public void onVideoProviderChanged( + Connection c, VideoProvider videoProvider) {} + public void onAudioModeIsVoipChanged(Connection c, boolean isVoip) {} + public void onStatusHintsChanged(Connection c, StatusHints statusHints) {} + public void onConferenceablesChanged( + Connection c, List<Conferenceable> conferenceables) {} + public void onConferenceChanged(Connection c, Conference conference) {} + /** @hide */ + public void onConferenceParticipantsChanged(Connection c, + List<ConferenceParticipant> participants) {} + public void onConferenceStarted() {} + public void onConferenceMergeFailed(Connection c) {} + public void onExtrasChanged(Connection c, Bundle extras) {} + public void onExtrasRemoved(Connection c, List<String> keys) {} + public void onConnectionEvent(Connection c, String event, Bundle extras) {} + /** @hide */ + public void onConferenceSupportedChanged(Connection c, boolean isConferenceSupported) {} + public void onAudioRouteChanged(Connection c, int audioRoute) {} + public void onRttInitiationSuccess(Connection c) {} + public void onRttInitiationFailure(Connection c, int reason) {} + public void onRttSessionRemotelyTerminated(Connection c) {} + public void onRemoteRttRequest(Connection c) {} + /** @hide */ + public void onPhoneAccountChanged(Connection c, PhoneAccountHandle pHandle) {} + } + + /** + * Provides methods to read and write RTT data to/from the in-call app. + * @hide + */ + @TestApi + public static final class RttTextStream { + private static final int READ_BUFFER_SIZE = 1000; + private final InputStreamReader mPipeFromInCall; + private final OutputStreamWriter mPipeToInCall; + private final ParcelFileDescriptor mFdFromInCall; + private final ParcelFileDescriptor mFdToInCall; + private char[] mReadBuffer = new char[READ_BUFFER_SIZE]; + + /** + * @hide + */ + public RttTextStream(ParcelFileDescriptor toInCall, ParcelFileDescriptor fromInCall) { + mFdFromInCall = fromInCall; + mFdToInCall = toInCall; + mPipeFromInCall = new InputStreamReader( + new ParcelFileDescriptor.AutoCloseInputStream(fromInCall)); + mPipeToInCall = new OutputStreamWriter( + new ParcelFileDescriptor.AutoCloseOutputStream(toInCall)); + } + + /** + * Writes the string {@param input} into the text stream to the UI for this RTT call. Since + * RTT transmits text in real-time, this method should be called as often as text snippets + * are received from the remote user, even if it is only one character. + * + * This method is not thread-safe -- calling it from multiple threads simultaneously may + * lead to interleaved text. + * @param input The message to send to the in-call app. + */ + public void write(String input) throws IOException { + mPipeToInCall.write(input); + mPipeToInCall.flush(); + } + + + /** + * Reads a string from the in-call app, blocking if there is no data available. Returns + * {@code null} if the RTT conversation has been terminated and there is no further data + * to read. + * + * This method is not thread-safe -- calling it from multiple threads simultaneously may + * lead to interleaved text. + * @return A string containing text entered by the user, or {@code null} if the + * conversation has been terminated or if there was an error while reading. + */ + public String read() throws IOException { + int numRead = mPipeFromInCall.read(mReadBuffer, 0, READ_BUFFER_SIZE); + if (numRead < 0) { + return null; + } + return new String(mReadBuffer, 0, numRead); + } + + /** + * Non-blocking version of {@link #read()}. Returns {@code null} if there is nothing to + * be read. + * @return A string containing text entered by the user, or {@code null} if the user has + * not entered any new text yet. + */ + public String readImmediately() throws IOException { + if (mPipeFromInCall.ready()) { + return read(); + } else { + return null; + } + } + + /** @hide */ + public ParcelFileDescriptor getFdFromInCall() { + return mFdFromInCall; + } + + /** @hide */ + public ParcelFileDescriptor getFdToInCall() { + return mFdToInCall; + } + } + + /** + * Provides constants to represent the results of responses to session modify requests sent via + * {@link Call#sendRttRequest()} + */ + public static final class RttModifyStatus { + private RttModifyStatus() {} + /** + * Session modify request was successful. + */ + public static final int SESSION_MODIFY_REQUEST_SUCCESS = 1; + + /** + * Session modify request failed. + */ + public static final int SESSION_MODIFY_REQUEST_FAIL = 2; + + /** + * Session modify request ignored due to invalid parameters. + */ + public static final int SESSION_MODIFY_REQUEST_INVALID = 3; + + /** + * Session modify request timed out. + */ + public static final int SESSION_MODIFY_REQUEST_TIMED_OUT = 4; + + /** + * Session modify request rejected by remote user. + */ + public static final int SESSION_MODIFY_REQUEST_REJECTED_BY_REMOTE = 5; + } + + /** + * Provides a means of controlling the video session associated with a {@link Connection}. + * <p> + * Implementations create a custom subclass of {@link VideoProvider} and the + * {@link ConnectionService} creates an instance sets it on the {@link Connection} using + * {@link Connection#setVideoProvider(VideoProvider)}. Any connection which supports video + * should set the {@link VideoProvider}. + * <p> + * The {@link VideoProvider} serves two primary purposes: it provides a means for Telecom and + * {@link InCallService} implementations to issue requests related to the video session; + * it provides a means for the {@link ConnectionService} to report events and information + * related to the video session to Telecom and the {@link InCallService} implementations. + * <p> + * {@link InCallService} implementations interact with the {@link VideoProvider} via + * {@link android.telecom.InCallService.VideoCall}. + */ + public static abstract class VideoProvider { + /** + * Video is not being received (no protocol pause was issued). + * @see #handleCallSessionEvent(int) + */ + public static final int SESSION_EVENT_RX_PAUSE = 1; + + /** + * Video reception has resumed after a {@link #SESSION_EVENT_RX_PAUSE}. + * @see #handleCallSessionEvent(int) + */ + public static final int SESSION_EVENT_RX_RESUME = 2; + + /** + * Video transmission has begun. This occurs after a negotiated start of video transmission + * when the underlying protocol has actually begun transmitting video to the remote party. + * @see #handleCallSessionEvent(int) + */ + public static final int SESSION_EVENT_TX_START = 3; + + /** + * Video transmission has stopped. This occurs after a negotiated stop of video transmission + * when the underlying protocol has actually stopped transmitting video to the remote party. + * @see #handleCallSessionEvent(int) + */ + public static final int SESSION_EVENT_TX_STOP = 4; + + /** + * A camera failure has occurred for the selected camera. The {@link VideoProvider} can use + * this as a cue to inform the user the camera is not available. + * @see #handleCallSessionEvent(int) + */ + public static final int SESSION_EVENT_CAMERA_FAILURE = 5; + + /** + * Issued after {@link #SESSION_EVENT_CAMERA_FAILURE} when the camera is once again ready + * for operation. The {@link VideoProvider} can use this as a cue to inform the user that + * the camera has become available again. + * @see #handleCallSessionEvent(int) + */ + public static final int SESSION_EVENT_CAMERA_READY = 6; + + /** + * Session event raised by Telecom when + * {@link android.telecom.InCallService.VideoCall#setCamera(String)} is called and the + * caller does not have the necessary {@link android.Manifest.permission#CAMERA} permission. + * @see #handleCallSessionEvent(int) + */ + public static final int SESSION_EVENT_CAMERA_PERMISSION_ERROR = 7; + + /** + * Session modify request was successful. + * @see #receiveSessionModifyResponse(int, VideoProfile, VideoProfile) + */ + public static final int SESSION_MODIFY_REQUEST_SUCCESS = 1; + + /** + * Session modify request failed. + * @see #receiveSessionModifyResponse(int, VideoProfile, VideoProfile) + */ + public static final int SESSION_MODIFY_REQUEST_FAIL = 2; + + /** + * Session modify request ignored due to invalid parameters. + * @see #receiveSessionModifyResponse(int, VideoProfile, VideoProfile) + */ + public static final int SESSION_MODIFY_REQUEST_INVALID = 3; + + /** + * Session modify request timed out. + * @see #receiveSessionModifyResponse(int, VideoProfile, VideoProfile) + */ + public static final int SESSION_MODIFY_REQUEST_TIMED_OUT = 4; + + /** + * Session modify request rejected by remote user. + * @see #receiveSessionModifyResponse(int, VideoProfile, VideoProfile) + */ + public static final int SESSION_MODIFY_REQUEST_REJECTED_BY_REMOTE = 5; + + private static final int MSG_ADD_VIDEO_CALLBACK = 1; + private static final int MSG_SET_CAMERA = 2; + private static final int MSG_SET_PREVIEW_SURFACE = 3; + private static final int MSG_SET_DISPLAY_SURFACE = 4; + private static final int MSG_SET_DEVICE_ORIENTATION = 5; + private static final int MSG_SET_ZOOM = 6; + private static final int MSG_SEND_SESSION_MODIFY_REQUEST = 7; + private static final int MSG_SEND_SESSION_MODIFY_RESPONSE = 8; + private static final int MSG_REQUEST_CAMERA_CAPABILITIES = 9; + private static final int MSG_REQUEST_CONNECTION_DATA_USAGE = 10; + private static final int MSG_SET_PAUSE_IMAGE = 11; + private static final int MSG_REMOVE_VIDEO_CALLBACK = 12; + + private static final String SESSION_EVENT_RX_PAUSE_STR = "RX_PAUSE"; + private static final String SESSION_EVENT_RX_RESUME_STR = "RX_RESUME"; + private static final String SESSION_EVENT_TX_START_STR = "TX_START"; + private static final String SESSION_EVENT_TX_STOP_STR = "TX_STOP"; + private static final String SESSION_EVENT_CAMERA_FAILURE_STR = "CAMERA_FAIL"; + private static final String SESSION_EVENT_CAMERA_READY_STR = "CAMERA_READY"; + private static final String SESSION_EVENT_CAMERA_PERMISSION_ERROR_STR = + "CAMERA_PERMISSION_ERROR"; + private static final String SESSION_EVENT_UNKNOWN_STR = "UNKNOWN"; + + private VideoProvider.VideoProviderHandler mMessageHandler; + private final VideoProvider.VideoProviderBinder mBinder; + + /** + * Stores a list of the video callbacks, keyed by IBinder. + * + * ConcurrentHashMap constructor params: 8 is initial table size, 0.9f is + * load factor before resizing, 1 means we only expect a single thread to + * access the map so make only a single shard + */ + private ConcurrentHashMap<IBinder, IVideoCallback> mVideoCallbacks = + new ConcurrentHashMap<IBinder, IVideoCallback>(8, 0.9f, 1); + + /** + * Default handler used to consolidate binder method calls onto a single thread. + */ + private final class VideoProviderHandler extends Handler { + public VideoProviderHandler() { + super(); + } + + public VideoProviderHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_ADD_VIDEO_CALLBACK: { + IBinder binder = (IBinder) msg.obj; + IVideoCallback callback = IVideoCallback.Stub + .asInterface((IBinder) msg.obj); + if (callback == null) { + Log.w(this, "addVideoProvider - skipped; callback is null."); + break; + } + + if (mVideoCallbacks.containsKey(binder)) { + Log.i(this, "addVideoProvider - skipped; already present."); + break; + } + mVideoCallbacks.put(binder, callback); + break; + } + case MSG_REMOVE_VIDEO_CALLBACK: { + IBinder binder = (IBinder) msg.obj; + IVideoCallback callback = IVideoCallback.Stub + .asInterface((IBinder) msg.obj); + if (!mVideoCallbacks.containsKey(binder)) { + Log.i(this, "removeVideoProvider - skipped; not present."); + break; + } + mVideoCallbacks.remove(binder); + break; + } + case MSG_SET_CAMERA: + { + SomeArgs args = (SomeArgs) msg.obj; + try { + onSetCamera((String) args.arg1); + onSetCamera((String) args.arg1, (String) args.arg2, args.argi1, + args.argi2, args.argi3); + } finally { + args.recycle(); + } + } + break; + case MSG_SET_PREVIEW_SURFACE: + onSetPreviewSurface((Surface) msg.obj); + break; + case MSG_SET_DISPLAY_SURFACE: + onSetDisplaySurface((Surface) msg.obj); + break; + case MSG_SET_DEVICE_ORIENTATION: + onSetDeviceOrientation(msg.arg1); + break; + case MSG_SET_ZOOM: + onSetZoom((Float) msg.obj); + break; + case MSG_SEND_SESSION_MODIFY_REQUEST: { + SomeArgs args = (SomeArgs) msg.obj; + try { + onSendSessionModifyRequest((VideoProfile) args.arg1, + (VideoProfile) args.arg2); + } finally { + args.recycle(); + } + break; + } + case MSG_SEND_SESSION_MODIFY_RESPONSE: + onSendSessionModifyResponse((VideoProfile) msg.obj); + break; + case MSG_REQUEST_CAMERA_CAPABILITIES: + onRequestCameraCapabilities(); + break; + case MSG_REQUEST_CONNECTION_DATA_USAGE: + onRequestConnectionDataUsage(); + break; + case MSG_SET_PAUSE_IMAGE: + onSetPauseImage((Uri) msg.obj); + break; + default: + break; + } + } + } + + /** + * IVideoProvider stub implementation. + */ + private final class VideoProviderBinder extends IVideoProvider.Stub { + public void addVideoCallback(IBinder videoCallbackBinder) { + mMessageHandler.obtainMessage( + MSG_ADD_VIDEO_CALLBACK, videoCallbackBinder).sendToTarget(); + } + + public void removeVideoCallback(IBinder videoCallbackBinder) { + mMessageHandler.obtainMessage( + MSG_REMOVE_VIDEO_CALLBACK, videoCallbackBinder).sendToTarget(); + } + + public void setCamera(String cameraId, String callingPackageName, + int targetSdkVersion) { + + SomeArgs args = SomeArgs.obtain(); + args.arg1 = cameraId; + // Propagate the calling package; originally determined in + // android.telecom.InCallService.VideoCall#setCamera(String) from the calling + // process. + args.arg2 = callingPackageName; + // Pass along the uid and pid of the calling app; this gets lost when we put the + // message onto the handler. These are required for Telecom to perform a permission + // check to see if the calling app is able to use the camera. + args.argi1 = Binder.getCallingUid(); + args.argi2 = Binder.getCallingPid(); + // Pass along the target SDK version of the calling InCallService. This is used to + // maintain backwards compatibility of the API for older callers. + args.argi3 = targetSdkVersion; + mMessageHandler.obtainMessage(MSG_SET_CAMERA, args).sendToTarget(); + } + + public void setPreviewSurface(Surface surface) { + mMessageHandler.obtainMessage(MSG_SET_PREVIEW_SURFACE, surface).sendToTarget(); + } + + public void setDisplaySurface(Surface surface) { + mMessageHandler.obtainMessage(MSG_SET_DISPLAY_SURFACE, surface).sendToTarget(); + } + + public void setDeviceOrientation(int rotation) { + mMessageHandler.obtainMessage( + MSG_SET_DEVICE_ORIENTATION, rotation, 0).sendToTarget(); + } + + public void setZoom(float value) { + mMessageHandler.obtainMessage(MSG_SET_ZOOM, value).sendToTarget(); + } + + public void sendSessionModifyRequest(VideoProfile fromProfile, VideoProfile toProfile) { + SomeArgs args = SomeArgs.obtain(); + args.arg1 = fromProfile; + args.arg2 = toProfile; + mMessageHandler.obtainMessage(MSG_SEND_SESSION_MODIFY_REQUEST, args).sendToTarget(); + } + + public void sendSessionModifyResponse(VideoProfile responseProfile) { + mMessageHandler.obtainMessage( + MSG_SEND_SESSION_MODIFY_RESPONSE, responseProfile).sendToTarget(); + } + + public void requestCameraCapabilities() { + mMessageHandler.obtainMessage(MSG_REQUEST_CAMERA_CAPABILITIES).sendToTarget(); + } + + public void requestCallDataUsage() { + mMessageHandler.obtainMessage(MSG_REQUEST_CONNECTION_DATA_USAGE).sendToTarget(); + } + + public void setPauseImage(Uri uri) { + mMessageHandler.obtainMessage(MSG_SET_PAUSE_IMAGE, uri).sendToTarget(); + } + } + + public VideoProvider() { + mBinder = new VideoProvider.VideoProviderBinder(); + mMessageHandler = new VideoProvider.VideoProviderHandler(Looper.getMainLooper()); + } + + /** + * Creates an instance of the {@link VideoProvider}, specifying the looper to use. + * + * @param looper The looper. + * @hide + */ + public VideoProvider(Looper looper) { + mBinder = new VideoProvider.VideoProviderBinder(); + mMessageHandler = new VideoProvider.VideoProviderHandler(looper); + } + + /** + * Returns binder object which can be used across IPC methods. + * @hide + */ + public final IVideoProvider getInterface() { + return mBinder; + } + + /** + * Sets the camera to be used for the outgoing video. + * <p> + * The {@link VideoProvider} should respond by communicating the capabilities of the chosen + * camera via + * {@link VideoProvider#changeCameraCapabilities(VideoProfile.CameraCapabilities)}. + * <p> + * Sent from the {@link InCallService} via + * {@link InCallService.VideoCall#setCamera(String)}. + * + * @param cameraId The id of the camera (use ids as reported by + * {@link CameraManager#getCameraIdList()}). + */ + public abstract void onSetCamera(String cameraId); + + /** + * Sets the camera to be used for the outgoing video. + * <p> + * The {@link VideoProvider} should respond by communicating the capabilities of the chosen + * camera via + * {@link VideoProvider#changeCameraCapabilities(VideoProfile.CameraCapabilities)}. + * <p> + * This prototype is used internally to ensure that the calling package name, UID and PID + * are sent to Telecom so that can perform a camera permission check on the caller. + * <p> + * Sent from the {@link InCallService} via + * {@link InCallService.VideoCall#setCamera(String)}. + * + * @param cameraId The id of the camera (use ids as reported by + * {@link CameraManager#getCameraIdList()}). + * @param callingPackageName The AppOpps package name of the caller. + * @param callingUid The UID of the caller. + * @param callingPid The PID of the caller. + * @param targetSdkVersion The target SDK version of the caller. + * @hide + */ + public void onSetCamera(String cameraId, String callingPackageName, int callingUid, + int callingPid, int targetSdkVersion) {} + + /** + * Sets the surface to be used for displaying a preview of what the user's camera is + * currently capturing. When video transmission is enabled, this is the video signal which + * is sent to the remote device. + * <p> + * Sent from the {@link InCallService} via + * {@link InCallService.VideoCall#setPreviewSurface(Surface)}. + * + * @param surface The {@link Surface}. + */ + public abstract void onSetPreviewSurface(Surface surface); + + /** + * Sets the surface to be used for displaying the video received from the remote device. + * <p> + * Sent from the {@link InCallService} via + * {@link InCallService.VideoCall#setDisplaySurface(Surface)}. + * + * @param surface The {@link Surface}. + */ + public abstract void onSetDisplaySurface(Surface surface); + + /** + * Sets the device orientation, in degrees. Assumes that a standard portrait orientation of + * the device is 0 degrees. + * <p> + * Sent from the {@link InCallService} via + * {@link InCallService.VideoCall#setDeviceOrientation(int)}. + * + * @param rotation The device orientation, in degrees. + */ + public abstract void onSetDeviceOrientation(int rotation); + + /** + * Sets camera zoom ratio. + * <p> + * Sent from the {@link InCallService} via {@link InCallService.VideoCall#setZoom(float)}. + * + * @param value The camera zoom ratio. + */ + public abstract void onSetZoom(float value); + + /** + * Issues a request to modify the properties of the current video session. + * <p> + * Example scenarios include: requesting an audio-only call to be upgraded to a + * bi-directional video call, turning on or off the user's camera, sending a pause signal + * when the {@link InCallService} is no longer the foreground application. + * <p> + * If the {@link VideoProvider} determines a request to be invalid, it should call + * {@link #receiveSessionModifyResponse(int, VideoProfile, VideoProfile)} to report the + * invalid request back to the {@link InCallService}. + * <p> + * Where a request requires confirmation from the user of the peer device, the + * {@link VideoProvider} must communicate the request to the peer device and handle the + * user's response. {@link #receiveSessionModifyResponse(int, VideoProfile, VideoProfile)} + * is used to inform the {@link InCallService} of the result of the request. + * <p> + * Sent from the {@link InCallService} via + * {@link InCallService.VideoCall#sendSessionModifyRequest(VideoProfile)}. + * + * @param fromProfile The video profile prior to the request. + * @param toProfile The video profile with the requested changes made. + */ + public abstract void onSendSessionModifyRequest(VideoProfile fromProfile, + VideoProfile toProfile); + + /** + * Provides a response to a request to change the current video session properties. + * <p> + * For example, if the peer requests and upgrade from an audio-only call to a bi-directional + * video call, could decline the request and keep the call as audio-only. + * In such a scenario, the {@code responseProfile} would have a video state of + * {@link VideoProfile#STATE_AUDIO_ONLY}. If the user had decided to accept the request, + * the video state would be {@link VideoProfile#STATE_BIDIRECTIONAL}. + * <p> + * Sent from the {@link InCallService} via + * {@link InCallService.VideoCall#sendSessionModifyResponse(VideoProfile)} in response to + * a {@link InCallService.VideoCall.Callback#onSessionModifyRequestReceived(VideoProfile)} + * callback. + * + * @param responseProfile The response video profile. + */ + public abstract void onSendSessionModifyResponse(VideoProfile responseProfile); + + /** + * Issues a request to the {@link VideoProvider} to retrieve the camera capabilities. + * <p> + * The {@link VideoProvider} should respond by communicating the capabilities of the chosen + * camera via + * {@link VideoProvider#changeCameraCapabilities(VideoProfile.CameraCapabilities)}. + * <p> + * Sent from the {@link InCallService} via + * {@link InCallService.VideoCall#requestCameraCapabilities()}. + */ + public abstract void onRequestCameraCapabilities(); + + /** + * Issues a request to the {@link VideoProvider} to retrieve the current data usage for the + * video component of the current {@link Connection}. + * <p> + * The {@link VideoProvider} should respond by communicating current data usage, in bytes, + * via {@link VideoProvider#setCallDataUsage(long)}. + * <p> + * Sent from the {@link InCallService} via + * {@link InCallService.VideoCall#requestCallDataUsage()}. + */ + public abstract void onRequestConnectionDataUsage(); + + /** + * Provides the {@link VideoProvider} with the {@link Uri} of an image to be displayed to + * the peer device when the video signal is paused. + * <p> + * Sent from the {@link InCallService} via + * {@link InCallService.VideoCall#setPauseImage(Uri)}. + * + * @param uri URI of image to display. + */ + public abstract void onSetPauseImage(Uri uri); + + /** + * Used to inform listening {@link InCallService} implementations when the + * {@link VideoProvider} receives a session modification request. + * <p> + * Received by the {@link InCallService} via + * {@link InCallService.VideoCall.Callback#onSessionModifyRequestReceived(VideoProfile)}, + * + * @param videoProfile The requested video profile. + * @see #onSendSessionModifyRequest(VideoProfile, VideoProfile) + */ + public void receiveSessionModifyRequest(VideoProfile videoProfile) { + if (mVideoCallbacks != null) { + for (IVideoCallback callback : mVideoCallbacks.values()) { + try { + callback.receiveSessionModifyRequest(videoProfile); + } catch (RemoteException ignored) { + Log.w(this, "receiveSessionModifyRequest callback failed", ignored); + } + } + } + } + + /** + * Used to inform listening {@link InCallService} implementations when the + * {@link VideoProvider} receives a response to a session modification request. + * <p> + * Received by the {@link InCallService} via + * {@link InCallService.VideoCall.Callback#onSessionModifyResponseReceived(int, + * VideoProfile, VideoProfile)}. + * + * @param status Status of the session modify request. Valid values are + * {@link VideoProvider#SESSION_MODIFY_REQUEST_SUCCESS}, + * {@link VideoProvider#SESSION_MODIFY_REQUEST_FAIL}, + * {@link VideoProvider#SESSION_MODIFY_REQUEST_INVALID}, + * {@link VideoProvider#SESSION_MODIFY_REQUEST_TIMED_OUT}, + * {@link VideoProvider#SESSION_MODIFY_REQUEST_REJECTED_BY_REMOTE} + * @param requestedProfile The original request which was sent to the peer device. + * @param responseProfile The actual profile changes agreed to by the peer device. + * @see #onSendSessionModifyRequest(VideoProfile, VideoProfile) + */ + public void receiveSessionModifyResponse(int status, + VideoProfile requestedProfile, VideoProfile responseProfile) { + if (mVideoCallbacks != null) { + for (IVideoCallback callback : mVideoCallbacks.values()) { + try { + callback.receiveSessionModifyResponse(status, requestedProfile, + responseProfile); + } catch (RemoteException ignored) { + Log.w(this, "receiveSessionModifyResponse callback failed", ignored); + } + } + } + } + + /** + * Used to inform listening {@link InCallService} implementations when the + * {@link VideoProvider} reports a call session event. + * <p> + * Received by the {@link InCallService} via + * {@link InCallService.VideoCall.Callback#onCallSessionEvent(int)}. + * + * @param event The event. Valid values are: {@link VideoProvider#SESSION_EVENT_RX_PAUSE}, + * {@link VideoProvider#SESSION_EVENT_RX_RESUME}, + * {@link VideoProvider#SESSION_EVENT_TX_START}, + * {@link VideoProvider#SESSION_EVENT_TX_STOP}, + * {@link VideoProvider#SESSION_EVENT_CAMERA_FAILURE}, + * {@link VideoProvider#SESSION_EVENT_CAMERA_READY}, + * {@link VideoProvider#SESSION_EVENT_CAMERA_FAILURE}. + */ + public void handleCallSessionEvent(int event) { + if (mVideoCallbacks != null) { + for (IVideoCallback callback : mVideoCallbacks.values()) { + try { + callback.handleCallSessionEvent(event); + } catch (RemoteException ignored) { + Log.w(this, "handleCallSessionEvent callback failed", ignored); + } + } + } + } + + /** + * Used to inform listening {@link InCallService} implementations when the dimensions of the + * peer's video have changed. + * <p> + * This could occur if, for example, the peer rotates their device, changing the aspect + * ratio of the video, or if the user switches between the back and front cameras. + * <p> + * Received by the {@link InCallService} via + * {@link InCallService.VideoCall.Callback#onPeerDimensionsChanged(int, int)}. + * + * @param width The updated peer video width. + * @param height The updated peer video height. + */ + public void changePeerDimensions(int width, int height) { + if (mVideoCallbacks != null) { + for (IVideoCallback callback : mVideoCallbacks.values()) { + try { + callback.changePeerDimensions(width, height); + } catch (RemoteException ignored) { + Log.w(this, "changePeerDimensions callback failed", ignored); + } + } + } + } + + /** + * Used to inform listening {@link InCallService} implementations when the data usage of the + * video associated with the current {@link Connection} has changed. + * <p> + * This could be in response to a preview request via + * {@link #onRequestConnectionDataUsage()}, or as a periodic update by the + * {@link VideoProvider}. Where periodic updates of data usage are provided, they should be + * provided at most for every 1 MB of data transferred and no more than once every 10 sec. + * <p> + * Received by the {@link InCallService} via + * {@link InCallService.VideoCall.Callback#onCallDataUsageChanged(long)}. + * + * @param dataUsage The updated data usage (in bytes). Reported as the cumulative bytes + * used since the start of the call. + */ + public void setCallDataUsage(long dataUsage) { + if (mVideoCallbacks != null) { + for (IVideoCallback callback : mVideoCallbacks.values()) { + try { + callback.changeCallDataUsage(dataUsage); + } catch (RemoteException ignored) { + Log.w(this, "setCallDataUsage callback failed", ignored); + } + } + } + } + + /** + * @see #setCallDataUsage(long) + * + * @param dataUsage The updated data usage (in byes). + * @deprecated - Use {@link #setCallDataUsage(long)} instead. + * @hide + */ + public void changeCallDataUsage(long dataUsage) { + setCallDataUsage(dataUsage); + } + + /** + * Used to inform listening {@link InCallService} implementations when the capabilities of + * the current camera have changed. + * <p> + * The {@link VideoProvider} should call this in response to + * {@link VideoProvider#onRequestCameraCapabilities()}, or when the current camera is + * changed via {@link VideoProvider#onSetCamera(String)}. + * <p> + * Received by the {@link InCallService} via + * {@link InCallService.VideoCall.Callback#onCameraCapabilitiesChanged( + * VideoProfile.CameraCapabilities)}. + * + * @param cameraCapabilities The new camera capabilities. + */ + public void changeCameraCapabilities(VideoProfile.CameraCapabilities cameraCapabilities) { + if (mVideoCallbacks != null) { + for (IVideoCallback callback : mVideoCallbacks.values()) { + try { + callback.changeCameraCapabilities(cameraCapabilities); + } catch (RemoteException ignored) { + Log.w(this, "changeCameraCapabilities callback failed", ignored); + } + } + } + } + + /** + * Used to inform listening {@link InCallService} implementations when the video quality + * of the call has changed. + * <p> + * Received by the {@link InCallService} via + * {@link InCallService.VideoCall.Callback#onVideoQualityChanged(int)}. + * + * @param videoQuality The updated video quality. Valid values: + * {@link VideoProfile#QUALITY_HIGH}, + * {@link VideoProfile#QUALITY_MEDIUM}, + * {@link VideoProfile#QUALITY_LOW}, + * {@link VideoProfile#QUALITY_DEFAULT}. + */ + public void changeVideoQuality(int videoQuality) { + if (mVideoCallbacks != null) { + for (IVideoCallback callback : mVideoCallbacks.values()) { + try { + callback.changeVideoQuality(videoQuality); + } catch (RemoteException ignored) { + Log.w(this, "changeVideoQuality callback failed", ignored); + } + } + } + } + + /** + * Returns a string representation of a call session event. + * + * @param event A call session event passed to {@link #handleCallSessionEvent(int)}. + * @return String representation of the call session event. + * @hide + */ + public static String sessionEventToString(int event) { + switch (event) { + case SESSION_EVENT_CAMERA_FAILURE: + return SESSION_EVENT_CAMERA_FAILURE_STR; + case SESSION_EVENT_CAMERA_READY: + return SESSION_EVENT_CAMERA_READY_STR; + case SESSION_EVENT_RX_PAUSE: + return SESSION_EVENT_RX_PAUSE_STR; + case SESSION_EVENT_RX_RESUME: + return SESSION_EVENT_RX_RESUME_STR; + case SESSION_EVENT_TX_START: + return SESSION_EVENT_TX_START_STR; + case SESSION_EVENT_TX_STOP: + return SESSION_EVENT_TX_STOP_STR; + case SESSION_EVENT_CAMERA_PERMISSION_ERROR: + return SESSION_EVENT_CAMERA_PERMISSION_ERROR_STR; + default: + return SESSION_EVENT_UNKNOWN_STR + " " + event; + } + } + } + + private final Listener mConnectionDeathListener = new Listener() { + @Override + public void onDestroyed(Connection c) { + if (mConferenceables.remove(c)) { + fireOnConferenceableConnectionsChanged(); + } + } + }; + + private final Conference.Listener mConferenceDeathListener = new Conference.Listener() { + @Override + public void onDestroyed(Conference c) { + if (mConferenceables.remove(c)) { + fireOnConferenceableConnectionsChanged(); + } + } + }; + + /** + * ConcurrentHashMap constructor params: 8 is initial table size, 0.9f is + * load factor before resizing, 1 means we only expect a single thread to + * access the map so make only a single shard + */ + private final Set<Listener> mListeners = Collections.newSetFromMap( + new ConcurrentHashMap<Listener, Boolean>(8, 0.9f, 1)); + private final List<Conferenceable> mConferenceables = new ArrayList<>(); + private final List<Conferenceable> mUnmodifiableConferenceables = + Collections.unmodifiableList(mConferenceables); + + // The internal telecom call ID associated with this connection. + private String mTelecomCallId; + private int mState = STATE_NEW; + private CallAudioState mCallAudioState; + private Uri mAddress; + private int mAddressPresentation; + private String mCallerDisplayName; + private int mCallerDisplayNamePresentation; + private boolean mRingbackRequested = false; + private int mConnectionCapabilities; + private int mConnectionProperties; + private int mSupportedAudioRoutes = CallAudioState.ROUTE_ALL; + private VideoProvider mVideoProvider; + private boolean mAudioModeIsVoip; + private long mConnectTimeMillis = Conference.CONNECT_TIME_NOT_SPECIFIED; + private long mConnectElapsedTimeMillis = Conference.CONNECT_TIME_NOT_SPECIFIED; + private StatusHints mStatusHints; + private int mVideoState; + private DisconnectCause mDisconnectCause; + private Conference mConference; + private ConnectionService mConnectionService; + private Bundle mExtras; + private final Object mExtrasLock = new Object(); + + /** + * Tracks the key set for the extras bundle provided on the last invocation of + * {@link #setExtras(Bundle)}. Used so that on subsequent invocations we can remove any extras + * keys which were set previously but are no longer present in the replacement Bundle. + */ + private Set<String> mPreviousExtraKeys; + + /** + * Create a new Connection. + */ + public Connection() {} + + /** + * Returns the Telecom internal call ID associated with this connection. Should only be used + * for debugging and tracing purposes. + * + * @return The Telecom call ID. + * @hide + */ + public final String getTelecomCallId() { + return mTelecomCallId; + } + + /** + * @return The address (e.g., phone number) to which this Connection is currently communicating. + */ + public final Uri getAddress() { + return mAddress; + } + + /** + * @return The presentation requirements for the address. + * See {@link TelecomManager} for valid values. + */ + public final int getAddressPresentation() { + return mAddressPresentation; + } + + /** + * @return The caller display name (CNAP). + */ + public final String getCallerDisplayName() { + return mCallerDisplayName; + } + + /** + * @return The presentation requirements for the handle. + * See {@link TelecomManager} for valid values. + */ + public final int getCallerDisplayNamePresentation() { + return mCallerDisplayNamePresentation; + } + + /** + * @return The state of this Connection. + */ + public final int getState() { + return mState; + } + + /** + * Returns the video state of the connection. + * Valid values: {@link VideoProfile#STATE_AUDIO_ONLY}, + * {@link VideoProfile#STATE_BIDIRECTIONAL}, + * {@link VideoProfile#STATE_TX_ENABLED}, + * {@link VideoProfile#STATE_RX_ENABLED}. + * + * @return The video state of the connection. + * @hide + */ + public final int getVideoState() { + return mVideoState; + } + + /** + * @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 #getCallAudioState()} instead. + * @hide + */ + @SystemApi + @Deprecated + public final AudioState getAudioState() { + if (mCallAudioState == null) { + return null; + } + return new AudioState(mCallAudioState); + } + + /** + * @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. + */ + public final CallAudioState getCallAudioState() { + return mCallAudioState; + } + + /** + * @return The conference that this connection is a part of. Null if it is not part of any + * conference. + */ + public final Conference getConference() { + return mConference; + } + + /** + * Returns whether this connection is requesting that the system play a ringback tone + * on its behalf. + */ + public final boolean isRingbackRequested() { + return mRingbackRequested; + } + + /** + * @return True if the connection's audio mode is VOIP. + */ + public final boolean getAudioModeIsVoip() { + return mAudioModeIsVoip; + } + + /** + * Retrieves the connection start time of the {@code Connnection}, if specified. A value of + * {@link Conference#CONNECT_TIME_NOT_SPECIFIED} indicates that Telecom should determine the + * start time of the conference. + * + * @return The time at which the {@code Connnection} was connected. + * + * @hide + */ + public final long getConnectTimeMillis() { + return mConnectTimeMillis; + } + + /** + * Retrieves the connection start time of the {@link Connection}, if specified. A value of + * {@link Conference#CONNECT_TIME_NOT_SPECIFIED} indicates that Telecom should determine the + * start time of the conference. + * + * Based on the value of {@link SystemClock#elapsedRealtime()}, which ensures that wall-clock + * changes do not impact the call duration. + * + * @return The time at which the {@link Connection} was connected. + * + * @hide + */ + public final long getConnectElapsedTimeMillis() { + return mConnectElapsedTimeMillis; + } + + /** + * @return The status hints for this connection. + */ + public final StatusHints getStatusHints() { + return mStatusHints; + } + + /** + * Returns the extras associated with this connection. + * <p> + * Extras should be updated using {@link #putExtras(Bundle)}. + * <p> + * Telecom or an {@link InCallService} can also update the extras via + * {@link android.telecom.Call#putExtras(Bundle)}, and + * {@link Call#removeExtras(List)}. + * <p> + * The connection is notified of changes to the extras made by Telecom or an + * {@link InCallService} by {@link #onExtrasChanged(Bundle)}. + * + * @return The extras associated with this connection. + */ + public final Bundle getExtras() { + Bundle extras = null; + synchronized (mExtrasLock) { + if (mExtras != null) { + extras = new Bundle(mExtras); + } + } + return extras; + } + + /** + * Assign a listener to be notified of state changes. + * + * @param l A listener. + * @return This Connection. + * + * @hide + */ + public final Connection addConnectionListener(Listener l) { + mListeners.add(l); + return this; + } + + /** + * Remove a previously assigned listener that was being notified of state changes. + * + * @param l A Listener. + * @return This Connection. + * + * @hide + */ + public final Connection removeConnectionListener(Listener l) { + if (l != null) { + mListeners.remove(l); + } + return this; + } + + /** + * @return The {@link DisconnectCause} for this connection. + */ + public final DisconnectCause getDisconnectCause() { + return mDisconnectCause; + } + + /** + * Sets the telecom call ID associated with this Connection. The Telecom Call ID should be used + * ONLY for debugging purposes. + * + * @param callId The telecom call ID. + * @hide + */ + public void setTelecomCallId(String callId) { + mTelecomCallId = callId; + } + + /** + * Inform this Connection that the state of its audio output has been changed externally. + * + * @param state The new audio state. + * @hide + */ + final void setCallAudioState(CallAudioState state) { + checkImmutable(); + Log.d(this, "setAudioState %s", state); + mCallAudioState = state; + onAudioStateChanged(getAudioState()); + onCallAudioStateChanged(state); + } + + /** + * @param state An integer value of a {@code STATE_*} constant. + * @return A string representation of the value. + */ + public static String stateToString(int state) { + switch (state) { + case STATE_INITIALIZING: + return "INITIALIZING"; + case STATE_NEW: + return "NEW"; + case STATE_RINGING: + return "RINGING"; + case STATE_DIALING: + return "DIALING"; + case STATE_PULLING_CALL: + return "PULLING_CALL"; + case STATE_ACTIVE: + return "ACTIVE"; + case STATE_HOLDING: + return "HOLDING"; + case STATE_DISCONNECTED: + return "DISCONNECTED"; + default: + Log.wtf(Connection.class, "Unknown state %d", state); + return "UNKNOWN"; + } + } + + /** + * Returns the connection's capabilities, as a bit mask of the {@code CAPABILITY_*} constants. + */ + public final int getConnectionCapabilities() { + return mConnectionCapabilities; + } + + /** + * Returns the connection's properties, as a bit mask of the {@code PROPERTY_*} constants. + */ + public final int getConnectionProperties() { + return mConnectionProperties; + } + + /** + * Returns the connection's supported audio routes. + * + * @hide + */ + public final int getSupportedAudioRoutes() { + return mSupportedAudioRoutes; + } + + /** + * Sets the value of the {@link #getAddress()} property. + * + * @param address The new address. + * @param presentation The presentation requirements for the address. + * See {@link TelecomManager} for valid values. + */ + public final void setAddress(Uri address, int presentation) { + checkImmutable(); + Log.d(this, "setAddress %s", address); + mAddress = address; + mAddressPresentation = presentation; + for (Listener l : mListeners) { + l.onAddressChanged(this, address, presentation); + } + } + + /** + * Sets the caller display name (CNAP). + * + * @param callerDisplayName The new display name. + * @param presentation The presentation requirements for the handle. + * See {@link TelecomManager} for valid values. + */ + 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); + } + } + + /** + * Set the video state for the connection. + * Valid values: {@link VideoProfile#STATE_AUDIO_ONLY}, + * {@link VideoProfile#STATE_BIDIRECTIONAL}, + * {@link VideoProfile#STATE_TX_ENABLED}, + * {@link VideoProfile#STATE_RX_ENABLED}. + * + * @param videoState The new video state. + */ + public final void setVideoState(int videoState) { + checkImmutable(); + Log.d(this, "setVideoState %d", videoState); + mVideoState = videoState; + for (Listener l : mListeners) { + l.onVideoStateChanged(this, mVideoState); + } + } + + /** + * Sets state to active (e.g., an ongoing connection where two or more parties can actively + * communicate). + */ + public final void setActive() { + checkImmutable(); + setRingbackRequested(false); + setState(STATE_ACTIVE); + } + + /** + * Sets state to ringing (e.g., an inbound ringing connection). + */ + public final void setRinging() { + checkImmutable(); + setState(STATE_RINGING); + } + + /** + * Sets state to initializing (this Connection is not yet ready to be used). + */ + public final void setInitializing() { + checkImmutable(); + setState(STATE_INITIALIZING); + } + + /** + * Sets state to initialized (the Connection has been set up and is now ready to be used). + */ + public final void setInitialized() { + checkImmutable(); + setState(STATE_NEW); + } + + /** + * Sets state to dialing (e.g., dialing an outbound connection). + */ + public final void setDialing() { + checkImmutable(); + setState(STATE_DIALING); + } + + /** + * Sets state to pulling (e.g. the connection is being pulled to the local device from another + * device). Only applicable for {@link Connection}s with + * {@link Connection#PROPERTY_IS_EXTERNAL_CALL} and {@link Connection#CAPABILITY_CAN_PULL_CALL}. + */ + public final void setPulling() { + checkImmutable(); + setState(STATE_PULLING_CALL); + } + + /** + * Sets state to be on hold. + */ + public final void setOnHold() { + checkImmutable(); + setState(STATE_HOLDING); + } + + /** + * Sets the video connection provider. + * @param videoProvider The video provider. + */ + public final void setVideoProvider(VideoProvider videoProvider) { + checkImmutable(); + mVideoProvider = videoProvider; + for (Listener l : mListeners) { + l.onVideoProviderChanged(this, videoProvider); + } + } + + public final VideoProvider getVideoProvider() { + return mVideoProvider; + } + + /** + * Sets state to disconnected. + * + * @param disconnectCause The reason for the disconnection, as specified by + * {@link DisconnectCause}. + */ + public final void setDisconnected(DisconnectCause disconnectCause) { + checkImmutable(); + mDisconnectCause = disconnectCause; + setState(STATE_DISCONNECTED); + Log.d(this, "Disconnected with cause %s", disconnectCause); + for (Listener l : mListeners) { + l.onDisconnected(this, disconnectCause); + } + } + + /** + * Informs listeners that this {@code Connection} is in a post-dial wait state. This is done + * when (a) the {@code Connection} is issuing a DTMF sequence; (b) it has encountered a "wait" + * character; and (c) it wishes to inform the In-Call app that it is waiting for the end-user + * to send an {@link #onPostDialContinue(boolean)} signal. + * + * @param remaining The DTMF character sequence remaining to be emitted once the + * {@link #onPostDialContinue(boolean)} is received, including any "wait" characters + * that remaining sequence may contain. + */ + public final void setPostDialWait(String remaining) { + checkImmutable(); + for (Listener l : mListeners) { + l.onPostDialWait(this, remaining); + } + } + + /** + * Informs listeners that this {@code Connection} has processed a character in the post-dial + * started state. This is done when (a) the {@code Connection} is issuing a DTMF sequence; + * and (b) it wishes to signal Telecom to play the corresponding DTMF tone locally. + * + * @param nextChar The DTMF character that was just processed by the {@code Connection}. + */ + public final void setNextPostDialChar(char nextChar) { + checkImmutable(); + for (Listener l : mListeners) { + l.onPostDialChar(this, nextChar); + } + } + + /** + * Requests that the framework play a ringback tone. This is to be invoked by implementations + * that do not play a ringback tone themselves in the connection's audio stream. + * + * @param ringback Whether the ringback tone is to be played. + */ + public final void setRingbackRequested(boolean ringback) { + checkImmutable(); + if (mRingbackRequested != ringback) { + mRingbackRequested = ringback; + for (Listener l : mListeners) { + l.onRingbackRequested(this, ringback); + } + } + } + + /** + * Sets the connection's capabilities as a bit mask of the {@code CAPABILITY_*} constants. + * + * @param connectionCapabilities The new connection capabilities. + */ + public final void setConnectionCapabilities(int connectionCapabilities) { + checkImmutable(); + if (mConnectionCapabilities != connectionCapabilities) { + mConnectionCapabilities = connectionCapabilities; + for (Listener l : mListeners) { + l.onConnectionCapabilitiesChanged(this, mConnectionCapabilities); + } + } + } + + /** + * Sets the connection's properties as a bit mask of the {@code PROPERTY_*} constants. + * + * @param connectionProperties The new connection properties. + */ + public final void setConnectionProperties(int connectionProperties) { + checkImmutable(); + if (mConnectionProperties != connectionProperties) { + mConnectionProperties = connectionProperties; + for (Listener l : mListeners) { + l.onConnectionPropertiesChanged(this, mConnectionProperties); + } + } + } + + /** + * Sets the supported audio routes. + * + * @param supportedAudioRoutes the supported audio routes as a bitmask. + * See {@link CallAudioState} + * @hide + */ + public final void setSupportedAudioRoutes(int supportedAudioRoutes) { + if ((supportedAudioRoutes + & (CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER)) == 0) { + throw new IllegalArgumentException( + "supported audio routes must include either speaker or earpiece"); + } + + if (mSupportedAudioRoutes != supportedAudioRoutes) { + mSupportedAudioRoutes = supportedAudioRoutes; + for (Listener l : mListeners) { + l.onSupportedAudioRoutesChanged(this, mSupportedAudioRoutes); + } + } + } + + /** + * Tears down the Connection object. + */ + public final void destroy() { + for (Listener l : mListeners) { + l.onDestroyed(this); + } + } + + /** + * Requests that the framework use VOIP audio mode for this connection. + * + * @param isVoip True if the audio mode is VOIP. + */ + public final void setAudioModeIsVoip(boolean isVoip) { + checkImmutable(); + mAudioModeIsVoip = isVoip; + for (Listener l : mListeners) { + l.onAudioModeIsVoipChanged(this, isVoip); + } + } + + /** + * Sets the time at which a call became active on this Connection. This is set only + * when a conference call becomes active on this connection. + * + * @param connectTimeMillis The connection time, in milliseconds. Should be set using a value + * obtained from {@link System#currentTimeMillis()}. + * + * @hide + */ + public final void setConnectTimeMillis(long connectTimeMillis) { + mConnectTimeMillis = connectTimeMillis; + } + + /** + * Sets the time at which a call became active on this Connection. This is set only + * when a conference call becomes active on this connection. + * + * @param connectElapsedTimeMillis The connection time, in milliseconds. Stored in the format + * {@link SystemClock#elapsedRealtime()}. + * + * @hide + */ + public final void setConnectElapsedTimeMillis(long connectElapsedTimeMillis) { + mConnectElapsedTimeMillis = connectElapsedTimeMillis; + } + + /** + * Sets the label and icon status to display in the in-call UI. + * + * @param statusHints The status label and icon to set. + */ + public final void setStatusHints(StatusHints statusHints) { + checkImmutable(); + mStatusHints = statusHints; + for (Listener l : mListeners) { + l.onStatusHintsChanged(this, statusHints); + } + } + + /** + * Sets the connections with which this connection can be conferenced. + * + * @param conferenceableConnections The set of connections this connection can conference with. + */ + public final void setConferenceableConnections(List<Connection> conferenceableConnections) { + checkImmutable(); + clearConferenceableList(); + for (Connection c : conferenceableConnections) { + // If statement checks for duplicates in input. It makes it N^2 but we're dealing with a + // small amount of items here. + if (!mConferenceables.contains(c)) { + c.addConnectionListener(mConnectionDeathListener); + mConferenceables.add(c); + } + } + fireOnConferenceableConnectionsChanged(); + } + + /** + * Similar to {@link #setConferenceableConnections(java.util.List)}, sets a list of connections + * or conferences with which this connection can be conferenced. + * + * @param conferenceables The conferenceables. + */ + public final void setConferenceables(List<Conferenceable> conferenceables) { + clearConferenceableList(); + for (Conferenceable c : conferenceables) { + // If statement checks for duplicates in input. It makes it N^2 but we're dealing with a + // small amount of items here. + if (!mConferenceables.contains(c)) { + if (c instanceof Connection) { + Connection connection = (Connection) c; + connection.addConnectionListener(mConnectionDeathListener); + } else if (c instanceof Conference) { + Conference conference = (Conference) c; + conference.addListener(mConferenceDeathListener); + } + mConferenceables.add(c); + } + } + fireOnConferenceableConnectionsChanged(); + } + + /** + * Returns the connections or conferences with which this connection can be conferenced. + */ + public final List<Conferenceable> getConferenceables() { + return mUnmodifiableConferenceables; + } + + /** + * @hide + */ + public final void setConnectionService(ConnectionService connectionService) { + checkImmutable(); + if (mConnectionService != null) { + Log.e(this, new Exception(), "Trying to set ConnectionService on a connection " + + "which is already associated with another ConnectionService."); + } else { + mConnectionService = connectionService; + } + } + + /** + * @hide + */ + public final void unsetConnectionService(ConnectionService connectionService) { + if (mConnectionService != connectionService) { + Log.e(this, new Exception(), "Trying to remove ConnectionService from a Connection " + + "that does not belong to the ConnectionService."); + } else { + mConnectionService = null; + } + } + + /** + * @hide + */ + public final ConnectionService getConnectionService() { + return mConnectionService; + } + + /** + * Sets the conference that this connection is a part of. This will fail if the connection is + * already part of a conference. {@link #resetConference} to un-set the conference first. + * + * @param conference The conference. + * @return {@code true} if the conference was successfully set. + * @hide + */ + public final boolean setConference(Conference conference) { + checkImmutable(); + // We check to see if it is already part of another conference. + if (mConference == null) { + mConference = conference; + if (mConnectionService != null && mConnectionService.containsConference(conference)) { + fireConferenceChanged(); + } + return true; + } + return false; + } + + /** + * Resets the conference that this connection is a part of. + * @hide + */ + public final void resetConference() { + if (mConference != null) { + Log.d(this, "Conference reset"); + mConference = null; + fireConferenceChanged(); + } + } + + /** + * Set some extras that can be associated with this {@code Connection}. + * <p> + * New or existing keys are replaced in the {@code Connection} extras. Keys which are no longer + * in the new extras, but were present the last time {@code setExtras} was called are removed. + * <p> + * Alternatively you may use the {@link #putExtras(Bundle)}, and + * {@link #removeExtras(String...)} methods to modify the extras. + * <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. + * + * @param extras The extras associated with this {@code Connection}. + */ + public final void setExtras(@Nullable Bundle extras) { + checkImmutable(); + + // Add/replace any new or changed extras values. + putExtras(extras); + + // If we have used "setExtras" in the past, compare the key set from the last invocation to + // the current one and remove any keys that went away. + if (mPreviousExtraKeys != null) { + List<String> toRemove = new ArrayList<String>(); + for (String oldKey : mPreviousExtraKeys) { + if (extras == null || !extras.containsKey(oldKey)) { + toRemove.add(oldKey); + } + } + if (!toRemove.isEmpty()) { + removeExtras(toRemove); + } + } + + // Track the keys the last time set called setExtras. This way, the next time setExtras is + // called we can see if the caller has removed any extras values. + if (mPreviousExtraKeys == null) { + mPreviousExtraKeys = new ArraySet<String>(); + } + mPreviousExtraKeys.clear(); + if (extras != null) { + mPreviousExtraKeys.addAll(extras.keySet()); + } + } + + /** + * Adds some extras to this {@code Connection}. Existing keys are replaced and new ones are + * added. + * <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. + * + * @param extras The extras to add. + */ + public final void putExtras(@NonNull Bundle extras) { + checkImmutable(); + if (extras == null) { + return; + } + // Creating a duplicate bundle so we don't have to synchronize on mExtrasLock while calling + // the listeners. + Bundle listenerExtras; + synchronized (mExtrasLock) { + if (mExtras == null) { + mExtras = new Bundle(); + } + mExtras.putAll(extras); + listenerExtras = new Bundle(mExtras); + } + for (Listener l : mListeners) { + // Create a new clone of the extras for each listener so that they don't clobber + // each other + l.onExtrasChanged(this, new Bundle(listenerExtras)); + } + } + + /** + * Adds a boolean extra to this {@code Connection}. + * + * @param key The extra key. + * @param value The value. + * @hide + */ + public final void putExtra(String key, boolean value) { + Bundle newExtras = new Bundle(); + newExtras.putBoolean(key, value); + putExtras(newExtras); + } + + /** + * Adds an integer extra to this {@code Connection}. + * + * @param key The extra key. + * @param value The value. + * @hide + */ + public final void putExtra(String key, int value) { + Bundle newExtras = new Bundle(); + newExtras.putInt(key, value); + putExtras(newExtras); + } + + /** + * Adds a string extra to this {@code Connection}. + * + * @param key The extra key. + * @param value The value. + * @hide + */ + public final void putExtra(String key, String value) { + Bundle newExtras = new Bundle(); + newExtras.putString(key, value); + putExtras(newExtras); + } + + /** + * Removes extras from this {@code Connection}. + * + * @param keys The keys of the extras to remove. + */ + public final void removeExtras(List<String> keys) { + synchronized (mExtrasLock) { + if (mExtras != null) { + for (String key : keys) { + mExtras.remove(key); + } + } + } + List<String> unmodifiableKeys = Collections.unmodifiableList(keys); + for (Listener l : mListeners) { + l.onExtrasRemoved(this, unmodifiableKeys); + } + } + + /** + * Removes extras from this {@code Connection}. + * + * @param keys The keys of the extras to remove. + */ + public final void removeExtras(String ... keys) { + removeExtras(Arrays.asList(keys)); + } + + /** + * Sets the audio route (speaker, bluetooth, etc...). When this request is honored, there will + * be change to the {@link #getCallAudioState()}. + * <p> + * Used by self-managed {@link ConnectionService}s which wish to change the audio route for a + * self-managed {@link Connection} (see {@link PhoneAccount#CAPABILITY_SELF_MANAGED}.) + * <p> + * See also {@link InCallService#setAudioRoute(int)}. + * + * @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}). + */ + public final void setAudioRoute(int route) { + for (Listener l : mListeners) { + l.onAudioRouteChanged(this, route); + } + } + + /** + * Informs listeners that a previously requested RTT session via + * {@link ConnectionRequest#isRequestingRtt()} or + * {@link #onStartRtt(ParcelFileDescriptor, ParcelFileDescriptor)} has succeeded. + * @hide + */ + @TestApi + public final void sendRttInitiationSuccess() { + setRttProperty(); + mListeners.forEach((l) -> l.onRttInitiationSuccess(Connection.this)); + } + + /** + * Informs listeners that a previously requested RTT session via + * {@link ConnectionRequest#isRequestingRtt()} or + * {@link #onStartRtt(ParcelFileDescriptor, ParcelFileDescriptor)} + * has failed. + * @param reason One of the reason codes defined in {@link RttModifyStatus}, with the + * exception of {@link RttModifyStatus#SESSION_MODIFY_REQUEST_SUCCESS}. + * @hide + */ + @TestApi + public final void sendRttInitiationFailure(int reason) { + unsetRttProperty(); + mListeners.forEach((l) -> l.onRttInitiationFailure(Connection.this, reason)); + } + + /** + * Informs listeners that a currently active RTT session has been terminated by the remote + * side of the coll. + * @hide + */ + @TestApi + public final void sendRttSessionRemotelyTerminated() { + mListeners.forEach((l) -> l.onRttSessionRemotelyTerminated(Connection.this)); + } + + /** + * Informs listeners that the remote side of the call has requested an upgrade to include an + * RTT session in the call. + * @hide + */ + @TestApi + public final void sendRemoteRttRequest() { + mListeners.forEach((l) -> l.onRemoteRttRequest(Connection.this)); + } + + /** + * Notifies this Connection that the {@link #getAudioState()} property has a new value. + * + * @param state The new connection audio state. + * @deprecated Use {@link #onCallAudioStateChanged(CallAudioState)} instead. + * @hide + */ + @SystemApi + @Deprecated + public void onAudioStateChanged(AudioState state) {} + + /** + * Notifies this Connection that the {@link #getCallAudioState()} property has a new value. + * + * @param state The new connection audio state. + */ + public void onCallAudioStateChanged(CallAudioState state) {} + + /** + * Notifies this Connection of an internal state change. This method is called after the + * state is changed. + * + * @param state The new state, one of the {@code STATE_*} constants. + */ + public void onStateChanged(int state) {} + + /** + * Notifies this Connection of a request to play a DTMF tone. + * + * @param c A DTMF character. + */ + public void onPlayDtmfTone(char c) {} + + /** + * Notifies this Connection of a request to stop any currently playing DTMF tones. + */ + public void onStopDtmfTone() {} + + /** + * Notifies this Connection of a request to disconnect. + */ + public void onDisconnect() {} + + /** + * Notifies this Connection of a request to disconnect a participant of the conference managed + * by the connection. + * + * @param endpoint the {@link Uri} of the participant to disconnect. + * @hide + */ + public void onDisconnectConferenceParticipant(Uri endpoint) {} + + /** + * Notifies this Connection of a request to separate from its parent conference. + */ + public void onSeparate() {} + + /** + * Notifies this Connection of a request to abort. + */ + public void onAbort() {} + + /** + * Notifies this Connection of a request to hold. + */ + public void onHold() {} + + /** + * Notifies this Connection of a request to exit a hold state. + */ + public void onUnhold() {} + + /** + * Notifies this Connection, which is in {@link #STATE_RINGING}, of + * a request to accept. + * + * @param videoState The video state in which to answer the connection. + */ + public void onAnswer(int videoState) {} + + /** + * Notifies this Connection, which is in {@link #STATE_RINGING}, of + * a request to accept. + */ + public void onAnswer() { + onAnswer(VideoProfile.STATE_AUDIO_ONLY); + } + + /** + * Notifies this Connection, which is in {@link #STATE_RINGING}, of + * a request to reject. + */ + public void onReject() {} + + /** + * Notifies this Connection, which is in {@link #STATE_RINGING}, of + * a request to reject with a message. + */ + public void onReject(String replyMessage) {} + + /** + * Notifies the Connection of a request to silence the ringer. + * + * @hide + */ + public void onSilence() {} + + /** + * Notifies this Connection whether the user wishes to proceed with the post-dial DTMF codes. + */ + public void onPostDialContinue(boolean proceed) {} + + /** + * Notifies this Connection of a request to pull an external call to the local device. + * <p> + * The {@link InCallService} issues a request to pull an external call to the local device via + * {@link Call#pullExternalCall()}. + * <p> + * For a Connection to be pulled, both the {@link Connection#CAPABILITY_CAN_PULL_CALL} + * capability and {@link Connection#PROPERTY_IS_EXTERNAL_CALL} property bits must be set. + * <p> + * For more information on external calls, see {@link Connection#PROPERTY_IS_EXTERNAL_CALL}. + */ + public void onPullExternalCall() {} + + /** + * Notifies this Connection of a {@link Call} event initiated from an {@link InCallService}. + * <p> + * The {@link InCallService} issues a Call event via {@link Call#sendCallEvent(String, Bundle)}. + * <p> + * Where possible, the Connection should make an attempt to handle {@link Call} events which + * are part of the {@code android.telecom.*} namespace. The Connection should ignore any events + * it does not wish to handle. Unexpected events should be handled gracefully, as it is + * possible that a {@link InCallService} has defined its own Call events which a Connection is + * not aware of. + * <p> + * See also {@link Call#sendCallEvent(String, Bundle)}. + * + * @param event The call event. + * @param extras Extras associated with the call event. + */ + public void onCallEvent(String event, Bundle extras) {} + + /** + * Notifies this {@link Connection} of a change to the extras made outside the + * {@link ConnectionService}. + * <p> + * These extras changes can originate from Telecom itself, or from an {@link InCallService} via + * the {@link android.telecom.Call#putExtras(Bundle)} and + * {@link Call#removeExtras(List)}. + * + * @param extras The new extras bundle. + */ + public void onExtrasChanged(Bundle extras) {} + + /** + * Notifies this {@link Connection} that its {@link ConnectionService} is responsible for + * displaying its incoming call user interface for the {@link Connection}. + * <p> + * Will only be called for incoming calls added via a self-managed {@link ConnectionService} + * (see {@link PhoneAccount#CAPABILITY_SELF_MANAGED}), where the {@link ConnectionService} + * should show its own incoming call user interface. + * <p> + * Where there are ongoing calls in other self-managed {@link ConnectionService}s, or in a + * regular {@link ConnectionService}, the Telecom framework will display its own incoming call + * user interface to allow the user to choose whether to answer the new incoming call and + * disconnect other ongoing calls, or to reject the new incoming call. + * <p> + * You should trigger the display of the incoming call user interface for your application by + * showing a {@link Notification} with a full-screen {@link Intent} specified. + * For example: + * <pre><code> + * // Create an intent which triggers your fullscreen incoming call user interface. + * Intent intent = new Intent(Intent.ACTION_MAIN, null); + * intent.setFlags(Intent.FLAG_ACTIVITY_NO_USER_ACTION | Intent.FLAG_ACTIVITY_NEW_TASK); + * intent.setClass(context, YourIncomingCallActivity.class); + * PendingIntent pendingIntent = PendingIntent.getActivity(context, 1, intent, 0); + * + * // Build the notification as an ongoing high priority item; this ensures it will show as + * // a heads up notification which slides down over top of the current content. + * final Notification.Builder builder = new Notification.Builder(context); + * builder.setOngoing(true); + * builder.setPriority(Notification.PRIORITY_HIGH); + * + * // Set notification content intent to take user to fullscreen UI if user taps on the + * // notification body. + * builder.setContentIntent(pendingIntent); + * // Set full screen intent to trigger display of the fullscreen UI when the notification + * // manager deems it appropriate. + * builder.setFullScreenIntent(pendingIntent, true); + * + * // Setup notification content. + * builder.setSmallIcon( yourIconResourceId ); + * builder.setContentTitle("Your notification title"); + * builder.setContentText("Your notification content."); + * + * // Use builder.addAction(..) to add buttons to answer or reject the call. + * + * NotificationManager notificationManager = mContext.getSystemService( + * NotificationManager.class); + * notificationManager.notify(YOUR_TAG, YOUR_ID, builder.build()); + * </code></pre> + */ + public void onShowIncomingCallUi() {} + + /** + * Notifies this {@link Connection} that the user has requested an RTT session. + * The connection service should call {@link #sendRttInitiationSuccess} or + * {@link #sendRttInitiationFailure} to inform Telecom of the success or failure of the + * request, respectively. + * @param rttTextStream The object that should be used to send text to or receive text from + * the in-call app. + * @hide + */ + @TestApi + public void onStartRtt(@NonNull RttTextStream rttTextStream) {} + + /** + * Notifies this {@link Connection} that it should terminate any existing RTT communication + * channel. No response to Telecom is needed for this method. + * @hide + */ + @TestApi + public void onStopRtt() {} + + /** + * Notifies this connection of a response to a previous remotely-initiated RTT upgrade + * request sent via {@link #sendRemoteRttRequest}. Acceptance of the request is + * indicated by the supplied {@link RttTextStream} being non-null, and rejection is + * indicated by {@code rttTextStream} being {@code null} + * @hide + * @param rttTextStream The object that should be used to send text to or receive text from + * the in-call app. + */ + @TestApi + public void handleRttUpgradeResponse(@Nullable RttTextStream rttTextStream) {} + + /** + * Internal method to set {@link #PROPERTY_IS_RTT}. + * @hide + */ + void setRttProperty() { + setConnectionProperties(getConnectionProperties() | PROPERTY_IS_RTT); + } + + /** + * Internal method to un-set {@link #PROPERTY_IS_RTT}. + * @hide + */ + void unsetRttProperty() { + setConnectionProperties(getConnectionProperties() & (~PROPERTY_IS_RTT)); + } + + static String toLogSafePhoneNumber(String number) { + // For unknown number, log empty string. + if (number == null) { + return ""; + } + + if (PII_DEBUG) { + // When PII_DEBUG is true we emit PII. + return number; + } + + // Do exactly same thing as Uri#toSafeString() does, which will enable us to compare + // sanitized phone numbers. + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < number.length(); i++) { + char c = number.charAt(i); + if (c == '-' || c == '@' || c == '.') { + builder.append(c); + } else { + builder.append('x'); + } + } + return builder.toString(); + } + + private void setState(int state) { + checkImmutable(); + if (mState == STATE_DISCONNECTED && mState != state) { + Log.d(this, "Connection already DISCONNECTED; cannot transition out of this state."); + return; + } + if (mState != state) { + Log.d(this, "setState: %s", stateToString(state)); + mState = state; + onStateChanged(state); + for (Listener l : mListeners) { + l.onStateChanged(this, state); + } + } + } + + private static class FailureSignalingConnection extends Connection { + private boolean mImmutable = false; + public FailureSignalingConnection(DisconnectCause disconnectCause) { + setDisconnected(disconnectCause); + mImmutable = true; + } + + public void checkImmutable() { + if (mImmutable) { + throw new UnsupportedOperationException("Connection is immutable"); + } + } + } + + /** + * Return a {@code Connection} which represents a failed connection attempt. The returned + * {@code Connection} will have a {@link android.telecom.DisconnectCause} and as specified, + * and a {@link #getState()} of {@link #STATE_DISCONNECTED}. + * <p> + * The returned {@code Connection} can be assumed to {@link #destroy()} itself when appropriate, + * so users of this method need not maintain a reference to its return value to destroy it. + * + * @param disconnectCause The disconnect cause, ({@see android.telecomm.DisconnectCause}). + * @return A {@code Connection} which indicates failure. + */ + public static Connection createFailedConnection(DisconnectCause disconnectCause) { + return new FailureSignalingConnection(disconnectCause); + } + + /** + * Override to throw an {@link UnsupportedOperationException} if this {@code Connection} is + * not intended to be mutated, e.g., if it is a marker for failure. Only for framework use; + * this should never be un-@hide-den. + * + * @hide + */ + public void checkImmutable() {} + + /** + * Return a {@code Connection} which represents a canceled connection attempt. The returned + * {@code Connection} will have state {@link #STATE_DISCONNECTED}, and cannot be moved out of + * that state. This connection should not be used for anything, and no other + * {@code Connection}s should be attempted. + * <p> + * so users of this method need not maintain a reference to its return value to destroy it. + * + * @return A {@code Connection} which indicates that the underlying connection should + * be canceled. + */ + public static Connection createCanceledConnection() { + return new FailureSignalingConnection(new DisconnectCause(DisconnectCause.CANCELED)); + } + + private final void fireOnConferenceableConnectionsChanged() { + for (Listener l : mListeners) { + l.onConferenceablesChanged(this, getConferenceables()); + } + } + + private final void fireConferenceChanged() { + for (Listener l : mListeners) { + l.onConferenceChanged(this, mConference); + } + } + + private final void clearConferenceableList() { + for (Conferenceable c : mConferenceables) { + if (c instanceof Connection) { + Connection connection = (Connection) c; + connection.removeConnectionListener(mConnectionDeathListener); + } else if (c instanceof Conference) { + Conference conference = (Conference) c; + conference.removeListener(mConferenceDeathListener); + } + } + mConferenceables.clear(); + } + + /** + * Handles a change to extras received from Telecom. + * + * @param extras The new extras. + * @hide + */ + final void handleExtrasChanged(Bundle extras) { + Bundle b = null; + synchronized (mExtrasLock) { + mExtras = extras; + if (mExtras != null) { + b = new Bundle(mExtras); + } + } + onExtrasChanged(b); + } + + /** + * Notifies listeners that the merge request failed. + * + * @hide + */ + protected final void notifyConferenceMergeFailed() { + for (Listener l : mListeners) { + l.onConferenceMergeFailed(this); + } + } + + /** + * Notifies listeners of a change to conference participant(s). + * + * @param conferenceParticipants The participants. + * @hide + */ + protected final void updateConferenceParticipants( + List<ConferenceParticipant> conferenceParticipants) { + for (Listener l : mListeners) { + l.onConferenceParticipantsChanged(this, conferenceParticipants); + } + } + + /** + * Notifies listeners that a conference call has been started. + * @hide + */ + protected void notifyConferenceStarted() { + for (Listener l : mListeners) { + l.onConferenceStarted(); + } + } + + /** + * Notifies listeners when a change has occurred to the Connection which impacts its ability to + * be a part of a conference call. + * @param isConferenceSupported {@code true} if the connection supports being part of a + * conference call, {@code false} otherwise. + * @hide + */ + protected void notifyConferenceSupportedChanged(boolean isConferenceSupported) { + for (Listener l : mListeners) { + l.onConferenceSupportedChanged(this, isConferenceSupported); + } + } + + /** + * Notifies listeners when phone account is changed. For example, when the PhoneAccount is + * changed due to an emergency call being redialed. + * @param pHandle The new PhoneAccountHandle for this connection. + * @hide + */ + public void notifyPhoneAccountChanged(PhoneAccountHandle pHandle) { + for (Listener l : mListeners) { + l.onPhoneAccountChanged(this, pHandle); + } + } + + /** + * Sends an event associated with this {@code Connection} with associated event extras to the + * {@link InCallService}. + * <p> + * Connection events are used to communicate point in time information from a + * {@link ConnectionService} to a {@link InCallService} implementations. An example of a + * custom connection event includes notifying the UI when a WIFI call has been handed over to + * LTE, which the InCall UI might use to inform the user that billing charges may apply. The + * Android Telephony framework will send the {@link #EVENT_CALL_MERGE_FAILED} connection event + * when a call to {@link Call#mergeConference()} has failed to complete successfully. A + * connection event could also be used to trigger UI in the {@link InCallService} which prompts + * the user to make a choice (e.g. whether they want to incur roaming costs for making a call), + * which is communicated back via {@link Call#sendCallEvent(String, Bundle)}. + * <p> + * Events are exposed to {@link InCallService} implementations via + * {@link Call.Callback#onConnectionEvent(Call, String, Bundle)}. + * <p> + * No assumptions should be made as to how an In-Call UI or service will handle these events. + * The {@link ConnectionService} must assume that the In-Call UI could even chose to ignore + * some events altogether. + * <p> + * Events should be fully qualified (e.g. {@code com.example.event.MY_EVENT}) to avoid + * conflicts between {@link ConnectionService} implementations. Further, custom + * {@link ConnectionService} implementations shall not re-purpose events in the + * {@code android.*} namespace, nor shall they define new event types in this namespace. When + * defining a custom event type, ensure the contents of the extras {@link Bundle} is clearly + * defined. Extra keys for this bundle should be named similar to the event type (e.g. + * {@code com.example.extra.MY_EXTRA}). + * <p> + * When defining events and the associated extras, it is important to keep their behavior + * consistent when the associated {@link ConnectionService} is updated. Support for deprecated + * events/extras should me maintained to ensure backwards compatibility with older + * {@link InCallService} implementations which were built to support the older behavior. + * + * @param event The connection event. + * @param extras Optional bundle containing extra information associated with the event. + */ + public void sendConnectionEvent(String event, Bundle extras) { + for (Listener l : mListeners) { + l.onConnectionEvent(this, event, extras); + } + } +} diff --git a/android/telecom/ConnectionRequest.java b/android/telecom/ConnectionRequest.java new file mode 100644 index 00000000..e169e5f8 --- /dev/null +++ b/android/telecom/ConnectionRequest.java @@ -0,0 +1,374 @@ +/* + * Copyright (C) 2014 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.TestApi; +import android.net.Uri; +import android.os.Bundle; +import android.os.Parcel; +import android.os.ParcelFileDescriptor; +import android.os.Parcelable; + +/** + * Simple data container encapsulating a request to some entity to + * create a new {@link Connection}. + */ +public final class ConnectionRequest implements Parcelable { + + /** + * Builder class for {@link ConnectionRequest} + * @hide + */ + public static final class Builder { + private PhoneAccountHandle mAccountHandle; + private Uri mAddress; + private Bundle mExtras; + private int mVideoState = VideoProfile.STATE_AUDIO_ONLY; + private String mTelecomCallId; + private boolean mShouldShowIncomingCallUi = false; + private ParcelFileDescriptor mRttPipeToInCall; + private ParcelFileDescriptor mRttPipeFromInCall; + + public Builder() { } + + /** + * Sets the phone account handle for the resulting {@link ConnectionRequest} + * @param accountHandle The accountHandle which should be used to place the call. + */ + public Builder setAccountHandle(PhoneAccountHandle accountHandle) { + this.mAccountHandle = accountHandle; + return this; + } + + /** + * Sets the address for the resulting {@link ConnectionRequest} + * @param address The address(e.g., phone number) to which the {@link Connection} is to + * connect. + */ + public Builder setAddress(Uri address) { + this.mAddress = address; + return this; + } + + /** + * Sets the extras bundle for the resulting {@link ConnectionRequest} + * @param extras Application-specific extra data. + */ + public Builder setExtras(Bundle extras) { + this.mExtras = extras; + return this; + } + + /** + * Sets the video state for the resulting {@link ConnectionRequest} + * @param videoState Determines the video state for the connection. + */ + public Builder setVideoState(int videoState) { + this.mVideoState = videoState; + return this; + } + + /** + * Sets the Telecom call ID for the resulting {@link ConnectionRequest} + * @param telecomCallId The telecom call ID. + */ + public Builder setTelecomCallId(String telecomCallId) { + this.mTelecomCallId = telecomCallId; + return this; + } + + /** + * Sets shouldShowIncomingUi for the resulting {@link ConnectionRequest} + * @param shouldShowIncomingCallUi For a self-managed {@link ConnectionService}, will be + * {@code true} if the {@link ConnectionService} should show + * its own incoming call UI for an incoming call. When + * {@code false}, Telecom shows the incoming call UI. + */ + public Builder setShouldShowIncomingCallUi(boolean shouldShowIncomingCallUi) { + this.mShouldShowIncomingCallUi = shouldShowIncomingCallUi; + return this; + } + + /** + * Sets the RTT pipe for transferring text into the {@link ConnectionService} for the + * resulting {@link ConnectionRequest} + * @param rttPipeFromInCall The data pipe to read from. + */ + public Builder setRttPipeFromInCall(ParcelFileDescriptor rttPipeFromInCall) { + this.mRttPipeFromInCall = rttPipeFromInCall; + return this; + } + + /** + * Sets the RTT pipe for transferring text out of {@link ConnectionService} for the + * resulting {@link ConnectionRequest} + * @param rttPipeToInCall The data pipe to write to. + */ + public Builder setRttPipeToInCall(ParcelFileDescriptor rttPipeToInCall) { + this.mRttPipeToInCall = rttPipeToInCall; + return this; + } + + public ConnectionRequest build() { + return new ConnectionRequest( + mAccountHandle, + mAddress, + mExtras, + mVideoState, + mTelecomCallId, + mShouldShowIncomingCallUi, + mRttPipeFromInCall, + mRttPipeToInCall); + } + } + + private final PhoneAccountHandle mAccountHandle; + private final Uri mAddress; + private final Bundle mExtras; + private final int mVideoState; + private final String mTelecomCallId; + private final boolean mShouldShowIncomingCallUi; + private final ParcelFileDescriptor mRttPipeToInCall; + private final ParcelFileDescriptor mRttPipeFromInCall; + + /** + * @param accountHandle The accountHandle which should be used to place the call. + * @param handle The handle (e.g., phone number) to which the {@link Connection} is to connect. + * @param extras Application-specific extra data. + */ + public ConnectionRequest( + PhoneAccountHandle accountHandle, + Uri handle, + Bundle extras) { + this(accountHandle, handle, extras, VideoProfile.STATE_AUDIO_ONLY, null, false, null, null); + } + + /** + * @param accountHandle The accountHandle which should be used to place the call. + * @param handle The handle (e.g., phone number) to which the {@link Connection} is to connect. + * @param extras Application-specific extra data. + * @param videoState Determines the video state for the connection. + */ + public ConnectionRequest( + PhoneAccountHandle accountHandle, + Uri handle, + Bundle extras, + int videoState) { + this(accountHandle, handle, extras, videoState, null, false, null, null); + } + + /** + * @param accountHandle The accountHandle which should be used to place the call. + * @param handle The handle (e.g., phone number) to which the {@link Connection} is to connect. + * @param extras Application-specific extra data. + * @param videoState Determines the video state for the connection. + * @param telecomCallId The telecom call ID. + * @param shouldShowIncomingCallUi For a self-managed {@link ConnectionService}, will be + * {@code true} if the {@link ConnectionService} should show its + * own incoming call UI for an incoming call. When + * {@code false}, Telecom shows the incoming call UI. + * @hide + */ + public ConnectionRequest( + PhoneAccountHandle accountHandle, + Uri handle, + Bundle extras, + int videoState, + String telecomCallId, + boolean shouldShowIncomingCallUi) { + this(accountHandle, handle, extras, videoState, telecomCallId, + shouldShowIncomingCallUi, null, null); + } + + private ConnectionRequest( + PhoneAccountHandle accountHandle, + Uri handle, + Bundle extras, + int videoState, + String telecomCallId, + boolean shouldShowIncomingCallUi, + ParcelFileDescriptor rttPipeFromInCall, + ParcelFileDescriptor rttPipeToInCall) { + mAccountHandle = accountHandle; + mAddress = handle; + mExtras = extras; + mVideoState = videoState; + mTelecomCallId = telecomCallId; + mShouldShowIncomingCallUi = shouldShowIncomingCallUi; + mRttPipeFromInCall = rttPipeFromInCall; + mRttPipeToInCall = rttPipeToInCall; + } + + private ConnectionRequest(Parcel in) { + mAccountHandle = in.readParcelable(getClass().getClassLoader()); + mAddress = in.readParcelable(getClass().getClassLoader()); + mExtras = in.readParcelable(getClass().getClassLoader()); + mVideoState = in.readInt(); + mTelecomCallId = in.readString(); + mShouldShowIncomingCallUi = in.readInt() == 1; + mRttPipeFromInCall = in.readParcelable(getClass().getClassLoader()); + mRttPipeToInCall = in.readParcelable(getClass().getClassLoader()); + } + + /** + * The account which should be used to place the call. + */ + public PhoneAccountHandle getAccountHandle() { return mAccountHandle; } + + /** + * The handle (e.g., phone number) to which the {@link Connection} is to connect. + */ + public Uri getAddress() { return mAddress; } + + /** + * Application-specific extra data. Used for passing back information from an incoming + * call {@code Intent}, and for any proprietary extensions arranged between a client + * and servant {@code ConnectionService} which agree on a vocabulary for such data. + */ + public Bundle getExtras() { return mExtras; } + + /** + * Describes the video states supported by the client requesting the connection. + * Valid values: {@link VideoProfile#STATE_AUDIO_ONLY}, + * {@link VideoProfile#STATE_BIDIRECTIONAL}, + * {@link VideoProfile#STATE_TX_ENABLED}, + * {@link VideoProfile#STATE_RX_ENABLED}. + * + * @return The video state for the connection. + */ + public int getVideoState() { + return mVideoState; + } + + /** + * Returns the internal Telecom ID associated with the connection request. + * + * @return The Telecom ID. + * @hide + */ + public String getTelecomCallId() { + return mTelecomCallId; + } + + /** + * For a self-managed {@link ConnectionService}, indicates for an incoming call whether the + * {@link ConnectionService} should show its own incoming call UI for an incoming call. + * + * @return {@code true} if the {@link ConnectionService} should show its own incoming call UI. + * When {@code false}, Telecom shows the incoming call UI for the call. + * @hide + */ + public boolean shouldShowIncomingCallUi() { + return mShouldShowIncomingCallUi; + } + + /** + * Gets the {@link ParcelFileDescriptor} that is used to send RTT text from the connection + * service to the in-call UI. In order to obtain an + * {@link java.io.InputStream} from this {@link ParcelFileDescriptor}, use + * {@link android.os.ParcelFileDescriptor.AutoCloseInputStream}. + * Only text data encoded using UTF-8 should be written into this {@link ParcelFileDescriptor}. + * @return The {@link ParcelFileDescriptor} that should be used for communication. + * Do not un-hide -- only for use by Telephony + * @hide + */ + public ParcelFileDescriptor getRttPipeToInCall() { + return mRttPipeToInCall; + } + + /** + * Gets the {@link ParcelFileDescriptor} that is used to send RTT text from the in-call UI to + * the connection service. In order to obtain an + * {@link java.io.OutputStream} from this {@link ParcelFileDescriptor}, use + * {@link android.os.ParcelFileDescriptor.AutoCloseOutputStream}. + * The contents of this {@link ParcelFileDescriptor} will consist solely of text encoded in + * UTF-8. + * @return The {@link ParcelFileDescriptor} that should be used for communication + * Do not un-hide -- only for use by Telephony + * @hide + */ + public ParcelFileDescriptor getRttPipeFromInCall() { + return mRttPipeFromInCall; + } + + /** + * Gets the {@link android.telecom.Connection.RttTextStream} object that should be used to + * send and receive RTT text to/from the in-call app. + * @return An instance of {@link android.telecom.Connection.RttTextStream}, or {@code null} + * if this connection request is not requesting an RTT session upon connection establishment. + * @hide + */ + @TestApi + public Connection.RttTextStream getRttTextStream() { + if (isRequestingRtt()) { + return new Connection.RttTextStream(mRttPipeToInCall, mRttPipeFromInCall); + } else { + return null; + } + } + + /** + * Convenience method for determining whether the ConnectionRequest is requesting an RTT session + * @return {@code true} if RTT is requested, {@code false} otherwise. + * @hide + */ + @TestApi + public boolean isRequestingRtt() { + return mRttPipeFromInCall != null && mRttPipeToInCall != null; + } + + @Override + public String toString() { + return String.format("ConnectionRequest %s %s", + mAddress == null + ? Uri.EMPTY + : Connection.toLogSafePhoneNumber(mAddress.toString()), + mExtras == null ? "" : mExtras); + } + + public static final Creator<ConnectionRequest> CREATOR = new Creator<ConnectionRequest> () { + @Override + public ConnectionRequest createFromParcel(Parcel source) { + return new ConnectionRequest(source); + } + + @Override + public ConnectionRequest[] newArray(int size) { + return new ConnectionRequest[size]; + } + }; + + /** + * {@inheritDoc} + */ + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel destination, int flags) { + destination.writeParcelable(mAccountHandle, 0); + destination.writeParcelable(mAddress, 0); + destination.writeParcelable(mExtras, 0); + destination.writeInt(mVideoState); + destination.writeString(mTelecomCallId); + destination.writeInt(mShouldShowIncomingCallUi ? 1 : 0); + destination.writeParcelable(mRttPipeFromInCall, 0); + destination.writeParcelable(mRttPipeToInCall, 0); + } +} diff --git a/android/telecom/ConnectionService.java b/android/telecom/ConnectionService.java new file mode 100644 index 00000000..a81fba95 --- /dev/null +++ b/android/telecom/ConnectionService.java @@ -0,0 +1,2276 @@ +/* + * Copyright (C) 2014 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.SdkConstant; +import android.app.Service; +import android.content.ComponentName; +import android.content.Intent; +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.ParcelFileDescriptor; +import android.os.RemoteException; +import android.telecom.Logging.Session; + +import com.android.internal.os.SomeArgs; +import com.android.internal.telecom.IConnectionService; +import com.android.internal.telecom.IConnectionServiceAdapter; +import com.android.internal.telecom.RemoteServiceCallback; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +/** + * An abstract service that should be implemented by any apps which either: + * <ol> + * <li>Can make phone calls (VoIP or otherwise) and want those calls to be integrated into the + * built-in phone app. Referred to as a <b>system managed</b> {@link ConnectionService}.</li> + * <li>Are a standalone calling app and don't want their calls to be integrated into the + * built-in phone app. Referred to as a <b>self managed</b> {@link ConnectionService}.</li> + * </ol> + * Once implemented, the {@link ConnectionService} needs to take the following steps so that Telecom + * will bind to it: + * <p> + * 1. <i>Registration in AndroidManifest.xml</i> + * <br/> + * <pre> + * <service android:name="com.example.package.MyConnectionService" + * android:label="@string/some_label_for_my_connection_service" + * android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE"> + * <intent-filter> + * <action android:name="android.telecom.ConnectionService" /> + * </intent-filter> + * </service> + * </pre> + * <p> + * 2. <i> Registration of {@link PhoneAccount} with {@link TelecomManager}.</i> + * <br/> + * 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-manged {@link ConnectionService}s must be granted the + * appropriate permission 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} + * 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}. + */ +public abstract class ConnectionService extends Service { + /** + * The {@link Intent} that must be declared as handled by the service. + */ + @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION) + public static final String SERVICE_INTERFACE = "android.telecom.ConnectionService"; + + /** + * Boolean extra used by Telecom to inform a {@link ConnectionService} that the purpose of it + * being asked to create a new outgoing {@link Connection} is to perform a handover of an + * ongoing call on the device from another {@link PhoneAccount}/{@link ConnectionService}. Will + * be specified in the {@link ConnectionRequest#getExtras()} passed by Telecom when + * {@link #onCreateOutgoingConnection(PhoneAccountHandle, ConnectionRequest)} is called. + * <p> + * When your {@link ConnectionService} receives this extra, it should communicate the fact that + * this is a handover to the other device's matching {@link ConnectionService}. That + * {@link ConnectionService} will continue the handover using + * {@link TelecomManager#addNewIncomingCall(PhoneAccountHandle, Bundle)}, specifying + * {@link TelecomManager#EXTRA_IS_HANDOVER}. Telecom will match the phone numbers of the + * handover call on the other device with ongoing calls for {@link ConnectionService}s which + * support {@link PhoneAccount#EXTRA_SUPPORTS_HANDOVER_FROM}. + * @hide + */ + public static final String EXTRA_IS_HANDOVER = TelecomManager.EXTRA_IS_HANDOVER; + + // Flag controlling whether PII is emitted into the logs + private static final boolean PII_DEBUG = Log.isLoggable(android.util.Log.DEBUG); + + // Session Definitions + private static final String SESSION_HANDLER = "H."; + private static final String SESSION_ADD_CS_ADAPTER = "CS.aCSA"; + private static final String SESSION_REMOVE_CS_ADAPTER = "CS.rCSA"; + private static final String SESSION_CREATE_CONN = "CS.crCo"; + private static final String SESSION_CREATE_CONN_COMPLETE = "CS.crCoC"; + private static final String SESSION_CREATE_CONN_FAILED = "CS.crCoF"; + private static final String SESSION_ABORT = "CS.ab"; + private static final String SESSION_ANSWER = "CS.an"; + private static final String SESSION_ANSWER_VIDEO = "CS.anV"; + private static final String SESSION_REJECT = "CS.r"; + private static final String SESSION_REJECT_MESSAGE = "CS.rWM"; + private static final String SESSION_SILENCE = "CS.s"; + private static final String SESSION_DISCONNECT = "CS.d"; + private static final String SESSION_HOLD = "CS.h"; + private static final String SESSION_UNHOLD = "CS.u"; + private static final String SESSION_CALL_AUDIO_SC = "CS.cASC"; + private static final String SESSION_PLAY_DTMF = "CS.pDT"; + private static final String SESSION_STOP_DTMF = "CS.sDT"; + private static final String SESSION_CONFERENCE = "CS.c"; + private static final String SESSION_SPLIT_CONFERENCE = "CS.sFC"; + private static final String SESSION_MERGE_CONFERENCE = "CS.mC"; + private static final String SESSION_SWAP_CONFERENCE = "CS.sC"; + private static final String SESSION_POST_DIAL_CONT = "CS.oPDC"; + private static final String SESSION_PULL_EXTERNAL_CALL = "CS.pEC"; + private static final String SESSION_SEND_CALL_EVENT = "CS.sCE"; + private static final String SESSION_EXTRAS_CHANGED = "CS.oEC"; + private static final String SESSION_START_RTT = "CS.+RTT"; + private static final String SESSION_STOP_RTT = "CS.-RTT"; + private static final String SESSION_RTT_UPGRADE_RESPONSE = "CS.rTRUR"; + + private static final int MSG_ADD_CONNECTION_SERVICE_ADAPTER = 1; + private static final int MSG_CREATE_CONNECTION = 2; + private static final int MSG_ABORT = 3; + private static final int MSG_ANSWER = 4; + private static final int MSG_REJECT = 5; + private static final int MSG_DISCONNECT = 6; + private static final int MSG_HOLD = 7; + private static final int MSG_UNHOLD = 8; + private static final int MSG_ON_CALL_AUDIO_STATE_CHANGED = 9; + private static final int MSG_PLAY_DTMF_TONE = 10; + private static final int MSG_STOP_DTMF_TONE = 11; + private static final int MSG_CONFERENCE = 12; + private static final int MSG_SPLIT_FROM_CONFERENCE = 13; + private static final int MSG_ON_POST_DIAL_CONTINUE = 14; + private static final int MSG_REMOVE_CONNECTION_SERVICE_ADAPTER = 16; + private static final int MSG_ANSWER_VIDEO = 17; + private static final int MSG_MERGE_CONFERENCE = 18; + private static final int MSG_SWAP_CONFERENCE = 19; + private static final int MSG_REJECT_WITH_MESSAGE = 20; + private static final int MSG_SILENCE = 21; + private static final int MSG_PULL_EXTERNAL_CALL = 22; + private static final int MSG_SEND_CALL_EVENT = 23; + private static final int MSG_ON_EXTRAS_CHANGED = 24; + private static final int MSG_CREATE_CONNECTION_FAILED = 25; + private static final int MSG_ON_START_RTT = 26; + private static final int MSG_ON_STOP_RTT = 27; + private static final int MSG_RTT_UPGRADE_RESPONSE = 28; + private static final int MSG_CREATE_CONNECTION_COMPLETE = 29; + + private static Connection sNullConnection; + + private final Map<String, Connection> mConnectionById = new ConcurrentHashMap<>(); + private final Map<Connection, String> mIdByConnection = new ConcurrentHashMap<>(); + private final Map<String, Conference> mConferenceById = new ConcurrentHashMap<>(); + private final Map<Conference, String> mIdByConference = new ConcurrentHashMap<>(); + private final RemoteConnectionManager mRemoteConnectionManager = + new RemoteConnectionManager(this); + private final List<Runnable> mPreInitializationConnectionRequests = new ArrayList<>(); + private final ConnectionServiceAdapter mAdapter = new ConnectionServiceAdapter(); + + private boolean mAreAccountsInitialized = false; + private Conference sNullConference; + private Object mIdSyncRoot = new Object(); + private int mId = 0; + + private final IBinder mBinder = new IConnectionService.Stub() { + @Override + public void addConnectionServiceAdapter(IConnectionServiceAdapter adapter, + Session.Info sessionInfo) { + Log.startSession(sessionInfo, SESSION_ADD_CS_ADAPTER); + try { + SomeArgs args = SomeArgs.obtain(); + args.arg1 = adapter; + args.arg2 = Log.createSubsession(); + mHandler.obtainMessage(MSG_ADD_CONNECTION_SERVICE_ADAPTER, args).sendToTarget(); + } finally { + Log.endSession(); + } + } + + public void removeConnectionServiceAdapter(IConnectionServiceAdapter adapter, + Session.Info sessionInfo) { + Log.startSession(sessionInfo, SESSION_REMOVE_CS_ADAPTER); + try { + SomeArgs args = SomeArgs.obtain(); + args.arg1 = adapter; + args.arg2 = Log.createSubsession(); + mHandler.obtainMessage(MSG_REMOVE_CONNECTION_SERVICE_ADAPTER, args).sendToTarget(); + } finally { + Log.endSession(); + } + } + + @Override + public void createConnection( + PhoneAccountHandle connectionManagerPhoneAccount, + String id, + ConnectionRequest request, + boolean isIncoming, + boolean isUnknown, + Session.Info sessionInfo) { + Log.startSession(sessionInfo, SESSION_CREATE_CONN); + try { + SomeArgs args = SomeArgs.obtain(); + args.arg1 = connectionManagerPhoneAccount; + args.arg2 = id; + args.arg3 = request; + args.arg4 = Log.createSubsession(); + args.argi1 = isIncoming ? 1 : 0; + args.argi2 = isUnknown ? 1 : 0; + mHandler.obtainMessage(MSG_CREATE_CONNECTION, args).sendToTarget(); + } finally { + Log.endSession(); + } + } + + @Override + public void createConnectionComplete(String id, Session.Info sessionInfo) { + Log.startSession(sessionInfo, SESSION_CREATE_CONN_COMPLETE); + try { + SomeArgs args = SomeArgs.obtain(); + args.arg1 = id; + args.arg2 = Log.createSubsession(); + mHandler.obtainMessage(MSG_CREATE_CONNECTION_COMPLETE, args).sendToTarget(); + } finally { + Log.endSession(); + } + } + + @Override + public void createConnectionFailed( + PhoneAccountHandle connectionManagerPhoneAccount, + String callId, + ConnectionRequest request, + boolean isIncoming, + Session.Info sessionInfo) { + Log.startSession(sessionInfo, SESSION_CREATE_CONN_FAILED); + try { + SomeArgs args = SomeArgs.obtain(); + args.arg1 = callId; + args.arg2 = request; + args.arg3 = Log.createSubsession(); + args.arg4 = connectionManagerPhoneAccount; + args.argi1 = isIncoming ? 1 : 0; + mHandler.obtainMessage(MSG_CREATE_CONNECTION_FAILED, args).sendToTarget(); + } finally { + Log.endSession(); + } + } + + @Override + public void abort(String callId, Session.Info sessionInfo) { + Log.startSession(sessionInfo, SESSION_ABORT); + try { + SomeArgs args = SomeArgs.obtain(); + args.arg1 = callId; + args.arg2 = Log.createSubsession(); + mHandler.obtainMessage(MSG_ABORT, args).sendToTarget(); + } finally { + Log.endSession(); + } + } + + @Override + public void answerVideo(String callId, int videoState, Session.Info sessionInfo) { + Log.startSession(sessionInfo, SESSION_ANSWER_VIDEO); + try { + SomeArgs args = SomeArgs.obtain(); + args.arg1 = callId; + args.arg2 = Log.createSubsession(); + args.argi1 = videoState; + mHandler.obtainMessage(MSG_ANSWER_VIDEO, args).sendToTarget(); + } finally { + Log.endSession(); + } + } + + @Override + public void answer(String callId, Session.Info sessionInfo) { + Log.startSession(sessionInfo, SESSION_ANSWER); + try { + SomeArgs args = SomeArgs.obtain(); + args.arg1 = callId; + args.arg2 = Log.createSubsession(); + mHandler.obtainMessage(MSG_ANSWER, args).sendToTarget(); + } finally { + Log.endSession(); + } + } + + @Override + public void reject(String callId, Session.Info sessionInfo) { + Log.startSession(sessionInfo, SESSION_REJECT); + try { + SomeArgs args = SomeArgs.obtain(); + args.arg1 = callId; + args.arg2 = Log.createSubsession(); + mHandler.obtainMessage(MSG_REJECT, args).sendToTarget(); + } finally { + Log.endSession(); + } + } + + @Override + public void rejectWithMessage(String callId, String message, Session.Info sessionInfo) { + Log.startSession(sessionInfo, SESSION_REJECT_MESSAGE); + try { + SomeArgs args = SomeArgs.obtain(); + args.arg1 = callId; + args.arg2 = message; + args.arg3 = Log.createSubsession(); + mHandler.obtainMessage(MSG_REJECT_WITH_MESSAGE, args).sendToTarget(); + } finally { + Log.endSession(); + } + } + + @Override + public void silence(String callId, Session.Info sessionInfo) { + Log.startSession(sessionInfo, SESSION_SILENCE); + try { + SomeArgs args = SomeArgs.obtain(); + args.arg1 = callId; + args.arg2 = Log.createSubsession(); + mHandler.obtainMessage(MSG_SILENCE, args).sendToTarget(); + } finally { + Log.endSession(); + } + } + + @Override + public void disconnect(String callId, Session.Info sessionInfo) { + Log.startSession(sessionInfo, SESSION_DISCONNECT); + try { + SomeArgs args = SomeArgs.obtain(); + args.arg1 = callId; + args.arg2 = Log.createSubsession(); + mHandler.obtainMessage(MSG_DISCONNECT, args).sendToTarget(); + } finally { + Log.endSession(); + } + } + + @Override + public void hold(String callId, Session.Info sessionInfo) { + Log.startSession(sessionInfo, SESSION_HOLD); + try { + SomeArgs args = SomeArgs.obtain(); + args.arg1 = callId; + args.arg2 = Log.createSubsession(); + mHandler.obtainMessage(MSG_HOLD, args).sendToTarget(); + } finally { + Log.endSession(); + } + } + + @Override + public void unhold(String callId, Session.Info sessionInfo) { + Log.startSession(sessionInfo, SESSION_UNHOLD); + try { + SomeArgs args = SomeArgs.obtain(); + args.arg1 = callId; + args.arg2 = Log.createSubsession(); + mHandler.obtainMessage(MSG_UNHOLD, args).sendToTarget(); + } finally { + Log.endSession(); + } + } + + @Override + public void onCallAudioStateChanged(String callId, CallAudioState callAudioState, + Session.Info sessionInfo) { + Log.startSession(sessionInfo, SESSION_CALL_AUDIO_SC); + try { + SomeArgs args = SomeArgs.obtain(); + args.arg1 = callId; + args.arg2 = callAudioState; + args.arg3 = Log.createSubsession(); + mHandler.obtainMessage(MSG_ON_CALL_AUDIO_STATE_CHANGED, args).sendToTarget(); + } finally { + Log.endSession(); + } + } + + @Override + public void playDtmfTone(String callId, char digit, Session.Info sessionInfo) { + Log.startSession(sessionInfo, SESSION_PLAY_DTMF); + try { + SomeArgs args = SomeArgs.obtain(); + args.arg1 = digit; + args.arg2 = callId; + args.arg3 = Log.createSubsession(); + mHandler.obtainMessage(MSG_PLAY_DTMF_TONE, args).sendToTarget(); + } finally { + Log.endSession(); + } + } + + @Override + public void stopDtmfTone(String callId, Session.Info sessionInfo) { + Log.startSession(sessionInfo, SESSION_STOP_DTMF); + try { + SomeArgs args = SomeArgs.obtain(); + args.arg1 = callId; + args.arg2 = Log.createSubsession(); + mHandler.obtainMessage(MSG_STOP_DTMF_TONE, args).sendToTarget(); + } finally { + Log.endSession(); + } + } + + @Override + public void conference(String callId1, String callId2, Session.Info sessionInfo) { + Log.startSession(sessionInfo, SESSION_CONFERENCE); + try { + SomeArgs args = SomeArgs.obtain(); + args.arg1 = callId1; + args.arg2 = callId2; + args.arg3 = Log.createSubsession(); + mHandler.obtainMessage(MSG_CONFERENCE, args).sendToTarget(); + } finally { + Log.endSession(); + } + } + + @Override + public void splitFromConference(String callId, Session.Info sessionInfo) { + Log.startSession(sessionInfo, SESSION_SPLIT_CONFERENCE); + try { + SomeArgs args = SomeArgs.obtain(); + args.arg1 = callId; + args.arg2 = Log.createSubsession(); + mHandler.obtainMessage(MSG_SPLIT_FROM_CONFERENCE, args).sendToTarget(); + } finally { + Log.endSession(); + } + } + + @Override + public void mergeConference(String callId, Session.Info sessionInfo) { + Log.startSession(sessionInfo, SESSION_MERGE_CONFERENCE); + try { + SomeArgs args = SomeArgs.obtain(); + args.arg1 = callId; + args.arg2 = Log.createSubsession(); + mHandler.obtainMessage(MSG_MERGE_CONFERENCE, args).sendToTarget(); + } finally { + Log.endSession(); + } + } + + @Override + public void swapConference(String callId, Session.Info sessionInfo) { + Log.startSession(sessionInfo, SESSION_SWAP_CONFERENCE); + try { + SomeArgs args = SomeArgs.obtain(); + args.arg1 = callId; + args.arg2 = Log.createSubsession(); + mHandler.obtainMessage(MSG_SWAP_CONFERENCE, args).sendToTarget(); + } finally { + Log.endSession(); + } + } + + @Override + public void onPostDialContinue(String callId, boolean proceed, Session.Info sessionInfo) { + Log.startSession(sessionInfo, SESSION_POST_DIAL_CONT); + try { + SomeArgs args = SomeArgs.obtain(); + args.arg1 = callId; + args.arg2 = Log.createSubsession(); + args.argi1 = proceed ? 1 : 0; + mHandler.obtainMessage(MSG_ON_POST_DIAL_CONTINUE, args).sendToTarget(); + } finally { + Log.endSession(); + } + } + + @Override + public void pullExternalCall(String callId, Session.Info sessionInfo) { + Log.startSession(sessionInfo, SESSION_PULL_EXTERNAL_CALL); + try { + SomeArgs args = SomeArgs.obtain(); + args.arg1 = callId; + args.arg2 = Log.createSubsession(); + mHandler.obtainMessage(MSG_PULL_EXTERNAL_CALL, args).sendToTarget(); + } finally { + Log.endSession(); + } + } + + @Override + public void sendCallEvent(String callId, String event, Bundle extras, + Session.Info sessionInfo) { + Log.startSession(sessionInfo, SESSION_SEND_CALL_EVENT); + try { + SomeArgs args = SomeArgs.obtain(); + args.arg1 = callId; + args.arg2 = event; + args.arg3 = extras; + args.arg4 = Log.createSubsession(); + mHandler.obtainMessage(MSG_SEND_CALL_EVENT, args).sendToTarget(); + } finally { + Log.endSession(); + } + } + + @Override + public void onExtrasChanged(String callId, Bundle extras, Session.Info sessionInfo) { + Log.startSession(sessionInfo, SESSION_EXTRAS_CHANGED); + try { + SomeArgs args = SomeArgs.obtain(); + args.arg1 = callId; + args.arg2 = extras; + args.arg3 = Log.createSubsession(); + mHandler.obtainMessage(MSG_ON_EXTRAS_CHANGED, args).sendToTarget(); + } finally { + Log.endSession(); + } + } + + @Override + public void startRtt(String callId, ParcelFileDescriptor fromInCall, + ParcelFileDescriptor toInCall, Session.Info sessionInfo) throws RemoteException { + Log.startSession(sessionInfo, SESSION_START_RTT); + try { + SomeArgs args = SomeArgs.obtain(); + args.arg1 = callId; + args.arg2 = new Connection.RttTextStream(toInCall, fromInCall); + args.arg3 = Log.createSubsession(); + mHandler.obtainMessage(MSG_ON_START_RTT, args).sendToTarget(); + } finally { + Log.endSession(); + } + } + + @Override + public void stopRtt(String callId, Session.Info sessionInfo) throws RemoteException { + Log.startSession(sessionInfo, SESSION_STOP_RTT); + try { + SomeArgs args = SomeArgs.obtain(); + args.arg1 = callId; + args.arg2 = Log.createSubsession(); + mHandler.obtainMessage(MSG_ON_STOP_RTT, args).sendToTarget(); + } finally { + Log.endSession(); + } + } + + @Override + public void respondToRttUpgradeRequest(String callId, ParcelFileDescriptor fromInCall, + ParcelFileDescriptor toInCall, Session.Info sessionInfo) throws RemoteException { + Log.startSession(sessionInfo, SESSION_RTT_UPGRADE_RESPONSE); + try { + SomeArgs args = SomeArgs.obtain(); + args.arg1 = callId; + if (toInCall == null || fromInCall == null) { + args.arg2 = null; + } else { + args.arg2 = new Connection.RttTextStream(toInCall, fromInCall); + } + args.arg3 = Log.createSubsession(); + mHandler.obtainMessage(MSG_RTT_UPGRADE_RESPONSE, args).sendToTarget(); + } finally { + Log.endSession(); + } + } + }; + + private final Handler mHandler = new Handler(Looper.getMainLooper()) { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_ADD_CONNECTION_SERVICE_ADAPTER: { + SomeArgs args = (SomeArgs) msg.obj; + try { + IConnectionServiceAdapter adapter = (IConnectionServiceAdapter) args.arg1; + Log.continueSession((Session) args.arg2, + SESSION_HANDLER + SESSION_ADD_CS_ADAPTER); + mAdapter.addAdapter(adapter); + onAdapterAttached(); + } finally { + args.recycle(); + Log.endSession(); + } + break; + } + case MSG_REMOVE_CONNECTION_SERVICE_ADAPTER: { + SomeArgs args = (SomeArgs) msg.obj; + try { + Log.continueSession((Session) args.arg2, + SESSION_HANDLER + SESSION_REMOVE_CS_ADAPTER); + mAdapter.removeAdapter((IConnectionServiceAdapter) args.arg1); + } finally { + args.recycle(); + Log.endSession(); + } + break; + } + case MSG_CREATE_CONNECTION: { + SomeArgs args = (SomeArgs) msg.obj; + Log.continueSession((Session) args.arg4, SESSION_HANDLER + SESSION_CREATE_CONN); + try { + final PhoneAccountHandle connectionManagerPhoneAccount = + (PhoneAccountHandle) args.arg1; + final String id = (String) args.arg2; + final ConnectionRequest request = (ConnectionRequest) args.arg3; + final boolean isIncoming = args.argi1 == 1; + final boolean isUnknown = args.argi2 == 1; + if (!mAreAccountsInitialized) { + Log.d(this, "Enqueueing pre-init request %s", id); + mPreInitializationConnectionRequests.add( + new android.telecom.Logging.Runnable( + SESSION_HANDLER + SESSION_CREATE_CONN + ".pICR", + null /*lock*/) { + @Override + public void loggedRun() { + createConnection( + connectionManagerPhoneAccount, + id, + request, + isIncoming, + isUnknown); + } + }.prepare()); + } else { + createConnection( + connectionManagerPhoneAccount, + id, + request, + isIncoming, + isUnknown); + } + } finally { + args.recycle(); + Log.endSession(); + } + break; + } + case MSG_CREATE_CONNECTION_COMPLETE: { + SomeArgs args = (SomeArgs) msg.obj; + Log.continueSession((Session) args.arg2, + SESSION_HANDLER + SESSION_CREATE_CONN_COMPLETE); + try { + final String id = (String) args.arg1; + if (!mAreAccountsInitialized) { + Log.d(this, "Enqueueing pre-init request %s", id); + mPreInitializationConnectionRequests.add( + new android.telecom.Logging.Runnable( + SESSION_HANDLER + SESSION_CREATE_CONN_COMPLETE + + ".pICR", + null /*lock*/) { + @Override + public void loggedRun() { + notifyCreateConnectionComplete(id); + } + }.prepare()); + } else { + notifyCreateConnectionComplete(id); + } + } finally { + args.recycle(); + Log.endSession(); + } + break; + } + case MSG_CREATE_CONNECTION_FAILED: { + SomeArgs args = (SomeArgs) msg.obj; + Log.continueSession((Session) args.arg3, SESSION_HANDLER + + SESSION_CREATE_CONN_FAILED); + try { + final String id = (String) args.arg1; + final ConnectionRequest request = (ConnectionRequest) args.arg2; + final boolean isIncoming = args.argi1 == 1; + final PhoneAccountHandle connectionMgrPhoneAccount = + (PhoneAccountHandle) args.arg4; + if (!mAreAccountsInitialized) { + Log.d(this, "Enqueueing pre-init request %s", id); + mPreInitializationConnectionRequests.add( + new android.telecom.Logging.Runnable( + SESSION_HANDLER + SESSION_CREATE_CONN_FAILED + ".pICR", + null /*lock*/) { + @Override + public void loggedRun() { + createConnectionFailed(connectionMgrPhoneAccount, id, + request, isIncoming); + } + }.prepare()); + } else { + Log.i(this, "createConnectionFailed %s", id); + createConnectionFailed(connectionMgrPhoneAccount, id, request, + isIncoming); + } + } finally { + args.recycle(); + Log.endSession(); + } + break; + } + case MSG_ABORT: { + SomeArgs args = (SomeArgs) msg.obj; + Log.continueSession((Session) args.arg2, SESSION_HANDLER + SESSION_ABORT); + try { + abort((String) args.arg1); + } finally { + args.recycle(); + Log.endSession(); + } + break; + } + case MSG_ANSWER: { + SomeArgs args = (SomeArgs) msg.obj; + Log.continueSession((Session) args.arg2, SESSION_HANDLER + SESSION_ANSWER); + try { + answer((String) args.arg1); + } finally { + args.recycle(); + Log.endSession(); + } + break; + } + case MSG_ANSWER_VIDEO: { + SomeArgs args = (SomeArgs) msg.obj; + Log.continueSession((Session) args.arg2, + SESSION_HANDLER + SESSION_ANSWER_VIDEO); + try { + String callId = (String) args.arg1; + int videoState = args.argi1; + answerVideo(callId, videoState); + } finally { + args.recycle(); + Log.endSession(); + } + break; + } + case MSG_REJECT: { + SomeArgs args = (SomeArgs) msg.obj; + Log.continueSession((Session) args.arg2, SESSION_HANDLER + SESSION_REJECT); + try { + reject((String) args.arg1); + } finally { + args.recycle(); + Log.endSession(); + } + break; + } + case MSG_REJECT_WITH_MESSAGE: { + SomeArgs args = (SomeArgs) msg.obj; + Log.continueSession((Session) args.arg3, + SESSION_HANDLER + SESSION_REJECT_MESSAGE); + try { + reject((String) args.arg1, (String) args.arg2); + } finally { + args.recycle(); + Log.endSession(); + } + break; + } + case MSG_DISCONNECT: { + SomeArgs args = (SomeArgs) msg.obj; + Log.continueSession((Session) args.arg2, SESSION_HANDLER + SESSION_DISCONNECT); + try { + disconnect((String) args.arg1); + } finally { + args.recycle(); + Log.endSession(); + } + break; + } + case MSG_SILENCE: { + SomeArgs args = (SomeArgs) msg.obj; + Log.continueSession((Session) args.arg2, SESSION_HANDLER + SESSION_SILENCE); + try { + silence((String) args.arg1); + } finally { + args.recycle(); + Log.endSession(); + } + break; + } + case MSG_HOLD: { + SomeArgs args = (SomeArgs) msg.obj; + Log.continueSession((Session) args.arg2, SESSION_HANDLER + SESSION_REJECT); + try { + hold((String) args.arg1); + } finally { + args.recycle(); + Log.endSession(); + } + break; + } + case MSG_UNHOLD: { + SomeArgs args = (SomeArgs) msg.obj; + Log.continueSession((Session) args.arg2, SESSION_HANDLER + SESSION_UNHOLD); + try { + unhold((String) args.arg1); + } finally { + args.recycle(); + Log.endSession(); + } + break; + } + case MSG_ON_CALL_AUDIO_STATE_CHANGED: { + SomeArgs args = (SomeArgs) msg.obj; + Log.continueSession((Session) args.arg3, + SESSION_HANDLER + SESSION_CALL_AUDIO_SC); + try { + String callId = (String) args.arg1; + CallAudioState audioState = (CallAudioState) args.arg2; + onCallAudioStateChanged(callId, new CallAudioState(audioState)); + } finally { + args.recycle(); + Log.endSession(); + } + break; + } + case MSG_PLAY_DTMF_TONE: { + SomeArgs args = (SomeArgs) msg.obj; + try { + Log.continueSession((Session) args.arg3, + SESSION_HANDLER + SESSION_PLAY_DTMF); + playDtmfTone((String) args.arg2, (char) args.arg1); + } finally { + args.recycle(); + Log.endSession(); + } + break; + } + case MSG_STOP_DTMF_TONE: { + SomeArgs args = (SomeArgs) msg.obj; + try { + Log.continueSession((Session) args.arg2, + SESSION_HANDLER + SESSION_STOP_DTMF); + stopDtmfTone((String) args.arg1); + } finally { + args.recycle(); + Log.endSession(); + } + break; + } + case MSG_CONFERENCE: { + SomeArgs args = (SomeArgs) msg.obj; + try { + Log.continueSession((Session) args.arg3, + SESSION_HANDLER + SESSION_CONFERENCE); + String callId1 = (String) args.arg1; + String callId2 = (String) args.arg2; + conference(callId1, callId2); + } finally { + args.recycle(); + Log.endSession(); + } + break; + } + case MSG_SPLIT_FROM_CONFERENCE: { + SomeArgs args = (SomeArgs) msg.obj; + try { + Log.continueSession((Session) args.arg2, + SESSION_HANDLER + SESSION_SPLIT_CONFERENCE); + splitFromConference((String) args.arg1); + } finally { + args.recycle(); + Log.endSession(); + } + break; + } + case MSG_MERGE_CONFERENCE: { + SomeArgs args = (SomeArgs) msg.obj; + try { + Log.continueSession((Session) args.arg2, + SESSION_HANDLER + SESSION_MERGE_CONFERENCE); + mergeConference((String) args.arg1); + } finally { + args.recycle(); + Log.endSession(); + } + break; + } + case MSG_SWAP_CONFERENCE: { + SomeArgs args = (SomeArgs) msg.obj; + try { + Log.continueSession((Session) args.arg2, + SESSION_HANDLER + SESSION_SWAP_CONFERENCE); + swapConference((String) args.arg1); + } finally { + args.recycle(); + Log.endSession(); + } + break; + } + case MSG_ON_POST_DIAL_CONTINUE: { + SomeArgs args = (SomeArgs) msg.obj; + try { + Log.continueSession((Session) args.arg2, + SESSION_HANDLER + SESSION_POST_DIAL_CONT); + String callId = (String) args.arg1; + boolean proceed = (args.argi1 == 1); + onPostDialContinue(callId, proceed); + } finally { + args.recycle(); + Log.endSession(); + } + break; + } + case MSG_PULL_EXTERNAL_CALL: { + SomeArgs args = (SomeArgs) msg.obj; + try { + Log.continueSession((Session) args.arg2, + SESSION_HANDLER + SESSION_PULL_EXTERNAL_CALL); + pullExternalCall((String) args.arg1); + } finally { + args.recycle(); + Log.endSession(); + } + break; + } + case MSG_SEND_CALL_EVENT: { + SomeArgs args = (SomeArgs) msg.obj; + try { + Log.continueSession((Session) args.arg4, + SESSION_HANDLER + SESSION_SEND_CALL_EVENT); + String callId = (String) args.arg1; + String event = (String) args.arg2; + Bundle extras = (Bundle) args.arg3; + sendCallEvent(callId, event, extras); + } finally { + args.recycle(); + Log.endSession(); + } + break; + } + case MSG_ON_EXTRAS_CHANGED: { + SomeArgs args = (SomeArgs) msg.obj; + try { + Log.continueSession((Session) args.arg3, + SESSION_HANDLER + SESSION_EXTRAS_CHANGED); + String callId = (String) args.arg1; + Bundle extras = (Bundle) args.arg2; + handleExtrasChanged(callId, extras); + } finally { + args.recycle(); + Log.endSession(); + } + break; + } + case MSG_ON_START_RTT: { + SomeArgs args = (SomeArgs) msg.obj; + try { + Log.continueSession((Session) args.arg3, + SESSION_HANDLER + SESSION_START_RTT); + String callId = (String) args.arg1; + Connection.RttTextStream rttTextStream = + (Connection.RttTextStream) args.arg2; + startRtt(callId, rttTextStream); + } finally { + args.recycle(); + Log.endSession(); + } + break; + } + case MSG_ON_STOP_RTT: { + SomeArgs args = (SomeArgs) msg.obj; + try { + Log.continueSession((Session) args.arg2, + SESSION_HANDLER + SESSION_STOP_RTT); + String callId = (String) args.arg1; + stopRtt(callId); + } finally { + args.recycle(); + Log.endSession(); + } + break; + } + case MSG_RTT_UPGRADE_RESPONSE: { + SomeArgs args = (SomeArgs) msg.obj; + try { + Log.continueSession((Session) args.arg3, + SESSION_HANDLER + SESSION_RTT_UPGRADE_RESPONSE); + String callId = (String) args.arg1; + Connection.RttTextStream rttTextStream = + (Connection.RttTextStream) args.arg2; + handleRttUpgradeResponse(callId, rttTextStream); + } finally { + args.recycle(); + Log.endSession(); + } + break; + } + default: + break; + } + } + }; + + private final Conference.Listener mConferenceListener = new Conference.Listener() { + @Override + public void onStateChanged(Conference conference, int oldState, int newState) { + String id = mIdByConference.get(conference); + switch (newState) { + case Connection.STATE_ACTIVE: + mAdapter.setActive(id); + break; + case Connection.STATE_HOLDING: + mAdapter.setOnHold(id); + break; + case Connection.STATE_DISCONNECTED: + // handled by onDisconnected + break; + } + } + + @Override + public void onDisconnected(Conference conference, DisconnectCause disconnectCause) { + String id = mIdByConference.get(conference); + mAdapter.setDisconnected(id, disconnectCause); + } + + @Override + public void onConnectionAdded(Conference conference, Connection connection) { + } + + @Override + public void onConnectionRemoved(Conference conference, Connection connection) { + } + + @Override + public void onConferenceableConnectionsChanged( + Conference conference, List<Connection> conferenceableConnections) { + mAdapter.setConferenceableConnections( + mIdByConference.get(conference), + createConnectionIdList(conferenceableConnections)); + } + + @Override + public void onDestroyed(Conference conference) { + removeConference(conference); + } + + @Override + public void onConnectionCapabilitiesChanged( + Conference conference, + int connectionCapabilities) { + String id = mIdByConference.get(conference); + Log.d(this, "call capabilities: conference: %s", + Connection.capabilitiesToString(connectionCapabilities)); + mAdapter.setConnectionCapabilities(id, connectionCapabilities); + } + + @Override + public void onConnectionPropertiesChanged( + Conference conference, + int connectionProperties) { + String id = mIdByConference.get(conference); + Log.d(this, "call capabilities: conference: %s", + Connection.propertiesToString(connectionProperties)); + mAdapter.setConnectionProperties(id, connectionProperties); + } + + @Override + public void onVideoStateChanged(Conference c, int videoState) { + String id = mIdByConference.get(c); + Log.d(this, "onVideoStateChanged set video state %d", videoState); + mAdapter.setVideoState(id, videoState); + } + + @Override + public void onVideoProviderChanged(Conference c, Connection.VideoProvider videoProvider) { + String id = mIdByConference.get(c); + Log.d(this, "onVideoProviderChanged: Connection: %s, VideoProvider: %s", c, + videoProvider); + mAdapter.setVideoProvider(id, videoProvider); + } + + @Override + public void onStatusHintsChanged(Conference conference, StatusHints statusHints) { + String id = mIdByConference.get(conference); + if (id != null) { + mAdapter.setStatusHints(id, statusHints); + } + } + + @Override + public void onExtrasChanged(Conference c, Bundle extras) { + String id = mIdByConference.get(c); + if (id != null) { + mAdapter.putExtras(id, extras); + } + } + + @Override + public void onExtrasRemoved(Conference c, List<String> keys) { + String id = mIdByConference.get(c); + if (id != null) { + mAdapter.removeExtras(id, keys); + } + } + }; + + private final Connection.Listener mConnectionListener = new Connection.Listener() { + @Override + public void onStateChanged(Connection c, int state) { + String id = mIdByConnection.get(c); + Log.d(this, "Adapter set state %s %s", id, Connection.stateToString(state)); + switch (state) { + case Connection.STATE_ACTIVE: + mAdapter.setActive(id); + break; + case Connection.STATE_DIALING: + mAdapter.setDialing(id); + break; + case Connection.STATE_PULLING_CALL: + mAdapter.setPulling(id); + break; + case Connection.STATE_DISCONNECTED: + // Handled in onDisconnected() + break; + case Connection.STATE_HOLDING: + mAdapter.setOnHold(id); + break; + case Connection.STATE_NEW: + // Nothing to tell Telecom + break; + case Connection.STATE_RINGING: + mAdapter.setRinging(id); + break; + } + } + + @Override + public void onDisconnected(Connection c, DisconnectCause disconnectCause) { + String id = mIdByConnection.get(c); + Log.d(this, "Adapter set disconnected %s", disconnectCause); + mAdapter.setDisconnected(id, disconnectCause); + } + + @Override + public void onVideoStateChanged(Connection c, int videoState) { + String id = mIdByConnection.get(c); + Log.d(this, "Adapter set video state %d", videoState); + mAdapter.setVideoState(id, videoState); + } + + @Override + public void onAddressChanged(Connection c, Uri address, int presentation) { + String id = mIdByConnection.get(c); + mAdapter.setAddress(id, address, presentation); + } + + @Override + public void onCallerDisplayNameChanged( + Connection c, String callerDisplayName, int presentation) { + String id = mIdByConnection.get(c); + mAdapter.setCallerDisplayName(id, callerDisplayName, presentation); + } + + @Override + public void onDestroyed(Connection c) { + removeConnection(c); + } + + @Override + public void onPostDialWait(Connection c, String remaining) { + String id = mIdByConnection.get(c); + Log.d(this, "Adapter onPostDialWait %s, %s", c, remaining); + mAdapter.onPostDialWait(id, remaining); + } + + @Override + public void onPostDialChar(Connection c, char nextChar) { + String id = mIdByConnection.get(c); + Log.d(this, "Adapter onPostDialChar %s, %s", c, nextChar); + mAdapter.onPostDialChar(id, nextChar); + } + + @Override + public void onRingbackRequested(Connection c, boolean ringback) { + String id = mIdByConnection.get(c); + Log.d(this, "Adapter onRingback %b", ringback); + mAdapter.setRingbackRequested(id, ringback); + } + + @Override + public void onConnectionCapabilitiesChanged(Connection c, int capabilities) { + String id = mIdByConnection.get(c); + Log.d(this, "capabilities: parcelableconnection: %s", + Connection.capabilitiesToString(capabilities)); + mAdapter.setConnectionCapabilities(id, capabilities); + } + + @Override + public void onConnectionPropertiesChanged(Connection c, int properties) { + String id = mIdByConnection.get(c); + Log.d(this, "properties: parcelableconnection: %s", + Connection.propertiesToString(properties)); + mAdapter.setConnectionProperties(id, properties); + } + + @Override + public void onVideoProviderChanged(Connection c, Connection.VideoProvider videoProvider) { + String id = mIdByConnection.get(c); + Log.d(this, "onVideoProviderChanged: Connection: %s, VideoProvider: %s", c, + videoProvider); + mAdapter.setVideoProvider(id, videoProvider); + } + + @Override + public void onAudioModeIsVoipChanged(Connection c, boolean isVoip) { + String id = mIdByConnection.get(c); + mAdapter.setIsVoipAudioMode(id, isVoip); + } + + @Override + public void onStatusHintsChanged(Connection c, StatusHints statusHints) { + String id = mIdByConnection.get(c); + mAdapter.setStatusHints(id, statusHints); + } + + @Override + public void onConferenceablesChanged( + Connection connection, List<Conferenceable> conferenceables) { + mAdapter.setConferenceableConnections( + mIdByConnection.get(connection), + createIdList(conferenceables)); + } + + @Override + public void onConferenceChanged(Connection connection, Conference conference) { + String id = mIdByConnection.get(connection); + if (id != null) { + String conferenceId = null; + if (conference != null) { + conferenceId = mIdByConference.get(conference); + } + mAdapter.setIsConferenced(id, conferenceId); + } + } + + @Override + public void onConferenceMergeFailed(Connection connection) { + String id = mIdByConnection.get(connection); + if (id != null) { + mAdapter.onConferenceMergeFailed(id); + } + } + + @Override + public void onExtrasChanged(Connection c, Bundle extras) { + String id = mIdByConnection.get(c); + if (id != null) { + mAdapter.putExtras(id, extras); + } + } + + @Override + public void onExtrasRemoved(Connection c, List<String> keys) { + String id = mIdByConnection.get(c); + if (id != null) { + mAdapter.removeExtras(id, keys); + } + } + + @Override + public void onConnectionEvent(Connection connection, String event, Bundle extras) { + String id = mIdByConnection.get(connection); + if (id != null) { + mAdapter.onConnectionEvent(id, event, extras); + } + } + + @Override + public void onAudioRouteChanged(Connection c, int audioRoute) { + String id = mIdByConnection.get(c); + if (id != null) { + mAdapter.setAudioRoute(id, audioRoute); + } + } + + @Override + public void onRttInitiationSuccess(Connection c) { + String id = mIdByConnection.get(c); + if (id != null) { + mAdapter.onRttInitiationSuccess(id); + } + } + + @Override + public void onRttInitiationFailure(Connection c, int reason) { + String id = mIdByConnection.get(c); + if (id != null) { + mAdapter.onRttInitiationFailure(id, reason); + } + } + + @Override + public void onRttSessionRemotelyTerminated(Connection c) { + String id = mIdByConnection.get(c); + if (id != null) { + mAdapter.onRttSessionRemotelyTerminated(id); + } + } + + @Override + public void onRemoteRttRequest(Connection c) { + String id = mIdByConnection.get(c); + if (id != null) { + mAdapter.onRemoteRttRequest(id); + } + } + + @Override + public void onPhoneAccountChanged(Connection c, PhoneAccountHandle pHandle) { + String id = mIdByConnection.get(c); + if (id != null) { + mAdapter.onPhoneAccountChanged(id, pHandle); + } + } + }; + + /** {@inheritDoc} */ + @Override + public final IBinder onBind(Intent intent) { + return mBinder; + } + + /** {@inheritDoc} */ + @Override + public boolean onUnbind(Intent intent) { + endAllConnections(); + return super.onUnbind(intent); + } + + /** + * This can be used by telecom to either create a new outgoing call or attach to an existing + * incoming call. In either case, telecom will cycle through a set of services and call + * createConnection util a connection service cancels the process or completes it successfully. + */ + private void createConnection( + final PhoneAccountHandle callManagerAccount, + final String callId, + final ConnectionRequest request, + boolean isIncoming, + boolean isUnknown) { + Log.d(this, "createConnection, callManagerAccount: %s, callId: %s, request: %s, " + + "isIncoming: %b, isUnknown: %b", callManagerAccount, callId, request, + isIncoming, + isUnknown); + + Connection connection = isUnknown ? onCreateUnknownConnection(callManagerAccount, request) + : isIncoming ? onCreateIncomingConnection(callManagerAccount, request) + : onCreateOutgoingConnection(callManagerAccount, request); + Log.d(this, "createConnection, connection: %s", connection); + if (connection == null) { + connection = Connection.createFailedConnection( + new DisconnectCause(DisconnectCause.ERROR)); + } + + connection.setTelecomCallId(callId); + if (connection.getState() != Connection.STATE_DISCONNECTED) { + addConnection(callId, connection); + } + + Uri address = connection.getAddress(); + String number = address == null ? "null" : address.getSchemeSpecificPart(); + Log.v(this, "createConnection, number: %s, state: %s, capabilities: %s, properties: %s", + Connection.toLogSafePhoneNumber(number), + Connection.stateToString(connection.getState()), + Connection.capabilitiesToString(connection.getConnectionCapabilities()), + Connection.propertiesToString(connection.getConnectionProperties())); + + Log.d(this, "createConnection, calling handleCreateConnectionSuccessful %s", callId); + mAdapter.handleCreateConnectionComplete( + callId, + request, + new ParcelableConnection( + request.getAccountHandle(), + connection.getState(), + connection.getConnectionCapabilities(), + connection.getConnectionProperties(), + connection.getSupportedAudioRoutes(), + connection.getAddress(), + connection.getAddressPresentation(), + connection.getCallerDisplayName(), + connection.getCallerDisplayNamePresentation(), + connection.getVideoProvider() == null ? + null : connection.getVideoProvider().getInterface(), + connection.getVideoState(), + connection.isRingbackRequested(), + connection.getAudioModeIsVoip(), + connection.getConnectTimeMillis(), + connection.getConnectElapsedTimeMillis(), + connection.getStatusHints(), + connection.getDisconnectCause(), + createIdList(connection.getConferenceables()), + connection.getExtras())); + + if (isIncoming && request.shouldShowIncomingCallUi() && + (connection.getConnectionProperties() & Connection.PROPERTY_SELF_MANAGED) == + Connection.PROPERTY_SELF_MANAGED) { + // Tell ConnectionService to show its incoming call UX. + connection.onShowIncomingCallUi(); + } + if (isUnknown) { + triggerConferenceRecalculate(); + } + } + + private void createConnectionFailed(final PhoneAccountHandle callManagerAccount, + final String callId, final ConnectionRequest request, + boolean isIncoming) { + + Log.i(this, "createConnectionFailed %s", callId); + if (isIncoming) { + onCreateIncomingConnectionFailed(callManagerAccount, request); + } else { + onCreateOutgoingConnectionFailed(callManagerAccount, request); + } + } + + /** + * Called by Telecom when the creation of a new Connection has completed and it is now added + * to Telecom. + * @param callId The ID of the connection. + */ + private void notifyCreateConnectionComplete(final String callId) { + Log.i(this, "notifyCreateConnectionComplete %s", callId); + if (callId == null) { + // This could happen if the connection fails quickly and is removed from the + // ConnectionService before Telecom sends the create connection complete callback. + Log.w(this, "notifyCreateConnectionComplete: callId is null."); + return; + } + onCreateConnectionComplete(findConnectionForAction(callId, + "notifyCreateConnectionComplete")); + } + + private void abort(String callId) { + Log.d(this, "abort %s", callId); + findConnectionForAction(callId, "abort").onAbort(); + } + + private void answerVideo(String callId, int videoState) { + Log.d(this, "answerVideo %s", callId); + findConnectionForAction(callId, "answer").onAnswer(videoState); + } + + private void answer(String callId) { + Log.d(this, "answer %s", callId); + findConnectionForAction(callId, "answer").onAnswer(); + } + + private void reject(String callId) { + Log.d(this, "reject %s", callId); + findConnectionForAction(callId, "reject").onReject(); + } + + private void reject(String callId, String rejectWithMessage) { + Log.d(this, "reject %s with message", callId); + findConnectionForAction(callId, "reject").onReject(rejectWithMessage); + } + + private void silence(String callId) { + Log.d(this, "silence %s", callId); + findConnectionForAction(callId, "silence").onSilence(); + } + + private void disconnect(String callId) { + Log.d(this, "disconnect %s", callId); + if (mConnectionById.containsKey(callId)) { + findConnectionForAction(callId, "disconnect").onDisconnect(); + } else { + findConferenceForAction(callId, "disconnect").onDisconnect(); + } + } + + private void hold(String callId) { + Log.d(this, "hold %s", callId); + if (mConnectionById.containsKey(callId)) { + findConnectionForAction(callId, "hold").onHold(); + } else { + findConferenceForAction(callId, "hold").onHold(); + } + } + + private void unhold(String callId) { + Log.d(this, "unhold %s", callId); + if (mConnectionById.containsKey(callId)) { + findConnectionForAction(callId, "unhold").onUnhold(); + } else { + findConferenceForAction(callId, "unhold").onUnhold(); + } + } + + private void onCallAudioStateChanged(String callId, CallAudioState callAudioState) { + Log.d(this, "onAudioStateChanged %s %s", callId, callAudioState); + if (mConnectionById.containsKey(callId)) { + findConnectionForAction(callId, "onCallAudioStateChanged").setCallAudioState( + callAudioState); + } else { + findConferenceForAction(callId, "onCallAudioStateChanged").setCallAudioState( + callAudioState); + } + } + + private void playDtmfTone(String callId, char digit) { + Log.d(this, "playDtmfTone %s %c", callId, digit); + if (mConnectionById.containsKey(callId)) { + findConnectionForAction(callId, "playDtmfTone").onPlayDtmfTone(digit); + } else { + findConferenceForAction(callId, "playDtmfTone").onPlayDtmfTone(digit); + } + } + + private void stopDtmfTone(String callId) { + Log.d(this, "stopDtmfTone %s", callId); + if (mConnectionById.containsKey(callId)) { + findConnectionForAction(callId, "stopDtmfTone").onStopDtmfTone(); + } else { + findConferenceForAction(callId, "stopDtmfTone").onStopDtmfTone(); + } + } + + private void conference(String callId1, String callId2) { + Log.d(this, "conference %s, %s", callId1, callId2); + + // Attempt to get second connection or conference. + Connection connection2 = findConnectionForAction(callId2, "conference"); + Conference conference2 = getNullConference(); + if (connection2 == getNullConnection()) { + conference2 = findConferenceForAction(callId2, "conference"); + if (conference2 == getNullConference()) { + Log.w(this, "Connection2 or Conference2 missing in conference request %s.", + callId2); + return; + } + } + + // Attempt to get first connection or conference and perform merge. + Connection connection1 = findConnectionForAction(callId1, "conference"); + if (connection1 == getNullConnection()) { + Conference conference1 = findConferenceForAction(callId1, "addConnection"); + if (conference1 == getNullConference()) { + Log.w(this, + "Connection1 or Conference1 missing in conference request %s.", + callId1); + } else { + // Call 1 is a conference. + if (connection2 != getNullConnection()) { + // Call 2 is a connection so merge via call 1 (conference). + conference1.onMerge(connection2); + } else { + // Call 2 is ALSO a conference; this should never happen. + Log.wtf(this, "There can only be one conference and an attempt was made to " + + "merge two conferences."); + return; + } + } + } else { + // Call 1 is a connection. + if (conference2 != getNullConference()) { + // Call 2 is a conference, so merge via call 2. + conference2.onMerge(connection1); + } else { + // Call 2 is a connection, so merge together. + onConference(connection1, connection2); + } + } + } + + private void splitFromConference(String callId) { + Log.d(this, "splitFromConference(%s)", callId); + + Connection connection = findConnectionForAction(callId, "splitFromConference"); + if (connection == getNullConnection()) { + Log.w(this, "Connection missing in conference request %s.", callId); + return; + } + + Conference conference = connection.getConference(); + if (conference != null) { + conference.onSeparate(connection); + } + } + + private void mergeConference(String callId) { + Log.d(this, "mergeConference(%s)", callId); + Conference conference = findConferenceForAction(callId, "mergeConference"); + if (conference != null) { + conference.onMerge(); + } + } + + private void swapConference(String callId) { + Log.d(this, "swapConference(%s)", callId); + Conference conference = findConferenceForAction(callId, "swapConference"); + if (conference != null) { + conference.onSwap(); + } + } + + /** + * Notifies a {@link Connection} of a request to pull an external call. + * + * See {@link Call#pullExternalCall()}. + * + * @param callId The ID of the call to pull. + */ + private void pullExternalCall(String callId) { + Log.d(this, "pullExternalCall(%s)", callId); + Connection connection = findConnectionForAction(callId, "pullExternalCall"); + if (connection != null) { + connection.onPullExternalCall(); + } + } + + /** + * Notifies a {@link Connection} of a call event. + * + * See {@link Call#sendCallEvent(String, Bundle)}. + * + * @param callId The ID of the call receiving the event. + * @param event The event. + * @param extras Extras associated with the event. + */ + private void sendCallEvent(String callId, String event, Bundle extras) { + Log.d(this, "sendCallEvent(%s, %s)", callId, event); + Connection connection = findConnectionForAction(callId, "sendCallEvent"); + if (connection != null) { + connection.onCallEvent(event, extras); + } + } + + /** + * Notifies a {@link Connection} or {@link Conference} of a change to the extras from Telecom. + * <p> + * These extra changes can originate from Telecom itself, or from an {@link InCallService} via + * the {@link android.telecom.Call#putExtra(String, boolean)}, + * {@link android.telecom.Call#putExtra(String, int)}, + * {@link android.telecom.Call#putExtra(String, String)}, + * {@link Call#removeExtras(List)}. + * + * @param callId The ID of the call receiving the event. + * @param extras The new extras bundle. + */ + private void handleExtrasChanged(String callId, Bundle extras) { + Log.d(this, "handleExtrasChanged(%s, %s)", callId, extras); + if (mConnectionById.containsKey(callId)) { + findConnectionForAction(callId, "handleExtrasChanged").handleExtrasChanged(extras); + } else if (mConferenceById.containsKey(callId)) { + findConferenceForAction(callId, "handleExtrasChanged").handleExtrasChanged(extras); + } + } + + private void startRtt(String callId, Connection.RttTextStream rttTextStream) { + Log.d(this, "startRtt(%s)", callId); + if (mConnectionById.containsKey(callId)) { + findConnectionForAction(callId, "startRtt").onStartRtt(rttTextStream); + } else if (mConferenceById.containsKey(callId)) { + Log.w(this, "startRtt called on a conference."); + } + } + + private void stopRtt(String callId) { + Log.d(this, "stopRtt(%s)", callId); + if (mConnectionById.containsKey(callId)) { + findConnectionForAction(callId, "stopRtt").onStopRtt(); + findConnectionForAction(callId, "stopRtt").unsetRttProperty(); + } else if (mConferenceById.containsKey(callId)) { + Log.w(this, "stopRtt called on a conference."); + } + } + + private void handleRttUpgradeResponse(String callId, Connection.RttTextStream rttTextStream) { + Log.d(this, "handleRttUpgradeResponse(%s, %s)", callId, rttTextStream == null); + if (mConnectionById.containsKey(callId)) { + findConnectionForAction(callId, "handleRttUpgradeResponse") + .handleRttUpgradeResponse(rttTextStream); + } else if (mConferenceById.containsKey(callId)) { + Log.w(this, "handleRttUpgradeResponse called on a conference."); + } + } + + private void onPostDialContinue(String callId, boolean proceed) { + Log.d(this, "onPostDialContinue(%s)", callId); + findConnectionForAction(callId, "stopDtmfTone").onPostDialContinue(proceed); + } + + private void onAdapterAttached() { + if (mAreAccountsInitialized) { + // No need to query again if we already did it. + return; + } + + mAdapter.queryRemoteConnectionServices(new RemoteServiceCallback.Stub() { + @Override + public void onResult( + final List<ComponentName> componentNames, + final List<IBinder> services) { + mHandler.post(new android.telecom.Logging.Runnable("oAA.qRCS.oR", null /*lock*/) { + @Override + public void loggedRun() { + for (int i = 0; i < componentNames.size() && i < services.size(); i++) { + mRemoteConnectionManager.addConnectionService( + componentNames.get(i), + IConnectionService.Stub.asInterface(services.get(i))); + } + onAccountsInitialized(); + Log.d(this, "remote connection services found: " + services); + } + }.prepare()); + } + + @Override + public void onError() { + mHandler.post(new android.telecom.Logging.Runnable("oAA.qRCS.oE", null /*lock*/) { + @Override + public void loggedRun() { + mAreAccountsInitialized = true; + } + }.prepare()); + } + }); + } + + /** + * Ask some other {@code ConnectionService} to create a {@code RemoteConnection} given an + * incoming request. This is used by {@code ConnectionService}s that are registered with + * {@link PhoneAccount#CAPABILITY_CONNECTION_MANAGER} and want to be able to manage + * SIM-based incoming calls. + * + * @param connectionManagerPhoneAccount See description at + * {@link #onCreateOutgoingConnection(PhoneAccountHandle, ConnectionRequest)}. + * @param request Details about the incoming call. + * @return The {@code Connection} object to satisfy this call, or {@code null} to + * not handle the call. + */ + public final RemoteConnection createRemoteIncomingConnection( + PhoneAccountHandle connectionManagerPhoneAccount, + ConnectionRequest request) { + return mRemoteConnectionManager.createRemoteConnection( + connectionManagerPhoneAccount, request, true); + } + + /** + * Ask some other {@code ConnectionService} to create a {@code RemoteConnection} given an + * outgoing request. This is used by {@code ConnectionService}s that are registered with + * {@link PhoneAccount#CAPABILITY_CONNECTION_MANAGER} and want to be able to use the + * SIM-based {@code ConnectionService} to place its outgoing calls. + * + * @param connectionManagerPhoneAccount See description at + * {@link #onCreateOutgoingConnection(PhoneAccountHandle, ConnectionRequest)}. + * @param request Details about the outgoing call. + * @return The {@code Connection} object to satisfy this call, or {@code null} to + * not handle the call. + */ + public final RemoteConnection createRemoteOutgoingConnection( + PhoneAccountHandle connectionManagerPhoneAccount, + ConnectionRequest request) { + return mRemoteConnectionManager.createRemoteConnection( + connectionManagerPhoneAccount, request, false); + } + + /** + * Indicates to the relevant {@code RemoteConnectionService} that the specified + * {@link RemoteConnection}s should be merged into a conference call. + * <p> + * If the conference request is successful, the method {@link #onRemoteConferenceAdded} will + * be invoked. + * + * @param remoteConnection1 The first of the remote connections to conference. + * @param remoteConnection2 The second of the remote connections to conference. + */ + public final void conferenceRemoteConnections( + RemoteConnection remoteConnection1, + RemoteConnection remoteConnection2) { + mRemoteConnectionManager.conferenceRemoteConnections(remoteConnection1, remoteConnection2); + } + + /** + * Adds a new conference call. When a conference call is created either as a result of an + * explicit request via {@link #onConference} or otherwise, the connection service should supply + * an instance of {@link Conference} by invoking this method. A conference call provided by this + * method will persist until {@link Conference#destroy} is invoked on the conference instance. + * + * @param conference The new conference object. + */ + public final void addConference(Conference conference) { + Log.d(this, "addConference: conference=%s", conference); + + String id = addConferenceInternal(conference); + if (id != null) { + List<String> connectionIds = new ArrayList<>(2); + for (Connection connection : conference.getConnections()) { + if (mIdByConnection.containsKey(connection)) { + connectionIds.add(mIdByConnection.get(connection)); + } + } + conference.setTelecomCallId(id); + ParcelableConference parcelableConference = new ParcelableConference( + conference.getPhoneAccountHandle(), + conference.getState(), + conference.getConnectionCapabilities(), + conference.getConnectionProperties(), + connectionIds, + conference.getVideoProvider() == null ? + null : conference.getVideoProvider().getInterface(), + conference.getVideoState(), + conference.getConnectTimeMillis(), + conference.getConnectElapsedTime(), + conference.getStatusHints(), + conference.getExtras()); + + mAdapter.addConferenceCall(id, parcelableConference); + mAdapter.setVideoProvider(id, conference.getVideoProvider()); + mAdapter.setVideoState(id, conference.getVideoState()); + + // Go through any child calls and set the parent. + for (Connection connection : conference.getConnections()) { + String connectionId = mIdByConnection.get(connection); + if (connectionId != null) { + mAdapter.setIsConferenced(connectionId, id); + } + } + } + } + + /** + * Adds a connection created by the {@link ConnectionService} and informs telecom of the new + * connection. + * + * @param phoneAccountHandle The phone account handle for the connection. + * @param connection The connection to add. + */ + public final void addExistingConnection(PhoneAccountHandle phoneAccountHandle, + Connection connection) { + addExistingConnection(phoneAccountHandle, connection, null /* conference */); + } + + /** + * Adds a connection created by the {@link ConnectionService} and informs telecom of the new + * connection. + * + * @param phoneAccountHandle The phone account handle for the connection. + * @param connection The connection to add. + * @param conference The parent conference of the new connection. + * @hide + */ + public final void addExistingConnection(PhoneAccountHandle phoneAccountHandle, + Connection connection, Conference conference) { + + String id = addExistingConnectionInternal(phoneAccountHandle, connection); + if (id != null) { + List<String> emptyList = new ArrayList<>(0); + String conferenceId = null; + if (conference != null) { + conferenceId = mIdByConference.get(conference); + } + + ParcelableConnection parcelableConnection = new ParcelableConnection( + phoneAccountHandle, + connection.getState(), + connection.getConnectionCapabilities(), + connection.getConnectionProperties(), + connection.getSupportedAudioRoutes(), + connection.getAddress(), + connection.getAddressPresentation(), + connection.getCallerDisplayName(), + connection.getCallerDisplayNamePresentation(), + connection.getVideoProvider() == null ? + null : connection.getVideoProvider().getInterface(), + connection.getVideoState(), + connection.isRingbackRequested(), + connection.getAudioModeIsVoip(), + connection.getConnectTimeMillis(), + connection.getConnectElapsedTimeMillis(), + connection.getStatusHints(), + connection.getDisconnectCause(), + emptyList, + connection.getExtras(), + conferenceId); + mAdapter.addExistingConnection(id, parcelableConnection); + } + } + + /** + * Returns all the active {@code Connection}s for which this {@code ConnectionService} + * has taken responsibility. + * + * @return A collection of {@code Connection}s created by this {@code ConnectionService}. + */ + public final Collection<Connection> getAllConnections() { + return mConnectionById.values(); + } + + /** + * Returns all the active {@code Conference}s for which this {@code ConnectionService} + * has taken responsibility. + * + * @return A collection of {@code Conference}s created by this {@code ConnectionService}. + */ + public final Collection<Conference> getAllConferences() { + return mConferenceById.values(); + } + + /** + * Create a {@code Connection} given an incoming request. This is used to attach to existing + * incoming calls. + * + * @param connectionManagerPhoneAccount See description at + * {@link #onCreateOutgoingConnection(PhoneAccountHandle, ConnectionRequest)}. + * @param request Details about the incoming call. + * @return The {@code Connection} object to satisfy this call, or {@code null} to + * not handle the call. + */ + public Connection onCreateIncomingConnection( + PhoneAccountHandle connectionManagerPhoneAccount, + ConnectionRequest request) { + return null; + } + + /** + * Called after the {@link Connection} returned by + * {@link #onCreateIncomingConnection(PhoneAccountHandle, ConnectionRequest)} + * or {@link #onCreateOutgoingConnection(PhoneAccountHandle, ConnectionRequest)} has been + * added to the {@link ConnectionService} and sent to Telecom. + * + * @param connection the {@link Connection}. + * @hide + */ + public void onCreateConnectionComplete(Connection connection) { + } + + /** + * Called by Telecom to inform the {@link ConnectionService} that its request to create a new + * incoming {@link Connection} was denied. + * <p> + * Used when a self-managed {@link ConnectionService} attempts to create a new incoming + * {@link Connection}, but Telecom has determined that the call cannot be allowed at this time. + * The {@link ConnectionService} is responsible for silently rejecting the new incoming + * {@link Connection}. + * <p> + * See {@link TelecomManager#isIncomingCallPermitted(PhoneAccountHandle)} for more information. + * + * @param connectionManagerPhoneAccount See description at + * {@link #onCreateOutgoingConnection(PhoneAccountHandle, ConnectionRequest)}. + * @param request The incoming connection request. + */ + public void onCreateIncomingConnectionFailed(PhoneAccountHandle connectionManagerPhoneAccount, + ConnectionRequest request) { + } + + /** + * Called by Telecom to inform the {@link ConnectionService} that its request to create a new + * outgoing {@link Connection} was denied. + * <p> + * Used when a self-managed {@link ConnectionService} attempts to create a new outgoing + * {@link Connection}, but Telecom has determined that the call cannot be placed at this time. + * The {@link ConnectionService} is responisible for informing the user that the + * {@link Connection} cannot be made at this time. + * <p> + * See {@link TelecomManager#isOutgoingCallPermitted(PhoneAccountHandle)} for more information. + * + * @param connectionManagerPhoneAccount See description at + * {@link #onCreateOutgoingConnection(PhoneAccountHandle, ConnectionRequest)}. + * @param request The outgoing connection request. + */ + public void onCreateOutgoingConnectionFailed(PhoneAccountHandle connectionManagerPhoneAccount, + ConnectionRequest request) { + } + + /** + * Trigger recalculate functinality for conference calls. This is used when a Telephony + * Connection is part of a conference controller but is not yet added to Connection + * Service and hence cannot be added to the conference call. + * + * @hide + */ + public void triggerConferenceRecalculate() { + } + + /** + * Create a {@code Connection} given an outgoing request. This is used to initiate new + * outgoing calls. + * + * @param connectionManagerPhoneAccount The connection manager account to use for managing + * this call. + * <p> + * If this parameter is not {@code null}, it means that this {@code ConnectionService} + * has registered one or more {@code PhoneAccount}s having + * {@link PhoneAccount#CAPABILITY_CONNECTION_MANAGER}. This parameter will contain + * one of these {@code PhoneAccount}s, while the {@code request} will contain another + * (usually but not always distinct) {@code PhoneAccount} to be used for actually + * making the connection. + * <p> + * If this parameter is {@code null}, it means that this {@code ConnectionService} is + * being asked to make a direct connection. The + * {@link ConnectionRequest#getAccountHandle()} of parameter {@code request} will be + * a {@code PhoneAccount} registered by this {@code ConnectionService} to use for + * making the connection. + * @param request Details about the outgoing call. + * @return The {@code Connection} object to satisfy this call, or the result of an invocation + * of {@link Connection#createFailedConnection(DisconnectCause)} to not handle the call. + */ + public Connection onCreateOutgoingConnection( + PhoneAccountHandle connectionManagerPhoneAccount, + ConnectionRequest request) { + return null; + } + + /** + * Create a {@code Connection} for a new unknown call. An unknown call is a call originating + * from the ConnectionService that was neither a user-initiated outgoing call, nor an incoming + * call created using + * {@code TelecomManager#addNewIncomingCall(PhoneAccountHandle, android.os.Bundle)}. + * + * @hide + */ + public Connection onCreateUnknownConnection(PhoneAccountHandle connectionManagerPhoneAccount, + ConnectionRequest request) { + return null; + } + + /** + * Conference two specified connections. Invoked when the user has made a request to merge the + * specified connections into a conference call. In response, the connection service should + * create an instance of {@link Conference} and pass it into {@link #addConference}. + * + * @param connection1 A connection to merge into a conference call. + * @param connection2 A connection to merge into a conference call. + */ + public void onConference(Connection connection1, Connection connection2) {} + + /** + * Indicates that a remote conference has been created for existing {@link RemoteConnection}s. + * When this method is invoked, this {@link ConnectionService} should create its own + * representation of the conference call and send it to telecom using {@link #addConference}. + * <p> + * This is only relevant to {@link ConnectionService}s which are registered with + * {@link PhoneAccount#CAPABILITY_CONNECTION_MANAGER}. + * + * @param conference The remote conference call. + */ + public void onRemoteConferenceAdded(RemoteConference conference) {} + + /** + * Called when an existing connection is added remotely. + * @param connection The existing connection which was added. + */ + public void onRemoteExistingConnectionAdded(RemoteConnection connection) {} + + /** + * @hide + */ + public boolean containsConference(Conference conference) { + return mIdByConference.containsKey(conference); + } + + /** {@hide} */ + void addRemoteConference(RemoteConference remoteConference) { + onRemoteConferenceAdded(remoteConference); + } + + /** {@hide} */ + void addRemoteExistingConnection(RemoteConnection remoteConnection) { + onRemoteExistingConnectionAdded(remoteConnection); + } + + private void onAccountsInitialized() { + mAreAccountsInitialized = true; + for (Runnable r : mPreInitializationConnectionRequests) { + r.run(); + } + mPreInitializationConnectionRequests.clear(); + } + + /** + * Adds an existing connection to the list of connections, identified by a new call ID unique + * to this connection service. + * + * @param connection The connection. + * @return The ID of the connection (e.g. the call-id). + */ + private String addExistingConnectionInternal(PhoneAccountHandle handle, Connection connection) { + String id; + + if (connection.getExtras() != null && connection.getExtras() + .containsKey(Connection.EXTRA_ORIGINAL_CONNECTION_ID)) { + id = connection.getExtras().getString(Connection.EXTRA_ORIGINAL_CONNECTION_ID); + Log.d(this, "addExistingConnectionInternal - conn %s reusing original id %s", + connection.getTelecomCallId(), id); + } else if (handle == null) { + // If no phone account handle was provided, we cannot be sure the call ID is unique, + // so just use a random UUID. + id = UUID.randomUUID().toString(); + } else { + // Phone account handle was provided, so use the ConnectionService class name as a + // prefix for a unique incremental call ID. + id = handle.getComponentName().getClassName() + "@" + getNextCallId(); + } + addConnection(id, connection); + return id; + } + + private void addConnection(String callId, Connection connection) { + connection.setTelecomCallId(callId); + mConnectionById.put(callId, connection); + mIdByConnection.put(connection, callId); + connection.addConnectionListener(mConnectionListener); + connection.setConnectionService(this); + } + + /** {@hide} */ + protected void removeConnection(Connection connection) { + connection.unsetConnectionService(this); + connection.removeConnectionListener(mConnectionListener); + String id = mIdByConnection.get(connection); + if (id != null) { + mConnectionById.remove(id); + mIdByConnection.remove(connection); + mAdapter.removeCall(id); + } + } + + private String addConferenceInternal(Conference conference) { + String originalId = null; + if (conference.getExtras() != null && conference.getExtras() + .containsKey(Connection.EXTRA_ORIGINAL_CONNECTION_ID)) { + originalId = conference.getExtras().getString(Connection.EXTRA_ORIGINAL_CONNECTION_ID); + Log.d(this, "addConferenceInternal: conf %s reusing original id %s", + conference.getTelecomCallId(), + originalId); + } + if (mIdByConference.containsKey(conference)) { + Log.w(this, "Re-adding an existing conference: %s.", conference); + } else if (conference != null) { + // Conferences do not (yet) have a PhoneAccountHandle associated with them, so we + // cannot determine a ConnectionService class name to associate with the ID, so use + // a unique UUID (for now). + String id = originalId == null ? UUID.randomUUID().toString() : originalId; + mConferenceById.put(id, conference); + mIdByConference.put(conference, id); + conference.addListener(mConferenceListener); + return id; + } + + return null; + } + + private void removeConference(Conference conference) { + if (mIdByConference.containsKey(conference)) { + conference.removeListener(mConferenceListener); + + String id = mIdByConference.get(conference); + mConferenceById.remove(id); + mIdByConference.remove(conference); + mAdapter.removeCall(id); + } + } + + private Connection findConnectionForAction(String callId, String action) { + if (callId != null && mConnectionById.containsKey(callId)) { + return mConnectionById.get(callId); + } + Log.w(this, "%s - Cannot find Connection %s", action, callId); + return getNullConnection(); + } + + static synchronized Connection getNullConnection() { + if (sNullConnection == null) { + sNullConnection = new Connection() {}; + } + return sNullConnection; + } + + private Conference findConferenceForAction(String conferenceId, String action) { + if (mConferenceById.containsKey(conferenceId)) { + return mConferenceById.get(conferenceId); + } + Log.w(this, "%s - Cannot find conference %s", action, conferenceId); + return getNullConference(); + } + + private List<String> createConnectionIdList(List<Connection> connections) { + List<String> ids = new ArrayList<>(); + for (Connection c : connections) { + if (mIdByConnection.containsKey(c)) { + ids.add(mIdByConnection.get(c)); + } + } + Collections.sort(ids); + return ids; + } + + /** + * Builds a list of {@link Connection} and {@link Conference} IDs based on the list of + * {@link Conferenceable}s passed in. + * + * @param conferenceables The {@link Conferenceable} connections and conferences. + * @return List of string conference and call Ids. + */ + private List<String> createIdList(List<Conferenceable> conferenceables) { + List<String> ids = new ArrayList<>(); + for (Conferenceable c : conferenceables) { + // Only allow Connection and Conference conferenceables. + if (c instanceof Connection) { + Connection connection = (Connection) c; + if (mIdByConnection.containsKey(connection)) { + ids.add(mIdByConnection.get(connection)); + } + } else if (c instanceof Conference) { + Conference conference = (Conference) c; + if (mIdByConference.containsKey(conference)) { + ids.add(mIdByConference.get(conference)); + } + } + } + Collections.sort(ids); + return ids; + } + + private Conference getNullConference() { + if (sNullConference == null) { + sNullConference = new Conference(null) {}; + } + return sNullConference; + } + + private void endAllConnections() { + // Unbound from telecomm. We should end all connections and conferences. + for (Connection connection : mIdByConnection.keySet()) { + // only operate on top-level calls. Conference calls will be removed on their own. + if (connection.getConference() == null) { + connection.onDisconnect(); + } + } + for (Conference conference : mIdByConference.keySet()) { + conference.onDisconnect(); + } + } + + /** + * Retrieves the next call ID as maintainted by the connection service. + * + * @return The call ID. + */ + private int getNextCallId() { + synchronized (mIdSyncRoot) { + return ++mId; + } + } +} diff --git a/android/telecom/ConnectionServiceAdapter.java b/android/telecom/ConnectionServiceAdapter.java new file mode 100644 index 00000000..111fcc78 --- /dev/null +++ b/android/telecom/ConnectionServiceAdapter.java @@ -0,0 +1,628 @@ +/* + * Copyright (C) 2014 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.net.Uri; +import android.os.Bundle; +import android.os.IBinder.DeathRecipient; +import android.os.RemoteException; + +import com.android.internal.telecom.IConnectionServiceAdapter; +import com.android.internal.telecom.RemoteServiceCallback; + +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Provides methods for IConnectionService implementations to interact with the system phone app. + * + * @hide + */ +final class ConnectionServiceAdapter implements DeathRecipient { + /** + * ConcurrentHashMap constructor params: 8 is initial table size, 0.9f is + * load factor before resizing, 1 means we only expect a single thread to + * access the map so make only a single shard + */ + private final Set<IConnectionServiceAdapter> mAdapters = Collections.newSetFromMap( + new ConcurrentHashMap<IConnectionServiceAdapter, Boolean>(8, 0.9f, 1)); + + ConnectionServiceAdapter() { + } + + void addAdapter(IConnectionServiceAdapter adapter) { + for (IConnectionServiceAdapter it : mAdapters) { + if (it.asBinder() == adapter.asBinder()) { + Log.w(this, "Ignoring duplicate adapter addition."); + return; + } + } + if (mAdapters.add(adapter)) { + try { + adapter.asBinder().linkToDeath(this, 0); + } catch (RemoteException e) { + mAdapters.remove(adapter); + } + } + } + + void removeAdapter(IConnectionServiceAdapter adapter) { + if (adapter != null) { + for (IConnectionServiceAdapter it : mAdapters) { + if (it.asBinder() == adapter.asBinder() && mAdapters.remove(it)) { + adapter.asBinder().unlinkToDeath(this, 0); + break; + } + } + } + } + + /** ${inheritDoc} */ + @Override + public void binderDied() { + Iterator<IConnectionServiceAdapter> it = mAdapters.iterator(); + while (it.hasNext()) { + IConnectionServiceAdapter adapter = it.next(); + if (!adapter.asBinder().isBinderAlive()) { + it.remove(); + adapter.asBinder().unlinkToDeath(this, 0); + } + } + } + + void handleCreateConnectionComplete( + String id, + ConnectionRequest request, + ParcelableConnection connection) { + for (IConnectionServiceAdapter adapter : mAdapters) { + try { + adapter.handleCreateConnectionComplete(id, request, connection, + Log.getExternalSession()); + } catch (RemoteException e) { + } + } + } + + /** + * Sets a call's state to active (e.g., an ongoing call where two parties can actively + * communicate). + * + * @param callId The unique ID of the call whose state is changing to active. + */ + void setActive(String callId) { + for (IConnectionServiceAdapter adapter : mAdapters) { + try { + adapter.setActive(callId, Log.getExternalSession()); + } catch (RemoteException e) { + } + } + } + + /** + * Sets a call's state to ringing (e.g., an inbound ringing call). + * + * @param callId The unique ID of the call whose state is changing to ringing. + */ + void setRinging(String callId) { + for (IConnectionServiceAdapter adapter : mAdapters) { + try { + adapter.setRinging(callId, Log.getExternalSession()); + } catch (RemoteException e) { + } + } + } + + /** + * Sets a call's state to dialing (e.g., dialing an outbound call). + * + * @param callId The unique ID of the call whose state is changing to dialing. + */ + void setDialing(String callId) { + for (IConnectionServiceAdapter adapter : mAdapters) { + try { + adapter.setDialing(callId, Log.getExternalSession()); + } catch (RemoteException e) { + } + } + } + + /** + * Sets a call's state to pulling (e.g. a call with {@link Connection#PROPERTY_IS_EXTERNAL_CALL} + * is being pulled to the local device. + * + * @param callId The unique ID of the call whose state is changing to dialing. + */ + void setPulling(String callId) { + for (IConnectionServiceAdapter adapter : mAdapters) { + try { + adapter.setPulling(callId, Log.getExternalSession()); + } catch (RemoteException e) { + } + } + } + + /** + * Sets a call's state to disconnected. + * + * @param callId The unique ID of the call whose state is changing to disconnected. + * @param disconnectCause The reason for the disconnection, as described by + * {@link android.telecomm.DisconnectCause}. + */ + void setDisconnected(String callId, DisconnectCause disconnectCause) { + for (IConnectionServiceAdapter adapter : mAdapters) { + try { + adapter.setDisconnected(callId, disconnectCause, Log.getExternalSession()); + } catch (RemoteException e) { + } + } + } + + /** + * Sets a call's state to be on hold. + * + * @param callId - The unique ID of the call whose state is changing to be on hold. + */ + void setOnHold(String callId) { + for (IConnectionServiceAdapter adapter : mAdapters) { + try { + adapter.setOnHold(callId, Log.getExternalSession()); + } catch (RemoteException e) { + } + } + } + + /** + * Asks Telecom to start or stop a ringback tone for a call. + * + * @param callId The unique ID of the call whose ringback is being changed. + * @param ringback Whether Telecom should start playing a ringback tone. + */ + void setRingbackRequested(String callId, boolean ringback) { + for (IConnectionServiceAdapter adapter : mAdapters) { + try { + adapter.setRingbackRequested(callId, ringback, Log.getExternalSession()); + } catch (RemoteException e) { + } + } + } + + void setConnectionCapabilities(String callId, int capabilities) { + for (IConnectionServiceAdapter adapter : mAdapters) { + try { + adapter.setConnectionCapabilities(callId, capabilities, Log.getExternalSession()); + } catch (RemoteException ignored) { + } + } + } + + void setConnectionProperties(String callId, int properties) { + for (IConnectionServiceAdapter adapter : mAdapters) { + try { + adapter.setConnectionProperties(callId, properties, Log.getExternalSession()); + } catch (RemoteException ignored) { + } + } + } + + /** + * Indicates whether or not the specified call is currently conferenced into the specified + * conference call. + * + * @param callId The unique ID of the call being conferenced. + * @param conferenceCallId The unique ID of the conference call. Null if call is not + * conferenced. + */ + void setIsConferenced(String callId, String conferenceCallId) { + for (IConnectionServiceAdapter adapter : mAdapters) { + try { + Log.d(this, "sending connection %s with conference %s", callId, conferenceCallId); + adapter.setIsConferenced(callId, conferenceCallId, Log.getExternalSession()); + } catch (RemoteException ignored) { + } + } + } + + /** + * Indicates that the merge request on this call has failed. + * + * @param callId The unique ID of the call being conferenced. + */ + void onConferenceMergeFailed(String callId) { + for (IConnectionServiceAdapter adapter : mAdapters) { + try { + Log.d(this, "merge failed for call %s", callId); + adapter.setConferenceMergeFailed(callId, Log.getExternalSession()); + } catch (RemoteException ignored) { + } + } + } + + /** + * Indicates that the call no longer exists. Can be used with either a call or a conference + * call. + * + * @param callId The unique ID of the call. + */ + void removeCall(String callId) { + for (IConnectionServiceAdapter adapter : mAdapters) { + try { + adapter.removeCall(callId, Log.getExternalSession()); + } catch (RemoteException ignored) { + } + } + } + + void onPostDialWait(String callId, String remaining) { + for (IConnectionServiceAdapter adapter : mAdapters) { + try { + adapter.onPostDialWait(callId, remaining, Log.getExternalSession()); + } catch (RemoteException ignored) { + } + } + } + + void onPostDialChar(String callId, char nextChar) { + for (IConnectionServiceAdapter adapter : mAdapters) { + try { + adapter.onPostDialChar(callId, nextChar, Log.getExternalSession()); + } catch (RemoteException ignored) { + } + } + } + + /** + * Indicates that a new conference call has been created. + * + * @param callId The unique ID of the conference call. + */ + void addConferenceCall(String callId, ParcelableConference parcelableConference) { + for (IConnectionServiceAdapter adapter : mAdapters) { + try { + adapter.addConferenceCall(callId, parcelableConference, Log.getExternalSession()); + } catch (RemoteException ignored) { + } + } + } + + /** + * Retrieves a list of remote connection services usable to place calls. + */ + void queryRemoteConnectionServices(RemoteServiceCallback callback) { + // Only supported when there is only one adapter. + if (mAdapters.size() == 1) { + try { + mAdapters.iterator().next().queryRemoteConnectionServices(callback, + Log.getExternalSession()); + } catch (RemoteException e) { + Log.e(this, e, "Exception trying to query for remote CSs"); + } + } + } + + /** + * Sets the call video provider for a call. + * + * @param callId The unique ID of the call to set with the given call video provider. + * @param videoProvider The call video provider instance to set on the call. + */ + void setVideoProvider( + String callId, Connection.VideoProvider videoProvider) { + for (IConnectionServiceAdapter adapter : mAdapters) { + try { + adapter.setVideoProvider( + callId, + videoProvider == null ? null : videoProvider.getInterface(), + Log.getExternalSession()); + } catch (RemoteException e) { + } + } + } + + /** + * Requests that the framework use VOIP audio mode for this connection. + * + * @param callId The unique ID of the call to set with the given call video provider. + * @param isVoip True if the audio mode is VOIP. + */ + void setIsVoipAudioMode(String callId, boolean isVoip) { + for (IConnectionServiceAdapter adapter : mAdapters) { + try { + adapter.setIsVoipAudioMode(callId, isVoip, Log.getExternalSession()); + } catch (RemoteException e) { + } + } + } + + void setStatusHints(String callId, StatusHints statusHints) { + for (IConnectionServiceAdapter adapter : mAdapters) { + try { + adapter.setStatusHints(callId, statusHints, Log.getExternalSession()); + } catch (RemoteException e) { + } + } + } + + void setAddress(String callId, Uri address, int presentation) { + for (IConnectionServiceAdapter adapter : mAdapters) { + try { + adapter.setAddress(callId, address, presentation, Log.getExternalSession()); + } catch (RemoteException e) { + } + } + } + + void setCallerDisplayName(String callId, String callerDisplayName, int presentation) { + for (IConnectionServiceAdapter adapter : mAdapters) { + try { + adapter.setCallerDisplayName(callId, callerDisplayName, presentation, + Log.getExternalSession()); + } catch (RemoteException e) { + } + } + } + + /** + * Sets the video state associated with a call. + * + * Valid values: {@link VideoProfile#STATE_BIDIRECTIONAL}, + * {@link VideoProfile#STATE_AUDIO_ONLY}, + * {@link VideoProfile#STATE_TX_ENABLED}, + * {@link VideoProfile#STATE_RX_ENABLED}. + * + * @param callId The unique ID of the call to set the video state for. + * @param videoState The video state. + */ + void setVideoState(String callId, int videoState) { + Log.v(this, "setVideoState: %d", videoState); + for (IConnectionServiceAdapter adapter : mAdapters) { + try { + adapter.setVideoState(callId, videoState, Log.getExternalSession()); + } catch (RemoteException ignored) { + } + } + } + + void setConferenceableConnections(String callId, List<String> conferenceableCallIds) { + Log.v(this, "setConferenceableConnections: %s, %s", callId, conferenceableCallIds); + for (IConnectionServiceAdapter adapter : mAdapters) { + try { + adapter.setConferenceableConnections(callId, conferenceableCallIds, + Log.getExternalSession()); + } catch (RemoteException ignored) { + } + } + } + + /** + * Informs telecom of an existing connection which was added by the {@link ConnectionService}. + * + * @param callId The unique ID of the call being added. + * @param connection The connection. + */ + void addExistingConnection(String callId, ParcelableConnection connection) { + Log.v(this, "addExistingConnection: %s", callId); + for (IConnectionServiceAdapter adapter : mAdapters) { + try { + adapter.addExistingConnection(callId, connection, Log.getExternalSession()); + } catch (RemoteException ignored) { + } + } + } + + /** + * Adds some extras associated with a {@code Connection}. + * + * @param callId The unique ID of the call. + * @param extras The extras to add. + */ + void putExtras(String callId, Bundle extras) { + Log.v(this, "putExtras: %s", callId); + for (IConnectionServiceAdapter adapter : mAdapters) { + try { + adapter.putExtras(callId, extras, Log.getExternalSession()); + } catch (RemoteException ignored) { + } + } + } + + /** + * Adds an extra associated with a {@code Connection}. + * + * @param callId The unique ID of the call. + * @param key The extra key. + * @param value The extra value. + */ + void putExtra(String callId, String key, boolean value) { + Log.v(this, "putExtra: %s %s=%b", callId, key, value); + for (IConnectionServiceAdapter adapter : mAdapters) { + try { + Bundle bundle = new Bundle(); + bundle.putBoolean(key, value); + adapter.putExtras(callId, bundle, Log.getExternalSession()); + } catch (RemoteException ignored) { + } + } + } + + /** + * Adds an extra associated with a {@code Connection}. + * + * @param callId The unique ID of the call. + * @param key The extra key. + * @param value The extra value. + */ + void putExtra(String callId, String key, int value) { + Log.v(this, "putExtra: %s %s=%d", callId, key, value); + for (IConnectionServiceAdapter adapter : mAdapters) { + try { + Bundle bundle = new Bundle(); + bundle.putInt(key, value); + adapter.putExtras(callId, bundle, Log.getExternalSession()); + } catch (RemoteException ignored) { + } + } + } + + /** + * Adds an extra associated with a {@code Connection}. + * + * @param callId The unique ID of the call. + * @param key The extra key. + * @param value The extra value. + */ + void putExtra(String callId, String key, String value) { + Log.v(this, "putExtra: %s %s=%s", callId, key, value); + for (IConnectionServiceAdapter adapter : mAdapters) { + try { + Bundle bundle = new Bundle(); + bundle.putString(key, value); + adapter.putExtras(callId, bundle, Log.getExternalSession()); + } catch (RemoteException ignored) { + } + } + } + + /** + * Removes extras associated with a {@code Connection}. + * @param callId The unique ID of the call. + * @param keys The extra keys to remove. + */ + void removeExtras(String callId, List<String> keys) { + Log.v(this, "removeExtras: %s %s", callId, keys); + for (IConnectionServiceAdapter adapter : mAdapters) { + try { + adapter.removeExtras(callId, keys, Log.getExternalSession()); + } catch (RemoteException ignored) { + } + } + } + + /** + * Sets the audio route associated with a {@link Connection}. + * + * @param callId The unique ID of the call. + * @param audioRoute The new audio route (see {@code CallAudioState#ROUTE_*}). + */ + void setAudioRoute(String callId, int audioRoute) { + Log.v(this, "setAudioRoute: %s %s", callId, CallAudioState.audioRouteToString(audioRoute)); + for (IConnectionServiceAdapter adapter : mAdapters) { + try { + adapter.setAudioRoute(callId, audioRoute, Log.getExternalSession()); + } catch (RemoteException ignored) { + } + } + } + + + /** + * Informs Telecom of a connection level event. + * + * @param callId The unique ID of the call. + * @param event The event. + * @param extras Extras associated with the event. + */ + void onConnectionEvent(String callId, String event, Bundle extras) { + Log.v(this, "onConnectionEvent: %s", event); + for (IConnectionServiceAdapter adapter : mAdapters) { + try { + adapter.onConnectionEvent(callId, event, extras, Log.getExternalSession()); + } catch (RemoteException ignored) { + } + } + } + + /** + * Notifies Telecom that an RTT session was successfully established. + * + * @param callId The unique ID of the call. + */ + void onRttInitiationSuccess(String callId) { + Log.v(this, "onRttInitiationSuccess: %s", callId); + for (IConnectionServiceAdapter adapter : mAdapters) { + try { + adapter.onRttInitiationSuccess(callId, Log.getExternalSession()); + } catch (RemoteException ignored) { + } + } + } + + /** + * Notifies Telecom that a requested RTT session failed to be established. + * + * @param callId The unique ID of the call. + */ + void onRttInitiationFailure(String callId, int reason) { + Log.v(this, "onRttInitiationFailure: %s", callId); + for (IConnectionServiceAdapter adapter : mAdapters) { + try { + adapter.onRttInitiationFailure(callId, reason, Log.getExternalSession()); + } catch (RemoteException ignored) { + } + } + } + + /** + * Notifies Telecom that an established RTT session was terminated by the remote user on + * the call. + * + * @param callId The unique ID of the call. + */ + void onRttSessionRemotelyTerminated(String callId) { + Log.v(this, "onRttSessionRemotelyTerminated: %s", callId); + for (IConnectionServiceAdapter adapter : mAdapters) { + try { + adapter.onRttSessionRemotelyTerminated(callId, Log.getExternalSession()); + } catch (RemoteException ignored) { + } + } + } + + /** + * Notifies Telecom that the remote user on the call has requested an upgrade to an RTT + * session for this call. + * + * @param callId The unique ID of the call. + */ + void onRemoteRttRequest(String callId) { + Log.v(this, "onRemoteRttRequest: %s", callId); + for (IConnectionServiceAdapter adapter : mAdapters) { + try { + adapter.onRemoteRttRequest(callId, Log.getExternalSession()); + } catch (RemoteException ignored) { + } + } + } + + /** + * Notifies Telecom that a call's PhoneAccountHandle has changed. + * + * @param callId The unique ID of the call. + * @param pHandle The new PhoneAccountHandle associated with the call. + */ + void onPhoneAccountChanged(String callId, PhoneAccountHandle pHandle) { + for (IConnectionServiceAdapter adapter : mAdapters) { + try { + Log.d(this, "onPhoneAccountChanged %s", callId); + adapter.onPhoneAccountChanged(callId, pHandle, Log.getExternalSession()); + } catch (RemoteException ignored) { + } + } + } +} diff --git a/android/telecom/ConnectionServiceAdapterServant.java b/android/telecom/ConnectionServiceAdapterServant.java new file mode 100644 index 00000000..b1617f4d --- /dev/null +++ b/android/telecom/ConnectionServiceAdapterServant.java @@ -0,0 +1,613 @@ +/* + * Copyright (C) 2014 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 + R* limitations under the License. + */ + +package android.telecom; + +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.os.RemoteException; +import android.telecom.Logging.Session; + +import com.android.internal.os.SomeArgs; +import com.android.internal.telecom.IConnectionServiceAdapter; +import com.android.internal.telecom.IVideoProvider; +import com.android.internal.telecom.RemoteServiceCallback; + +import java.util.List; + +/** + * A component that provides an RPC servant implementation of {@link IConnectionServiceAdapter}, + * posting incoming messages on the main thread on a client-supplied delegate object. + * + * TODO: Generate this and similar classes using a compiler starting from AIDL interfaces. + * + * @hide + */ +final class ConnectionServiceAdapterServant { + private static final int MSG_HANDLE_CREATE_CONNECTION_COMPLETE = 1; + private static final int MSG_SET_ACTIVE = 2; + private static final int MSG_SET_RINGING = 3; + private static final int MSG_SET_DIALING = 4; + private static final int MSG_SET_DISCONNECTED = 5; + private static final int MSG_SET_ON_HOLD = 6; + private static final int MSG_SET_RINGBACK_REQUESTED = 7; + private static final int MSG_SET_CONNECTION_CAPABILITIES = 8; + private static final int MSG_SET_IS_CONFERENCED = 9; + private static final int MSG_ADD_CONFERENCE_CALL = 10; + private static final int MSG_REMOVE_CALL = 11; + private static final int MSG_ON_POST_DIAL_WAIT = 12; + private static final int MSG_QUERY_REMOTE_CALL_SERVICES = 13; + private static final int MSG_SET_VIDEO_STATE = 14; + private static final int MSG_SET_VIDEO_CALL_PROVIDER = 15; + private static final int MSG_SET_IS_VOIP_AUDIO_MODE = 16; + private static final int MSG_SET_STATUS_HINTS = 17; + private static final int MSG_SET_ADDRESS = 18; + private static final int MSG_SET_CALLER_DISPLAY_NAME = 19; + private static final int MSG_SET_CONFERENCEABLE_CONNECTIONS = 20; + private static final int MSG_ADD_EXISTING_CONNECTION = 21; + private static final int MSG_ON_POST_DIAL_CHAR = 22; + private static final int MSG_SET_CONFERENCE_MERGE_FAILED = 23; + private static final int MSG_PUT_EXTRAS = 24; + private static final int MSG_REMOVE_EXTRAS = 25; + private static final int MSG_ON_CONNECTION_EVENT = 26; + private static final int MSG_SET_CONNECTION_PROPERTIES = 27; + private static final int MSG_SET_PULLING = 28; + private static final int MSG_SET_AUDIO_ROUTE = 29; + private static final int MSG_ON_RTT_INITIATION_SUCCESS = 30; + private static final int MSG_ON_RTT_INITIATION_FAILURE = 31; + private static final int MSG_ON_RTT_REMOTELY_TERMINATED = 32; + private static final int MSG_ON_RTT_UPGRADE_REQUEST = 33; + private static final int MSG_SET_PHONE_ACCOUNT_CHANGED = 34; + + private final IConnectionServiceAdapter mDelegate; + + private final Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + try { + internalHandleMessage(msg); + } catch (RemoteException e) { + } + } + + // Internal method defined to centralize handling of RemoteException + private void internalHandleMessage(Message msg) throws RemoteException { + switch (msg.what) { + case MSG_HANDLE_CREATE_CONNECTION_COMPLETE: { + SomeArgs args = (SomeArgs) msg.obj; + try { + mDelegate.handleCreateConnectionComplete( + (String) args.arg1, + (ConnectionRequest) args.arg2, + (ParcelableConnection) args.arg3, + null /*Session.Info*/); + } finally { + args.recycle(); + } + break; + } + case MSG_SET_ACTIVE: + mDelegate.setActive((String) msg.obj, null /*Session.Info*/); + break; + case MSG_SET_RINGING: + mDelegate.setRinging((String) msg.obj, null /*Session.Info*/); + break; + case MSG_SET_DIALING: + mDelegate.setDialing((String) msg.obj, null /*Session.Info*/); + break; + case MSG_SET_PULLING: + mDelegate.setPulling((String) msg.obj, null /*Session.Info*/); + break; + case MSG_SET_DISCONNECTED: { + SomeArgs args = (SomeArgs) msg.obj; + try { + mDelegate.setDisconnected((String) args.arg1, (DisconnectCause) args.arg2, + null /*Session.Info*/); + } finally { + args.recycle(); + } + break; + } + case MSG_SET_ON_HOLD: + mDelegate.setOnHold((String) msg.obj, null /*Session.Info*/); + break; + case MSG_SET_RINGBACK_REQUESTED: + mDelegate.setRingbackRequested((String) msg.obj, msg.arg1 == 1, + null /*Session.Info*/); + break; + case MSG_SET_CONNECTION_CAPABILITIES: + mDelegate.setConnectionCapabilities((String) msg.obj, msg.arg1, + null /*Session.Info*/); + break; + case MSG_SET_CONNECTION_PROPERTIES: + mDelegate.setConnectionProperties((String) msg.obj, msg.arg1, + null /*Session.Info*/); + break; + case MSG_SET_IS_CONFERENCED: { + SomeArgs args = (SomeArgs) msg.obj; + try { + mDelegate.setIsConferenced((String) args.arg1, (String) args.arg2, + null /*Session.Info*/); + } finally { + args.recycle(); + } + break; + } + case MSG_ADD_CONFERENCE_CALL: { + SomeArgs args = (SomeArgs) msg.obj; + try { + mDelegate.addConferenceCall( + (String) args.arg1, (ParcelableConference) args.arg2, + null /*Session.Info*/); + } finally { + args.recycle(); + } + break; + } + case MSG_REMOVE_CALL: + mDelegate.removeCall((String) msg.obj, + null /*Session.Info*/); + break; + case MSG_ON_POST_DIAL_WAIT: { + SomeArgs args = (SomeArgs) msg.obj; + try { + mDelegate.onPostDialWait((String) args.arg1, (String) args.arg2, + null /*Session.Info*/); + } finally { + args.recycle(); + } + break; + } + case MSG_ON_POST_DIAL_CHAR: { + SomeArgs args = (SomeArgs) msg.obj; + try { + mDelegate.onPostDialChar((String) args.arg1, (char) args.argi1, + null /*Session.Info*/); + } finally { + args.recycle(); + } + break; + } + case MSG_QUERY_REMOTE_CALL_SERVICES: + mDelegate.queryRemoteConnectionServices((RemoteServiceCallback) msg.obj, + null /*Session.Info*/); + break; + case MSG_SET_VIDEO_STATE: + mDelegate.setVideoState((String) msg.obj, msg.arg1, null /*Session.Info*/); + break; + case MSG_SET_VIDEO_CALL_PROVIDER: { + SomeArgs args = (SomeArgs) msg.obj; + try { + mDelegate.setVideoProvider((String) args.arg1, + (IVideoProvider) args.arg2, null /*Session.Info*/); + } finally { + args.recycle(); + } + break; + } + case MSG_SET_IS_VOIP_AUDIO_MODE: + mDelegate.setIsVoipAudioMode((String) msg.obj, msg.arg1 == 1, + null /*Session.Info*/); + break; + case MSG_SET_STATUS_HINTS: { + SomeArgs args = (SomeArgs) msg.obj; + try { + mDelegate.setStatusHints((String) args.arg1, (StatusHints) args.arg2, + null /*Session.Info*/); + } finally { + args.recycle(); + } + break; + } + case MSG_SET_ADDRESS: { + SomeArgs args = (SomeArgs) msg.obj; + try { + mDelegate.setAddress((String) args.arg1, (Uri) args.arg2, args.argi1, + null /*Session.Info*/); + } finally { + args.recycle(); + } + break; + } + case MSG_SET_CALLER_DISPLAY_NAME: { + SomeArgs args = (SomeArgs) msg.obj; + try { + mDelegate.setCallerDisplayName( + (String) args.arg1, (String) args.arg2, args.argi1, + null /*Session.Info*/); + } finally { + args.recycle(); + } + break; + } + case MSG_SET_CONFERENCEABLE_CONNECTIONS: { + SomeArgs args = (SomeArgs) msg.obj; + try { + mDelegate.setConferenceableConnections((String) args.arg1, + (List<String>) args.arg2, null /*Session.Info*/); + } finally { + args.recycle(); + } + break; + } + case MSG_ADD_EXISTING_CONNECTION: { + SomeArgs args = (SomeArgs) msg.obj; + try { + mDelegate.addExistingConnection((String) args.arg1, + (ParcelableConnection) args.arg2, null /*Session.Info*/); + } finally { + args.recycle(); + } + break; + } + case MSG_SET_CONFERENCE_MERGE_FAILED: { + SomeArgs args = (SomeArgs) msg.obj; + try { + mDelegate.setConferenceMergeFailed((String) args.arg1, + null /*Session.Info*/); + } finally { + args.recycle(); + } + break; + } + case MSG_PUT_EXTRAS: { + SomeArgs args = (SomeArgs) msg.obj; + try { + mDelegate.putExtras((String) args.arg1, (Bundle) args.arg2, + null /*Session.Info*/); + } finally { + args.recycle(); + } + break; + } + case MSG_REMOVE_EXTRAS: { + SomeArgs args = (SomeArgs) msg.obj; + try { + mDelegate.removeExtras((String) args.arg1, (List<String>) args.arg2, + null /*Session.Info*/); + } finally { + args.recycle(); + } + break; + } + case MSG_ON_CONNECTION_EVENT: { + SomeArgs args = (SomeArgs) msg.obj; + try { + mDelegate.onConnectionEvent((String) args.arg1, (String) args.arg2, + (Bundle) args.arg3, null /*Session.Info*/); + } finally { + args.recycle(); + } + break; + } + case MSG_SET_AUDIO_ROUTE: { + SomeArgs args = (SomeArgs) msg.obj; + try { + mDelegate.setAudioRoute((String) args.arg1, args.argi1, + (Session.Info) args.arg2); + } finally { + args.recycle(); + } + break; + } + case MSG_ON_RTT_INITIATION_SUCCESS: + mDelegate.onRttInitiationSuccess((String) msg.obj, null /*Session.Info*/); + break; + case MSG_ON_RTT_INITIATION_FAILURE: + mDelegate.onRttInitiationFailure((String) msg.obj, msg.arg1, + null /*Session.Info*/); + break; + case MSG_ON_RTT_REMOTELY_TERMINATED: + mDelegate.onRttSessionRemotelyTerminated((String) msg.obj, + null /*Session.Info*/); + break; + case MSG_ON_RTT_UPGRADE_REQUEST: + mDelegate.onRemoteRttRequest((String) msg.obj, null /*Session.Info*/); + break; + case MSG_SET_PHONE_ACCOUNT_CHANGED: { + SomeArgs args = (SomeArgs) msg.obj; + try { + mDelegate.onPhoneAccountChanged((String) args.arg1, + (PhoneAccountHandle) args.arg2, null /*Session.Info*/); + } finally { + args.recycle(); + } + break; + } + } + } + }; + + private final IConnectionServiceAdapter mStub = new IConnectionServiceAdapter.Stub() { + @Override + public void handleCreateConnectionComplete( + String id, + ConnectionRequest request, + ParcelableConnection connection, + Session.Info sessionInfo) { + SomeArgs args = SomeArgs.obtain(); + args.arg1 = id; + args.arg2 = request; + args.arg3 = connection; + mHandler.obtainMessage(MSG_HANDLE_CREATE_CONNECTION_COMPLETE, args).sendToTarget(); + } + + @Override + public void setActive(String connectionId, Session.Info sessionInfo) { + mHandler.obtainMessage(MSG_SET_ACTIVE, connectionId).sendToTarget(); + } + + @Override + public void setRinging(String connectionId, Session.Info sessionInfo) { + mHandler.obtainMessage(MSG_SET_RINGING, connectionId).sendToTarget(); + } + + @Override + public void setDialing(String connectionId, Session.Info sessionInfo) { + mHandler.obtainMessage(MSG_SET_DIALING, connectionId).sendToTarget(); + } + + @Override + public void setPulling(String connectionId, Session.Info sessionInfo) { + mHandler.obtainMessage(MSG_SET_PULLING, connectionId).sendToTarget(); + } + + @Override + public void setDisconnected(String connectionId, DisconnectCause disconnectCause, + Session.Info sessionInfo) { + SomeArgs args = SomeArgs.obtain(); + args.arg1 = connectionId; + args.arg2 = disconnectCause; + mHandler.obtainMessage(MSG_SET_DISCONNECTED, args).sendToTarget(); + } + + @Override + public void setOnHold(String connectionId, Session.Info sessionInfo) { + mHandler.obtainMessage(MSG_SET_ON_HOLD, connectionId).sendToTarget(); + } + + @Override + public void setRingbackRequested(String connectionId, boolean ringback, + Session.Info sessionInfo) { + mHandler.obtainMessage(MSG_SET_RINGBACK_REQUESTED, ringback ? 1 : 0, 0, connectionId) + .sendToTarget(); + } + + @Override + public void setConnectionCapabilities(String connectionId, int connectionCapabilities, + Session.Info sessionInfo) { + mHandler.obtainMessage( + MSG_SET_CONNECTION_CAPABILITIES, connectionCapabilities, 0, connectionId) + .sendToTarget(); + } + + @Override + public void setConnectionProperties(String connectionId, int connectionProperties, + Session.Info sessionInfo) { + mHandler.obtainMessage( + MSG_SET_CONNECTION_PROPERTIES, connectionProperties, 0, connectionId) + .sendToTarget(); + } + + @Override + public void setConferenceMergeFailed(String callId, Session.Info sessionInfo) { + SomeArgs args = SomeArgs.obtain(); + args.arg1 = callId; + mHandler.obtainMessage(MSG_SET_CONFERENCE_MERGE_FAILED, args).sendToTarget(); + } + + @Override + public void setIsConferenced(String callId, String conferenceCallId, + Session.Info sessionInfo) { + SomeArgs args = SomeArgs.obtain(); + args.arg1 = callId; + args.arg2 = conferenceCallId; + mHandler.obtainMessage(MSG_SET_IS_CONFERENCED, args).sendToTarget(); + } + + @Override + public void addConferenceCall(String callId, ParcelableConference parcelableConference, + Session.Info sessionInfo) { + SomeArgs args = SomeArgs.obtain(); + args.arg1 = callId; + args.arg2 = parcelableConference; + mHandler.obtainMessage(MSG_ADD_CONFERENCE_CALL, args).sendToTarget(); + } + + @Override + public void removeCall(String connectionId, + Session.Info sessionInfo) { + mHandler.obtainMessage(MSG_REMOVE_CALL, connectionId).sendToTarget(); + } + + @Override + public void onPostDialWait(String connectionId, String remainingDigits, + Session.Info sessionInfo) { + SomeArgs args = SomeArgs.obtain(); + args.arg1 = connectionId; + args.arg2 = remainingDigits; + mHandler.obtainMessage(MSG_ON_POST_DIAL_WAIT, args).sendToTarget(); + } + + @Override + public void onPostDialChar(String connectionId, char nextChar, + Session.Info sessionInfo) { + SomeArgs args = SomeArgs.obtain(); + args.arg1 = connectionId; + args.argi1 = nextChar; + mHandler.obtainMessage(MSG_ON_POST_DIAL_CHAR, args).sendToTarget(); + } + + @Override + public void queryRemoteConnectionServices(RemoteServiceCallback callback, + Session.Info sessionInfo) { + mHandler.obtainMessage(MSG_QUERY_REMOTE_CALL_SERVICES, callback).sendToTarget(); + } + + @Override + public void setVideoState(String connectionId, int videoState, + Session.Info sessionInfo) { + mHandler.obtainMessage(MSG_SET_VIDEO_STATE, videoState, 0, connectionId).sendToTarget(); + } + + @Override + public void setVideoProvider(String connectionId, IVideoProvider videoProvider, + Session.Info sessionInfo) { + SomeArgs args = SomeArgs.obtain(); + args.arg1 = connectionId; + args.arg2 = videoProvider; + mHandler.obtainMessage(MSG_SET_VIDEO_CALL_PROVIDER, args).sendToTarget(); + } + + @Override + public final void setIsVoipAudioMode(String connectionId, boolean isVoip, + Session.Info sessionInfo) { + mHandler.obtainMessage(MSG_SET_IS_VOIP_AUDIO_MODE, isVoip ? 1 : 0, 0, + connectionId).sendToTarget(); + } + + @Override + public final void setStatusHints(String connectionId, StatusHints statusHints, + Session.Info sessionInfo) { + SomeArgs args = SomeArgs.obtain(); + args.arg1 = connectionId; + args.arg2 = statusHints; + mHandler.obtainMessage(MSG_SET_STATUS_HINTS, args).sendToTarget(); + } + + @Override + public final void setAddress(String connectionId, Uri address, int presentation, + Session.Info sessionInfo) { + SomeArgs args = SomeArgs.obtain(); + args.arg1 = connectionId; + args.arg2 = address; + args.argi1 = presentation; + mHandler.obtainMessage(MSG_SET_ADDRESS, args).sendToTarget(); + } + + @Override + public final void setCallerDisplayName( + String connectionId, String callerDisplayName, int presentation, + Session.Info sessionInfo) { + SomeArgs args = SomeArgs.obtain(); + args.arg1 = connectionId; + args.arg2 = callerDisplayName; + args.argi1 = presentation; + mHandler.obtainMessage(MSG_SET_CALLER_DISPLAY_NAME, args).sendToTarget(); + } + + @Override + public final void setConferenceableConnections(String connectionId, + List<String> conferenceableConnectionIds, Session.Info sessionInfo) { + SomeArgs args = SomeArgs.obtain(); + args.arg1 = connectionId; + args.arg2 = conferenceableConnectionIds; + mHandler.obtainMessage(MSG_SET_CONFERENCEABLE_CONNECTIONS, args).sendToTarget(); + } + + @Override + public final void addExistingConnection(String connectionId, + ParcelableConnection connection, Session.Info sessionInfo) { + SomeArgs args = SomeArgs.obtain(); + args.arg1 = connectionId; + args.arg2 = connection; + mHandler.obtainMessage(MSG_ADD_EXISTING_CONNECTION, args).sendToTarget(); + } + + @Override + public final void putExtras(String connectionId, Bundle extras, Session.Info sessionInfo) { + SomeArgs args = SomeArgs.obtain(); + args.arg1 = connectionId; + args.arg2 = extras; + mHandler.obtainMessage(MSG_PUT_EXTRAS, args).sendToTarget(); + } + + @Override + public final void removeExtras(String connectionId, List<String> keys, + Session.Info sessionInfo) { + SomeArgs args = SomeArgs.obtain(); + args.arg1 = connectionId; + args.arg2 = keys; + mHandler.obtainMessage(MSG_REMOVE_EXTRAS, args).sendToTarget(); + } + + @Override + public final void setAudioRoute(String connectionId, int audioRoute, + Session.Info sessionInfo) { + + SomeArgs args = SomeArgs.obtain(); + args.arg1 = connectionId; + args.argi1 = audioRoute; + args.arg2 = sessionInfo; + mHandler.obtainMessage(MSG_SET_AUDIO_ROUTE, args).sendToTarget(); + } + + @Override + public final void onConnectionEvent(String connectionId, String event, Bundle extras, + Session.Info sessionInfo) { + SomeArgs args = SomeArgs.obtain(); + args.arg1 = connectionId; + args.arg2 = event; + args.arg3 = extras; + mHandler.obtainMessage(MSG_ON_CONNECTION_EVENT, args).sendToTarget(); + } + + @Override + public void onRttInitiationSuccess(String connectionId, Session.Info sessionInfo) + throws RemoteException { + mHandler.obtainMessage(MSG_ON_RTT_INITIATION_SUCCESS, connectionId).sendToTarget(); + } + + @Override + public void onRttInitiationFailure(String connectionId, int reason, + Session.Info sessionInfo) + throws RemoteException { + mHandler.obtainMessage(MSG_ON_RTT_INITIATION_FAILURE, reason, 0, connectionId) + .sendToTarget(); + } + + @Override + public void onRttSessionRemotelyTerminated(String connectionId, Session.Info sessionInfo) + throws RemoteException { + mHandler.obtainMessage(MSG_ON_RTT_REMOTELY_TERMINATED, connectionId).sendToTarget(); + } + + @Override + public void onRemoteRttRequest(String connectionId, Session.Info sessionInfo) + throws RemoteException { + mHandler.obtainMessage(MSG_ON_RTT_UPGRADE_REQUEST, connectionId).sendToTarget(); + } + + @Override + public void onPhoneAccountChanged(String callId, PhoneAccountHandle pHandle, + Session.Info sessionInfo) { + SomeArgs args = SomeArgs.obtain(); + args.arg1 = callId; + args.arg2 = pHandle; + mHandler.obtainMessage(MSG_SET_PHONE_ACCOUNT_CHANGED, args).sendToTarget(); + } + }; + + public ConnectionServiceAdapterServant(IConnectionServiceAdapter delegate) { + mDelegate = delegate; + } + + public IConnectionServiceAdapter getStub() { + return mStub; + } +} diff --git a/android/telecom/DefaultDialerManager.java b/android/telecom/DefaultDialerManager.java new file mode 100644 index 00000000..2a707c91 --- /dev/null +++ b/android/telecom/DefaultDialerManager.java @@ -0,0 +1,235 @@ +/* + * Copyright (C) 2015 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.app.ActivityManager; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.net.Uri; +import android.os.Process; +import android.provider.Settings; +import android.text.TextUtils; + +import java.util.ArrayList; +import java.util.List; + +/** + * Class for managing the default dialer application that will receive incoming calls, and be + * allowed to make emergency outgoing calls. + * + * @hide + */ +public class DefaultDialerManager { + private static final String TAG = "DefaultDialerManager"; + + /** + * Sets the specified package name as the default dialer application for the current user. + * The caller of this method needs to have permission to write to secure settings and + * manage users on the device. + * + * @return {@code true} if the default dialer application was successfully changed, + * {@code false} otherwise. + * + * @hide + * */ + public static boolean setDefaultDialerApplication(Context context, String packageName) { + return setDefaultDialerApplication(context, packageName, ActivityManager.getCurrentUser()); + } + + /** + * Sets the specified package name as the default dialer application for the specified user. + * The caller of this method needs to have permission to write to secure settings and + * manage users on the device. + * + * @return {@code true} if the default dialer application was successfully changed, + * {@code false} otherwise. + * + * @hide + * */ + public static boolean setDefaultDialerApplication(Context context, String packageName, + int user) { + // Get old package name + String oldPackageName = Settings.Secure.getStringForUser(context.getContentResolver(), + Settings.Secure.DIALER_DEFAULT_APPLICATION, user); + + if (packageName != null && oldPackageName != null && packageName.equals(oldPackageName)) { + // No change + return false; + } + + // Only make the change if the new package belongs to a valid phone application + List<String> packageNames = getInstalledDialerApplications(context); + + if (packageNames.contains(packageName)) { + // Update the secure setting. + Settings.Secure.putStringForUser(context.getContentResolver(), + Settings.Secure.DIALER_DEFAULT_APPLICATION, packageName, user); + return true; + } + return false; + } + + /** + * Returns the installed dialer application for the current user that will be used to receive + * incoming calls, and is allowed to make emergency calls. + * + * The application will be returned in order of preference: + * 1) User selected phone application (if still installed) + * 2) Pre-installed system dialer (if not disabled) + * 3) Null + * + * The caller of this method needs to have permission to manage users on the device. + * + * @hide + * */ + public static String getDefaultDialerApplication(Context context) { + return getDefaultDialerApplication(context, context.getUserId()); + } + + /** + * Returns the installed dialer application for the specified user that will be used to receive + * incoming calls, and is allowed to make emergency calls. + * + * The application will be returned in order of preference: + * 1) User selected phone application (if still installed) + * 2) Pre-installed system dialer (if not disabled) + * 3) Null + * + * The caller of this method needs to have permission to manage users on the device. + * + * @hide + * */ + public static String getDefaultDialerApplication(Context context, int user) { + String defaultPackageName = Settings.Secure.getStringForUser(context.getContentResolver(), + Settings.Secure.DIALER_DEFAULT_APPLICATION, user); + + final List<String> packageNames = getInstalledDialerApplications(context, user); + + // Verify that the default dialer has not been disabled or uninstalled. + if (packageNames.contains(defaultPackageName)) { + return defaultPackageName; + } + + // No user-set dialer found, fallback to system dialer + String systemDialerPackageName = getTelecomManager(context).getSystemDialerPackage(); + + if (TextUtils.isEmpty(systemDialerPackageName)) { + // No system dialer configured at build time + return null; + } + + if (packageNames.contains(systemDialerPackageName)) { + return systemDialerPackageName; + } else { + return null; + } + } + + /** + * Returns a list of installed and available dialer applications. + * + * In order to appear in the list, a dialer application must implement an intent-filter with + * the DIAL intent for the following schemes: + * + * 1) Empty scheme + * 2) tel Uri scheme + * + * @hide + **/ + public static List<String> getInstalledDialerApplications(Context context, int userId) { + PackageManager packageManager = context.getPackageManager(); + + // Get the list of apps registered for the DIAL intent with empty scheme + Intent intent = new Intent(Intent.ACTION_DIAL); + List<ResolveInfo> resolveInfoList = + packageManager.queryIntentActivitiesAsUser(intent, 0, userId); + + List<String> packageNames = new ArrayList<>(); + + for (ResolveInfo resolveInfo : resolveInfoList) { + final ActivityInfo activityInfo = resolveInfo.activityInfo; + if (activityInfo != null && !packageNames.contains(activityInfo.packageName)) { + packageNames.add(activityInfo.packageName); + } + } + + final Intent dialIntentWithTelScheme = new Intent(Intent.ACTION_DIAL); + dialIntentWithTelScheme.setData(Uri.fromParts(PhoneAccount.SCHEME_TEL, "", null)); + return filterByIntent(context, packageNames, dialIntentWithTelScheme, userId); + } + + public static List<String> getInstalledDialerApplications(Context context) { + return getInstalledDialerApplications(context, Process.myUserHandle().getIdentifier()); + } + + /** + * Determines if the package name belongs to the user-selected default dialer or the preloaded + * system dialer, and thus should be allowed to perform certain privileged operations. + * + * @param context A valid context. + * @param packageName of the package to check for. + * + * @return {@code true} if the provided package name corresponds to the user-selected default + * dialer or the preloaded system dialer, {@code false} otherwise. + * + * @hide + */ + public static boolean isDefaultOrSystemDialer(Context context, String packageName) { + if (TextUtils.isEmpty(packageName)) { + return false; + } + final TelecomManager tm = getTelecomManager(context); + return packageName.equals(tm.getDefaultDialerPackage()) + || packageName.equals(tm.getSystemDialerPackage()); + } + + /** + * Filter a given list of package names for those packages that contain an activity that has + * an intent filter for a given intent. + * + * @param context A valid context + * @param packageNames List of package names to filter. + * @param userId The UserId + * @return The filtered list. + */ + private static List<String> filterByIntent(Context context, List<String> packageNames, + Intent intent, int userId) { + if (packageNames == null || packageNames.isEmpty()) { + return new ArrayList<>(); + } + + final List<String> result = new ArrayList<>(); + final List<ResolveInfo> resolveInfoList = context.getPackageManager() + .queryIntentActivitiesAsUser(intent, 0, userId); + final int length = resolveInfoList.size(); + for (int i = 0; i < length; i++) { + final ActivityInfo info = resolveInfoList.get(i).activityInfo; + if (info != null && packageNames.contains(info.packageName) + && !result.contains(info.packageName)) { + result.add(info.packageName); + } + } + + return result; + } + + + private static TelecomManager getTelecomManager(Context context) { + return (TelecomManager) context.getSystemService(Context.TELECOM_SERVICE); + } +} diff --git a/android/telecom/DisconnectCause.java b/android/telecom/DisconnectCause.java new file mode 100644 index 00000000..dcf5c271 --- /dev/null +++ b/android/telecom/DisconnectCause.java @@ -0,0 +1,313 @@ +/* + * Copyright (C) 2014 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.Parcel; +import android.os.Parcelable; +import android.media.ToneGenerator; +import android.text.TextUtils; + +import java.util.Objects; + +/** + * Describes the cause of a disconnected call. This always includes a code describing the generic + * cause of the disconnect. Optionally, it may include a label and/or description to display to the + * user. It is the responsibility of the {@link ConnectionService} to provide localized versions of + * the label and description. It also may contain a reason for the disconnect, which is intended for + * logging and not for display to the user. + */ +public final class DisconnectCause implements Parcelable { + + /** Disconnected because of an unknown or unspecified reason. */ + public static final int UNKNOWN = 0; + /** Disconnected because there was an error, such as a problem with the network. */ + public static final int ERROR = 1; + /** Disconnected because of a local user-initiated action, such as hanging up. */ + public static final int LOCAL = 2; + /** + * Disconnected because of a remote user-initiated action, such as the other party hanging up + * up. + */ + public static final int REMOTE = 3; + /** Disconnected because it has been canceled. */ + public static final int CANCELED = 4; + /** Disconnected because there was no response to an incoming call. */ + public static final int MISSED = 5; + /** Disconnected because the user rejected an incoming call. */ + public static final int REJECTED = 6; + /** Disconnected because the other party was busy. */ + public static final int BUSY = 7; + /** + * Disconnected because of a restriction on placing the call, such as dialing in airplane + * mode. + */ + public static final int RESTRICTED = 8; + /** Disconnected for reason not described by other disconnect codes. */ + public static final int OTHER = 9; + /** + * Disconnected because the connection manager did not support the call. The call will be tried + * again without a connection manager. See {@link PhoneAccount#CAPABILITY_CONNECTION_MANAGER}. + */ + public static final int CONNECTION_MANAGER_NOT_SUPPORTED = 10; + + /** + * Disconnected because the user did not locally answer the incoming call, but it was answered + * on another device where the call was ringing. + */ + public static final int ANSWERED_ELSEWHERE = 11; + + /** + * Disconnected because the call was pulled from the current device to another device. + */ + public static final int CALL_PULLED = 12; + + /** + * Reason code (returned via {@link #getReason()}) which indicates that a call could not be + * completed because the cellular radio is off or out of service, the device is connected to + * a wifi network, but the user has not enabled wifi calling. + * @hide + */ + public static final String REASON_WIFI_ON_BUT_WFC_OFF = "REASON_WIFI_ON_BUT_WFC_OFF"; + + /** + * Reason code (returned via {@link #getReason()}), which indicates that the video telephony + * call was disconnected because IMS access is blocked. + * @hide + */ + public static final String REASON_IMS_ACCESS_BLOCKED = "REASON_IMS_ACCESS_BLOCKED"; + + private int mDisconnectCode; + private CharSequence mDisconnectLabel; + private CharSequence mDisconnectDescription; + private String mDisconnectReason; + private int mToneToPlay; + + /** + * Creates a new DisconnectCause. + * + * @param code The code for the disconnect cause. + */ + public DisconnectCause(int code) { + this(code, null, null, null, ToneGenerator.TONE_UNKNOWN); + } + + /** + * Creates a new DisconnectCause. + * + * @param code The code for the disconnect cause. + * @param reason The reason for the disconnect. + */ + public DisconnectCause(int code, String reason) { + this(code, null, null, reason, ToneGenerator.TONE_UNKNOWN); + } + + /** + * Creates a new DisconnectCause. + * + * @param code The code for the disconnect cause. + * @param label The localized label to show to the user to explain the disconnect. + * @param description The localized description to show to the user to explain the disconnect. + * @param reason The reason for the disconnect. + */ + public DisconnectCause(int code, CharSequence label, CharSequence description, String reason) { + this(code, label, description, reason, ToneGenerator.TONE_UNKNOWN); + } + + /** + * Creates a new DisconnectCause. + * + * @param code The code for the disconnect cause. + * @param label The localized label to show to the user to explain the disconnect. + * @param description The localized description to show to the user to explain the disconnect. + * @param reason The reason for the disconnect. + * @param toneToPlay The tone to play on disconnect, as defined in {@link ToneGenerator}. + */ + public DisconnectCause(int code, CharSequence label, CharSequence description, String reason, + int toneToPlay) { + mDisconnectCode = code; + mDisconnectLabel = label; + mDisconnectDescription = description; + mDisconnectReason = reason; + mToneToPlay = toneToPlay; + } + + /** + * Returns the code for the reason for this disconnect. + * + * @return The disconnect code. + */ + public int getCode() { + return mDisconnectCode; + } + + /** + * Returns a short label which explains the reason for the disconnect cause and is for display + * in the user interface. If not null, it is expected that the In-Call UI should display this + * text where it would normally display the call state ("Dialing", "Disconnected") and is + * therefore expected to be relatively small. The {@link ConnectionService } is responsible for + * providing and localizing this label. If there is no string provided, returns null. + * + * @return The disconnect label. + */ + public CharSequence getLabel() { + return mDisconnectLabel; + } + + /** + * Returns a description which explains the reason for the disconnect cause and is for display + * in the user interface. This optional text is generally a longer and more descriptive version + * of {@link #getLabel}, however it can exist even if {@link #getLabel} is empty. The In-Call UI + * should display this relatively prominently; the traditional implementation displays this as + * an alert dialog. The {@link ConnectionService} is responsible for providing and localizing + * this message. If there is no string provided, returns null. + * + * @return The disconnect description. + */ + public CharSequence getDescription() { + return mDisconnectDescription; + } + + /** + * Returns an explanation of the reason for the disconnect. This is not intended for display to + * the user and is used mainly for logging. + * + * @return The disconnect reason. + */ + public String getReason() { + return mDisconnectReason; + } + + /** + * Returns the tone to play when disconnected. + * + * @return the tone as defined in {@link ToneGenerator} to play when disconnected. + */ + public int getTone() { + return mToneToPlay; + } + + public static final Creator<DisconnectCause> CREATOR = new Creator<DisconnectCause>() { + @Override + public DisconnectCause createFromParcel(Parcel source) { + int code = source.readInt(); + CharSequence label = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source); + CharSequence description = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source); + String reason = source.readString(); + int tone = source.readInt(); + return new DisconnectCause(code, label, description, reason, tone); + } + + @Override + public DisconnectCause[] newArray(int size) { + return new DisconnectCause[size]; + } + }; + + @Override + public void writeToParcel(Parcel destination, int flags) { + destination.writeInt(mDisconnectCode); + TextUtils.writeToParcel(mDisconnectLabel, destination, flags); + TextUtils.writeToParcel(mDisconnectDescription, destination, flags); + destination.writeString(mDisconnectReason); + destination.writeInt(mToneToPlay); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public int hashCode() { + return Objects.hashCode(mDisconnectCode) + + Objects.hashCode(mDisconnectLabel) + + Objects.hashCode(mDisconnectDescription) + + Objects.hashCode(mDisconnectReason) + + Objects.hashCode(mToneToPlay); + } + + @Override + public boolean equals(Object o) { + if (o instanceof DisconnectCause) { + DisconnectCause d = (DisconnectCause) o; + return Objects.equals(mDisconnectCode, d.getCode()) + && Objects.equals(mDisconnectLabel, d.getLabel()) + && Objects.equals(mDisconnectDescription, d.getDescription()) + && Objects.equals(mDisconnectReason, d.getReason()) + && Objects.equals(mToneToPlay, d.getTone()); + } + return false; + } + + @Override + public String toString() { + String code = ""; + switch (mDisconnectCode) { + case UNKNOWN: + code = "UNKNOWN"; + break; + case ERROR: + code = "ERROR"; + break; + case LOCAL: + code = "LOCAL"; + break; + case REMOTE: + code = "REMOTE"; + break; + case CANCELED: + code = "CANCELED"; + break; + case MISSED: + code = "MISSED"; + break; + case REJECTED: + code = "REJECTED"; + break; + case BUSY: + code = "BUSY"; + break; + case RESTRICTED: + code = "RESTRICTED"; + break; + case OTHER: + code = "OTHER"; + break; + case CONNECTION_MANAGER_NOT_SUPPORTED: + code = "CONNECTION_MANAGER_NOT_SUPPORTED"; + break; + case CALL_PULLED: + code = "CALL_PULLED"; + break; + case ANSWERED_ELSEWHERE: + code = "ANSWERED_ELSEWHERE"; + break; + default: + code = "invalid code: " + mDisconnectCode; + break; + } + String label = mDisconnectLabel == null ? "" : mDisconnectLabel.toString(); + String description = mDisconnectDescription == null + ? "" : mDisconnectDescription.toString(); + String reason = mDisconnectReason == null ? "" : mDisconnectReason; + return "DisconnectCause [ Code: (" + code + ")" + + " Label: (" + label + ")" + + " Description: (" + description + ")" + + " Reason: (" + reason + ")" + + " Tone: (" + mToneToPlay + ") ]"; + } +} diff --git a/android/telecom/GatewayInfo.java b/android/telecom/GatewayInfo.java new file mode 100644 index 00000000..928570e2 --- /dev/null +++ b/android/telecom/GatewayInfo.java @@ -0,0 +1,117 @@ +/* + * Copyright 2014, 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.net.Uri; +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; + +/** + * Encapsulated gateway address information for outgoing call. When calls are made, the system + * provides a facility to specify two addresses for the call: one to display as the address being + * dialed and a separate (gateway) address to actually dial. Telecom provides this information to + * {@link ConnectionService}s when placing the call as an instance of {@code GatewayInfo}. + * <p> + * The data consists of an address to call, an address to display and the package name of the + * service. This data is used in two ways: + * <ol> + * <li> Call the appropriate gateway address. + * <li> Display information about how the call is being routed to the user. + * </ol> + */ +public class GatewayInfo implements Parcelable { + + private final String mGatewayProviderPackageName; + private final Uri mGatewayAddress; + private final Uri mOriginalAddress; + + public GatewayInfo(String packageName, Uri gatewayUri, Uri originalAddress) { + mGatewayProviderPackageName = packageName; + mGatewayAddress = gatewayUri; + mOriginalAddress = originalAddress; + } + + /** + * Package name of the gateway provider service that provided the gateway information. + * This can be used to identify the gateway address source and to load an appropriate icon when + * displaying gateway information in the in-call UI. + */ + public String getGatewayProviderPackageName() { + return mGatewayProviderPackageName; + } + + /** + * Returns the gateway address to dial when placing the call. + */ + public Uri getGatewayAddress() { + return mGatewayAddress; + } + + /** + * Returns the address that the user is trying to connect to via the gateway. + */ + public Uri getOriginalAddress() { + return mOriginalAddress; + } + + /** + * Indicates whether this {@code GatewayInfo} instance contains any data. A returned value of + * false indicates that no gateway number is being used for the call. + */ + public boolean isEmpty() { + return TextUtils.isEmpty(mGatewayProviderPackageName) || mGatewayAddress == null; + } + + /** + * The Parcelable interface. + * */ + public static final Parcelable.Creator<GatewayInfo> CREATOR = + new Parcelable.Creator<GatewayInfo> () { + + @Override + public GatewayInfo createFromParcel(Parcel source) { + String gatewayPackageName = source.readString(); + Uri gatewayUri = Uri.CREATOR.createFromParcel(source); + Uri originalAddress = Uri.CREATOR.createFromParcel(source); + return new GatewayInfo(gatewayPackageName, gatewayUri, originalAddress); + } + + @Override + public GatewayInfo[] newArray(int size) { + return new GatewayInfo[size]; + } + }; + + /** + * {@inheritDoc} + */ + @Override + public int describeContents() { + return 0; + } + + /** + * {@inheritDoc} + */ + @Override + public void writeToParcel(Parcel destination, int flags) { + destination.writeString(mGatewayProviderPackageName); + mGatewayAddress.writeToParcel(destination, 0); + mOriginalAddress.writeToParcel(destination, 0); + } +} diff --git a/android/telecom/InCallAdapter.java b/android/telecom/InCallAdapter.java new file mode 100644 index 00000000..9559a28c --- /dev/null +++ b/android/telecom/InCallAdapter.java @@ -0,0 +1,422 @@ +/* + * Copyright (C) 2014 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.Bundle; +import android.os.RemoteException; + +import com.android.internal.telecom.IInCallAdapter; + +import java.util.List; + +/** + * Receives commands from {@link InCallService} implementations which should be executed by + * Telecom. When Telecom binds to a {@link InCallService}, an instance of this class is given to + * the in-call service through which it can manipulate live (active, dialing, ringing) calls. When + * the in-call service is notified of new calls, it can use the + * given call IDs to execute commands such as {@link #answerCall} for incoming calls or + * {@link #disconnectCall} for active calls the user would like to end. Some commands are only + * appropriate for calls in certain states; please consult each method for such limitations. + * <p> + * The adapter will stop functioning when there are no more calls. + * + * @hide + */ +public final class InCallAdapter { + private final IInCallAdapter mAdapter; + + /** + * {@hide} + */ + public InCallAdapter(IInCallAdapter adapter) { + mAdapter = adapter; + } + + /** + * Instructs Telecom to answer the specified call. + * + * @param callId The identifier of the call to answer. + * @param videoState The video state in which to answer the call. + */ + public void answerCall(String callId, int videoState) { + try { + mAdapter.answerCall(callId, videoState); + } catch (RemoteException e) { + } + } + + /** + * Instructs Telecom to reject the specified call. + * + * @param callId The identifier of the call to reject. + * @param rejectWithMessage Whether to reject with a text message. + * @param textMessage An optional text message with which to respond. + */ + public void rejectCall(String callId, boolean rejectWithMessage, String textMessage) { + try { + mAdapter.rejectCall(callId, rejectWithMessage, textMessage); + } catch (RemoteException e) { + } + } + + /** + * Instructs Telecom to disconnect the specified call. + * + * @param callId The identifier of the call to disconnect. + */ + public void disconnectCall(String callId) { + try { + mAdapter.disconnectCall(callId); + } catch (RemoteException e) { + } + } + + /** + * Instructs Telecom to put the specified call on hold. + * + * @param callId The identifier of the call to put on hold. + */ + public void holdCall(String callId) { + try { + mAdapter.holdCall(callId); + } catch (RemoteException e) { + } + } + + /** + * Instructs Telecom to release the specified call from hold. + * + * @param callId The identifier of the call to release from hold. + */ + public void unholdCall(String callId) { + try { + mAdapter.unholdCall(callId); + } catch (RemoteException e) { + } + } + + /** + * Mute the microphone. + * + * @param shouldMute True if the microphone should be muted. + */ + public void mute(boolean shouldMute) { + try { + mAdapter.mute(shouldMute); + } catch (RemoteException e) { + } + } + + /** + * Sets the audio route (speaker, bluetooth, etc...). See {@link CallAudioState}. + * + * @param route The audio route to use. + */ + public void setAudioRoute(int route) { + try { + mAdapter.setAudioRoute(route); + } catch (RemoteException e) { + } + } + + /** + * 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. + * + * @param callId The unique ID of the call in which the tone will be played. + * @param digit A character representing the DTMF digit for which to play the tone. This + * value must be one of {@code '0'} through {@code '9'}, {@code '*'} or {@code '#'}. + */ + public void playDtmfTone(String callId, char digit) { + try { + mAdapter.playDtmfTone(callId, digit); + } catch (RemoteException e) { + } + } + + /** + * Instructs Telecom to stop any dual-tone multi-frequency signaling (DTMF) tone currently + * playing. + * + * DTMF tones are played by calling {@link #playDtmfTone(String,char)}. If no DTMF tone is + * currently playing, this method will do nothing. + * + * @param callId The unique ID of the call in which any currently playing tone will be stopped. + */ + public void stopDtmfTone(String callId) { + try { + mAdapter.stopDtmfTone(callId); + } catch (RemoteException e) { + } + } + + /** + * Instructs Telecom to continue playing a post-dial DTMF string. + * + * A post-dial DTMF string is a string of digits entered after a phone number, when dialed, + * that are immediately sent as DTMF tones to the recipient as soon as the connection is made. + * While these tones are playing, Telecom will notify the {@link InCallService} that the call + * is in the post dial state. + * + * If the DTMF string contains a {@link TelecomManager#DTMF_CHARACTER_PAUSE} symbol, Telecom + * will temporarily pause playing the tones for a pre-defined period of time. + * + * If the DTMF string contains a {@link TelecomManager#DTMF_CHARACTER_WAIT} symbol, Telecom + * will pause playing the tones and notify the {@link InCallService} that the call is in the + * post dial wait state. When the user decides to continue the postdial sequence, the + * {@link InCallService} should invoke the {@link #postDialContinue(String,boolean)} method. + * + * @param callId The unique ID of the call for which postdial string playing should continue. + * @param proceed Whether or not to continue with the post-dial sequence. + */ + public void postDialContinue(String callId, boolean proceed) { + try { + mAdapter.postDialContinue(callId, proceed); + } catch (RemoteException e) { + } + } + + /** + * Instructs Telecom to add a PhoneAccountHandle to the specified call. + * + * @param callId The identifier of the call. + * @param accountHandle The PhoneAccountHandle through which to place the call. + * @param setDefault {@code True} if this account should be set as the default for calls. + */ + public void phoneAccountSelected(String callId, PhoneAccountHandle accountHandle, + boolean setDefault) { + try { + mAdapter.phoneAccountSelected(callId, accountHandle, setDefault); + } catch (RemoteException e) { + } + } + + /** + * Instructs Telecom to conference the specified call. + * + * @param callId The unique ID of the call. + * @hide + */ + public void conference(String callId, String otherCallId) { + try { + mAdapter.conference(callId, otherCallId); + } catch (RemoteException ignored) { + } + } + + /** + * Instructs Telecom to split the specified call from any conference call with which it may be + * connected. + * + * @param callId The unique ID of the call. + * @hide + */ + public void splitFromConference(String callId) { + try { + mAdapter.splitFromConference(callId); + } catch (RemoteException ignored) { + } + } + + /** + * Instructs Telecom to merge child calls of the specified conference call. + */ + public void mergeConference(String callId) { + try { + mAdapter.mergeConference(callId); + } catch (RemoteException ignored) { + } + } + + /** + * Instructs Telecom to swap the child calls of the specified conference call. + */ + public void swapConference(String callId) { + try { + mAdapter.swapConference(callId); + } catch (RemoteException ignored) { + } + } + + /** + * Instructs Telecom to pull an external call to the local device. + * + * @param callId The callId to pull. + */ + public void pullExternalCall(String callId) { + try { + mAdapter.pullExternalCall(callId); + } catch (RemoteException ignored) { + } + } + + /** + * Intructs Telecom to send a call event. + * + * @param callId The callId to send the event for. + * @param event The event. + * @param extras Extras associated with the event. + */ + public void sendCallEvent(String callId, String event, Bundle extras) { + try { + mAdapter.sendCallEvent(callId, event, extras); + } catch (RemoteException ignored) { + } + } + + /** + * Intructs Telecom to add extras to a call. + * + * @param callId The callId to add the extras to. + * @param extras The extras. + */ + public void putExtras(String callId, Bundle extras) { + try { + mAdapter.putExtras(callId, extras); + } catch (RemoteException ignored) { + } + } + + /** + * Intructs Telecom to add an extra to a call. + * + * @param callId The callId to add the extras to. + * @param key The extra key. + * @param value The extra value. + */ + public void putExtra(String callId, String key, boolean value) { + try { + Bundle bundle = new Bundle(); + bundle.putBoolean(key, value); + mAdapter.putExtras(callId, bundle); + } catch (RemoteException ignored) { + } + } + + /** + * Intructs Telecom to add an extra to a call. + * + * @param callId The callId to add the extras to. + * @param key The extra key. + * @param value The extra value. + */ + public void putExtra(String callId, String key, int value) { + try { + Bundle bundle = new Bundle(); + bundle.putInt(key, value); + mAdapter.putExtras(callId, bundle); + } catch (RemoteException ignored) { + } + } + + /** + * Intructs Telecom to add an extra to a call. + * + * @param callId The callId to add the extras to. + * @param key The extra key. + * @param value The extra value. + */ + public void putExtra(String callId, String key, String value) { + try { + Bundle bundle = new Bundle(); + bundle.putString(key, value); + mAdapter.putExtras(callId, bundle); + } catch (RemoteException ignored) { + } + } + + /** + * Intructs Telecom to remove extras from a call. + * @param callId The callId to remove the extras from. + * @param keys The extra keys to remove. + */ + public void removeExtras(String callId, List<String> keys) { + try { + mAdapter.removeExtras(callId, keys); + } catch (RemoteException ignored) { + } + } + + /** + * Instructs Telecom to turn the proximity sensor on. + */ + public void turnProximitySensorOn() { + try { + mAdapter.turnOnProximitySensor(); + } catch (RemoteException ignored) { + } + } + + /** + * Instructs Telecom to turn the proximity sensor off. + * + * @param screenOnImmediately If true, the screen will be turned on immediately if it was + * previously off. Otherwise, the screen will only be turned on after the proximity sensor + * is no longer triggered. + */ + public void turnProximitySensorOff(boolean screenOnImmediately) { + try { + mAdapter.turnOffProximitySensor(screenOnImmediately); + } catch (RemoteException ignored) { + } + } + + /** + * Sends an RTT upgrade request to the remote end of the connection. + */ + public void sendRttRequest(String callId) { + try { + mAdapter.sendRttRequest(callId); + } catch (RemoteException ignored) { + } + } + + /** + * Responds to an RTT upgrade request initiated from the remote end. + * + * @param id the ID of the request as specified by Telecom + * @param accept Whether the request should be accepted. + */ + public void respondToRttRequest(String callId, int id, boolean accept) { + try { + mAdapter.respondToRttRequest(callId, id, accept); + } catch (RemoteException ignored) { + } + } + + /** + * Instructs Telecom to shut down the RTT communication channel. + */ + public void stopRtt(String callId) { + try { + mAdapter.stopRtt(callId); + } catch (RemoteException ignored) { + } + } + + /** + * Sets the RTT audio mode. + * @param mode the desired RTT audio mode + */ + public void setRttMode(String callId, int mode) { + try { + mAdapter.setRttMode(callId, mode); + } catch (RemoteException ignored) { + } + } +} diff --git a/android/telecom/InCallService.java b/android/telecom/InCallService.java new file mode 100644 index 00000000..e384d469 --- /dev/null +++ b/android/telecom/InCallService.java @@ -0,0 +1,754 @@ +/* + * Copyright (C) 2013 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.SdkConstant; +import android.annotation.SystemApi; +import android.app.Service; +import android.content.Intent; +import android.hardware.camera2.CameraManager; +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.view.Surface; + +import com.android.internal.os.SomeArgs; +import com.android.internal.telecom.IInCallAdapter; +import com.android.internal.telecom.IInCallService; + +import java.lang.String; +import java.util.Collections; +import java.util.List; + +/** + * This service is implemented by any app that wishes to provide the user-interface for managing + * phone calls. Telecom binds to this service while there exists a live (active or incoming) call, + * and uses it to notify the in-call app of any live and recently disconnected calls. An app must + * first be set as the default phone app (See {@link TelecomManager#getDefaultDialerPackage()}) + * before the telecom service will bind to its {@code InCallService} implementation. + * <p> + * Below is an example manifest registration for an {@code InCallService}. The meta-data + * ({@link TelecomManager#METADATA_IN_CALL_SERVICE_UI}) indicates that this particular + * {@code InCallService} implementation intends to replace the built-in in-call UI. + * <pre> + * {@code + * <service android:name="your.package.YourInCallServiceImplementation" + * android:permission="android.permission.BIND_INCALL_SERVICE"> + * <meta-data android:name="android.telecom.IN_CALL_SERVICE_UI" android:value="true" /> + * <intent-filter> + * <action android:name="android.telecom.InCallService"/> + * </intent-filter> + * </service> + * } + * </pre> + */ +public abstract class InCallService extends Service { + + /** + * The {@link Intent} that must be declared as handled by the service. + */ + @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION) + public static final String SERVICE_INTERFACE = "android.telecom.InCallService"; + + private static final int MSG_SET_IN_CALL_ADAPTER = 1; + private static final int MSG_ADD_CALL = 2; + private static final int MSG_UPDATE_CALL = 3; + private static final int MSG_SET_POST_DIAL_WAIT = 4; + private static final int MSG_ON_CALL_AUDIO_STATE_CHANGED = 5; + private static final int MSG_BRING_TO_FOREGROUND = 6; + private static final int MSG_ON_CAN_ADD_CALL_CHANGED = 7; + private static final int MSG_SILENCE_RINGER = 8; + private static final int MSG_ON_CONNECTION_EVENT = 9; + private static final int MSG_ON_RTT_UPGRADE_REQUEST = 10; + private static final int MSG_ON_RTT_INITIATION_FAILURE = 11; + + /** 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 (mPhone == null && msg.what != MSG_SET_IN_CALL_ADAPTER) { + return; + } + + switch (msg.what) { + case MSG_SET_IN_CALL_ADAPTER: + String callingPackage = getApplicationContext().getOpPackageName(); + mPhone = new Phone(new InCallAdapter((IInCallAdapter) msg.obj), callingPackage, + getApplicationContext().getApplicationInfo().targetSdkVersion); + mPhone.addListener(mPhoneListener); + onPhoneCreated(mPhone); + break; + case MSG_ADD_CALL: + mPhone.internalAddCall((ParcelableCall) msg.obj); + break; + case MSG_UPDATE_CALL: + mPhone.internalUpdateCall((ParcelableCall) msg.obj); + break; + case MSG_SET_POST_DIAL_WAIT: { + SomeArgs args = (SomeArgs) msg.obj; + try { + String callId = (String) args.arg1; + String remaining = (String) args.arg2; + mPhone.internalSetPostDialWait(callId, remaining); + } finally { + args.recycle(); + } + break; + } + case MSG_ON_CALL_AUDIO_STATE_CHANGED: + mPhone.internalCallAudioStateChanged((CallAudioState) msg.obj); + break; + case MSG_BRING_TO_FOREGROUND: + mPhone.internalBringToForeground(msg.arg1 == 1); + break; + case MSG_ON_CAN_ADD_CALL_CHANGED: + mPhone.internalSetCanAddCall(msg.arg1 == 1); + break; + case MSG_SILENCE_RINGER: + mPhone.internalSilenceRinger(); + break; + case MSG_ON_CONNECTION_EVENT: { + SomeArgs args = (SomeArgs) msg.obj; + try { + String callId = (String) args.arg1; + String event = (String) args.arg2; + Bundle extras = (Bundle) args.arg3; + mPhone.internalOnConnectionEvent(callId, event, extras); + } finally { + args.recycle(); + } + break; + } + case MSG_ON_RTT_UPGRADE_REQUEST: { + String callId = (String) msg.obj; + int requestId = msg.arg1; + mPhone.internalOnRttUpgradeRequest(callId, requestId); + break; + } + case MSG_ON_RTT_INITIATION_FAILURE: { + String callId = (String) msg.obj; + int reason = msg.arg1; + mPhone.internalOnRttInitiationFailure(callId, reason); + break; + } + default: + break; + } + } + }; + + /** Manages the binder calls so that the implementor does not need to deal with it. */ + private final class InCallServiceBinder extends IInCallService.Stub { + @Override + public void setInCallAdapter(IInCallAdapter inCallAdapter) { + mHandler.obtainMessage(MSG_SET_IN_CALL_ADAPTER, inCallAdapter).sendToTarget(); + } + + @Override + public void addCall(ParcelableCall call) { + mHandler.obtainMessage(MSG_ADD_CALL, call).sendToTarget(); + } + + @Override + public void updateCall(ParcelableCall call) { + mHandler.obtainMessage(MSG_UPDATE_CALL, call).sendToTarget(); + } + + @Override + public void setPostDial(String callId, String remaining) { + // TODO: Unused + } + + @Override + public void setPostDialWait(String callId, String remaining) { + SomeArgs args = SomeArgs.obtain(); + args.arg1 = callId; + args.arg2 = remaining; + mHandler.obtainMessage(MSG_SET_POST_DIAL_WAIT, args).sendToTarget(); + } + + @Override + public void onCallAudioStateChanged(CallAudioState callAudioState) { + mHandler.obtainMessage(MSG_ON_CALL_AUDIO_STATE_CHANGED, callAudioState).sendToTarget(); + } + + @Override + public void bringToForeground(boolean showDialpad) { + mHandler.obtainMessage(MSG_BRING_TO_FOREGROUND, showDialpad ? 1 : 0, 0).sendToTarget(); + } + + @Override + public void onCanAddCallChanged(boolean canAddCall) { + mHandler.obtainMessage(MSG_ON_CAN_ADD_CALL_CHANGED, canAddCall ? 1 : 0, 0) + .sendToTarget(); + } + + @Override + public void silenceRinger() { + mHandler.obtainMessage(MSG_SILENCE_RINGER).sendToTarget(); + } + + @Override + public void onConnectionEvent(String callId, String event, Bundle extras) { + SomeArgs args = SomeArgs.obtain(); + args.arg1 = callId; + args.arg2 = event; + args.arg3 = extras; + mHandler.obtainMessage(MSG_ON_CONNECTION_EVENT, args).sendToTarget(); + } + + @Override + public void onRttUpgradeRequest(String callId, int id) { + mHandler.obtainMessage(MSG_ON_RTT_UPGRADE_REQUEST, id, 0, callId).sendToTarget(); + } + + @Override + public void onRttInitiationFailure(String callId, int reason) { + mHandler.obtainMessage(MSG_ON_RTT_INITIATION_FAILURE, reason, 0, callId).sendToTarget(); + } + } + + private Phone.Listener mPhoneListener = new Phone.Listener() { + /** ${inheritDoc} */ + @Override + public void onAudioStateChanged(Phone phone, AudioState audioState) { + InCallService.this.onAudioStateChanged(audioState); + } + + public void onCallAudioStateChanged(Phone phone, CallAudioState callAudioState) { + InCallService.this.onCallAudioStateChanged(callAudioState); + }; + + /** ${inheritDoc} */ + @Override + public void onBringToForeground(Phone phone, boolean showDialpad) { + InCallService.this.onBringToForeground(showDialpad); + } + + /** ${inheritDoc} */ + @Override + public void onCallAdded(Phone phone, Call call) { + InCallService.this.onCallAdded(call); + } + + /** ${inheritDoc} */ + @Override + public void onCallRemoved(Phone phone, Call call) { + InCallService.this.onCallRemoved(call); + } + + /** ${inheritDoc} */ + @Override + public void onCanAddCallChanged(Phone phone, boolean canAddCall) { + InCallService.this.onCanAddCallChanged(canAddCall); + } + + /** ${inheritDoc} */ + @Override + public void onSilenceRinger(Phone phone) { + InCallService.this.onSilenceRinger(); + } + + }; + + private Phone mPhone; + + public InCallService() { + } + + @Override + public IBinder onBind(Intent intent) { + return new InCallServiceBinder(); + } + + @Override + public boolean onUnbind(Intent intent) { + if (mPhone != null) { + Phone oldPhone = mPhone; + mPhone = null; + + oldPhone.destroy(); + // destroy sets all the calls to disconnected if any live ones still exist. Therefore, + // it is important to remove the Listener *after* the call to destroy so that + // InCallService.on* callbacks are appropriately called. + oldPhone.removeListener(mPhoneListener); + + onPhoneDestroyed(oldPhone); + } + + return false; + } + + /** + * Obtain the {@code Phone} associated with this {@code InCallService}. + * + * @return The {@code Phone} object associated with this {@code InCallService}, or {@code null} + * if the {@code InCallService} is not in a state where it has an associated + * {@code Phone}. + * @hide + * @deprecated Use direct methods on InCallService instead of {@link Phone}. + */ + @SystemApi + @Deprecated + public Phone getPhone() { + return mPhone; + } + + /** + * Obtains the current list of {@code Call}s to be displayed by this in-call service. + * + * @return A list of the relevant {@code Call}s. + */ + public final List<Call> getCalls() { + return mPhone == null ? Collections.<Call>emptyList() : mPhone.getCalls(); + } + + /** + * Returns if the device can support additional calls. + * + * @return Whether the phone supports adding more calls. + */ + public final boolean canAddCall() { + return mPhone == null ? false : mPhone.canAddCall(); + } + + /** + * Obtains the current phone call audio state. + * + * @return An object encapsulating the audio state. Returns null if the service is not + * fully initialized. + * @deprecated Use {@link #getCallAudioState()} instead. + * @hide + */ + @Deprecated + public final AudioState getAudioState() { + return mPhone == null ? null : mPhone.getAudioState(); + } + + /** + * Obtains the current phone call audio state. + * + * @return An object encapsulating the audio state. Returns null if the service is not + * fully initialized. + */ + public final CallAudioState getCallAudioState() { + return mPhone == null ? null : mPhone.getCallAudioState(); + } + + /** + * Sets the microphone mute state. When this request is honored, there will be change to + * the {@link #getCallAudioState()}. + * + * @param state {@code true} if the microphone should be muted; {@code false} otherwise. + */ + public final void setMuted(boolean state) { + if (mPhone != null) { + mPhone.setMuted(state); + } + } + + /** + * Sets the audio route (speaker, bluetooth, etc...). When this request is honored, there will + * be change to the {@link #getCallAudioState()}. + * + * @param route The audio route to use. + */ + public final void setAudioRoute(int route) { + if (mPhone != null) { + mPhone.setAudioRoute(route); + } + } + + /** + * 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 + * of the {@code InCallService}. + * + * @param phone The {@code Phone} object associated with this {@code InCallService}. + * @hide + * @deprecated Use direct methods on InCallService instead of {@link Phone}. + */ + @SystemApi + @Deprecated + public void onPhoneCreated(Phone phone) { + } + + /** + * Invoked when a {@code Phone} has been destroyed. This is a signal to the in-call experience + * to stop displaying in-call information to the user. This method will be called exactly once + * in the lifetime of the {@code InCallService}, and it will always be called after a previous + * call to {@link #onPhoneCreated(Phone)}. + * + * @param phone The {@code Phone} object associated with this {@code InCallService}. + * @hide + * @deprecated Use direct methods on InCallService instead of {@link Phone}. + */ + @SystemApi + @Deprecated + public void onPhoneDestroyed(Phone phone) { + } + + /** + * Called when the audio state changes. + * + * @param audioState The new {@link AudioState}. + * @deprecated Use {@link #onCallAudioStateChanged(CallAudioState) instead}. + * @hide + */ + @Deprecated + public void onAudioStateChanged(AudioState audioState) { + } + + /** + * Called when the audio state changes. + * + * @param audioState The new {@link CallAudioState}. + */ + public void onCallAudioStateChanged(CallAudioState audioState) { + } + + /** + * 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. + * + * @param showDialpad If true, put up the dialpad when the screen is shown. + */ + public void onBringToForeground(boolean showDialpad) { + } + + /** + * Called when a {@code Call} has been added to this in-call session. The in-call user + * experience should add necessary state listeners to the specified {@code Call} and + * immediately start to show the user information about the existence + * and nature of this {@code Call}. Subsequent invocations of {@link #getCalls()} will + * include this {@code Call}. + * + * @param call A newly added {@code Call}. + */ + public void onCallAdded(Call call) { + } + + /** + * Called when a {@code Call} has been removed from this in-call session. The in-call user + * experience should remove any state listeners from the specified {@code Call} and + * immediately stop displaying any information about this {@code Call}. + * Subsequent invocations of {@link #getCalls()} will no longer include this {@code Call}. + * + * @param call A newly removed {@code Call}. + */ + public void onCallRemoved(Call call) { + } + + /** + * Called when the ability to add more calls changes. If the phone cannot + * support more calls then {@code canAddCall} is set to {@code false}. If it can, then it + * is set to {@code true}. This can be used to control the visibility of UI to add more calls. + * + * @param canAddCall Indicates whether an additional call can be added. + */ + public void onCanAddCallChanged(boolean canAddCall) { + } + + /** + * Called to silence the ringer if a ringing call exists. + */ + public void onSilenceRinger() { + } + + /** + * Unused; to handle connection events issued by a {@link ConnectionService}, implement the + * {@link android.telecom.Call.Callback#onConnectionEvent(Call, String, Bundle)} callback. + * <p> + * See {@link Connection#sendConnectionEvent(String, Bundle)}. + * + * @param call The call the event is associated with. + * @param event The event. + * @param extras Any associated extras. + */ + public void onConnectionEvent(Call call, String event, Bundle extras) { + } + + /** + * Used to issue commands to the {@link Connection.VideoProvider} associated with a + * {@link Call}. + */ + public static abstract class VideoCall { + + /** @hide */ + public abstract void destroy(); + + /** + * Registers a callback to receive commands and state changes for video calls. + * + * @param callback The video call callback. + */ + public abstract void registerCallback(VideoCall.Callback callback); + + /** + * Registers a callback to receive commands and state changes for video calls. + * + * @param callback The video call callback. + * @param handler A handler which commands and status changes will be delivered to. + */ + public abstract void registerCallback(VideoCall.Callback callback, Handler handler); + + /** + * Clears the video call callback set via {@link #registerCallback}. + * + * @param callback The video call callback to clear. + */ + public abstract void unregisterCallback(VideoCall.Callback callback); + + /** + * Sets the camera to be used for the outgoing video. + * <p> + * Handled by {@link Connection.VideoProvider#onSetCamera(String)}. + * + * @param cameraId The id of the camera (use ids as reported by + * {@link CameraManager#getCameraIdList()}). + */ + public abstract void setCamera(String cameraId); + + /** + * Sets the surface to be used for displaying a preview of what the user's camera is + * currently capturing. When video transmission is enabled, this is the video signal which + * is sent to the remote device. + * <p> + * Handled by {@link Connection.VideoProvider#onSetPreviewSurface(Surface)}. + * + * @param surface The {@link Surface}. + */ + public abstract void setPreviewSurface(Surface surface); + + /** + * Sets the surface to be used for displaying the video received from the remote device. + * <p> + * Handled by {@link Connection.VideoProvider#onSetDisplaySurface(Surface)}. + * + * @param surface The {@link Surface}. + */ + public abstract void setDisplaySurface(Surface surface); + + /** + * Sets the device orientation, in degrees. Assumes that a standard portrait orientation of + * the device is 0 degrees. + * <p> + * Handled by {@link Connection.VideoProvider#onSetDeviceOrientation(int)}. + * + * @param rotation The device orientation, in degrees. + */ + public abstract void setDeviceOrientation(int rotation); + + /** + * Sets camera zoom ratio. + * <p> + * Handled by {@link Connection.VideoProvider#onSetZoom(float)}. + * + * @param value The camera zoom ratio. + */ + public abstract void setZoom(float value); + + /** + * Issues a request to modify the properties of the current video session. + * <p> + * Example scenarios include: requesting an audio-only call to be upgraded to a + * bi-directional video call, turning on or off the user's camera, sending a pause signal + * when the {@link InCallService} is no longer the foreground application. + * <p> + * Handled by + * {@link Connection.VideoProvider#onSendSessionModifyRequest(VideoProfile, VideoProfile)}. + * + * @param requestProfile The requested call video properties. + */ + public abstract void sendSessionModifyRequest(VideoProfile requestProfile); + + /** + * Provides a response to a request to change the current call video session + * properties. This should be called in response to a request the {@link InCallService} has + * received via {@link VideoCall.Callback#onSessionModifyRequestReceived}. + * <p> + * Handled by + * {@link Connection.VideoProvider#onSendSessionModifyResponse(VideoProfile)}. + * + * @param responseProfile The response call video properties. + */ + public abstract void sendSessionModifyResponse(VideoProfile responseProfile); + + /** + * Issues a request to the {@link Connection.VideoProvider} to retrieve the capabilities + * of the current camera. The current camera is selected using + * {@link VideoCall#setCamera(String)}. + * <p> + * Camera capabilities are reported to the caller via + * {@link VideoCall.Callback#onCameraCapabilitiesChanged(VideoProfile.CameraCapabilities)}. + * <p> + * Handled by {@link Connection.VideoProvider#onRequestCameraCapabilities()}. + */ + public abstract void requestCameraCapabilities(); + + /** + * Issues a request to the {@link Connection.VideoProvider} to retrieve the cumulative data + * usage for the video component of the current call (in bytes). Data usage is reported + * to the caller via {@link VideoCall.Callback#onCallDataUsageChanged}. + * <p> + * Handled by {@link Connection.VideoProvider#onRequestConnectionDataUsage()}. + */ + public abstract void requestCallDataUsage(); + + /** + * Provides the {@link Connection.VideoProvider} with the {@link Uri} of an image to be + * displayed to the peer device when the video signal is paused. + * <p> + * Handled by {@link Connection.VideoProvider#onSetPauseImage(Uri)}. + * + * @param uri URI of image to display. + */ + public abstract void setPauseImage(Uri uri); + + /** + * The {@link InCallService} extends this class to provide a means of receiving callbacks + * from the {@link Connection.VideoProvider}. + * <p> + * When the {@link InCallService} receives the + * {@link Call.Callback#onVideoCallChanged(Call, VideoCall)} callback, it should create an + * instance its {@link VideoCall.Callback} implementation and set it on the + * {@link VideoCall} using {@link VideoCall#registerCallback(Callback)}. + */ + public static abstract class Callback { + /** + * Called when the {@link Connection.VideoProvider} receives a session modification + * request from the peer device. + * <p> + * The {@link InCallService} may potentially prompt the user to confirm whether they + * wish to accept the request, or decide to automatically accept the request. In either + * case the {@link InCallService} should call + * {@link VideoCall#sendSessionModifyResponse(VideoProfile)} to indicate the video + * profile agreed upon. + * <p> + * Callback originates from + * {@link Connection.VideoProvider#receiveSessionModifyRequest(VideoProfile)}. + * + * @param videoProfile The requested video profile. + */ + public abstract void onSessionModifyRequestReceived(VideoProfile videoProfile); + + /** + * Called when the {@link Connection.VideoProvider} receives a response to a session + * modification request previously sent to the peer device. + * <p> + * The new video state should not be considered active by the {@link InCallService} + * until the {@link Call} video state changes (the + * {@link Call.Callback#onDetailsChanged(Call, Call.Details)} callback is triggered + * when the video state changes). + * <p> + * Callback originates from + * {@link Connection.VideoProvider#receiveSessionModifyResponse(int, VideoProfile, + * VideoProfile)}. + * + * @param status Status of the session modify request. Valid values are + * {@link Connection.VideoProvider#SESSION_MODIFY_REQUEST_SUCCESS}, + * {@link Connection.VideoProvider#SESSION_MODIFY_REQUEST_FAIL}, + * {@link Connection.VideoProvider#SESSION_MODIFY_REQUEST_INVALID}, + * {@link Connection.VideoProvider#SESSION_MODIFY_REQUEST_TIMED_OUT}, + * {@link Connection.VideoProvider#SESSION_MODIFY_REQUEST_REJECTED_BY_REMOTE}. + * @param requestedProfile The original request which was sent to the peer device. + * @param responseProfile The actual profile changes made by the peer device. + */ + public abstract void onSessionModifyResponseReceived(int status, + VideoProfile requestedProfile, VideoProfile responseProfile); + + /** + * Handles events related to the current video session which the {@link InCallService} + * may wish to handle. These are separate from requested changes to the session due to + * the underlying protocol or connection. + * <p> + * Callback originates from + * {@link Connection.VideoProvider#handleCallSessionEvent(int)}. + * + * @param event The event. Valid values are: + * {@link Connection.VideoProvider#SESSION_EVENT_RX_PAUSE}, + * {@link Connection.VideoProvider#SESSION_EVENT_RX_RESUME}, + * {@link Connection.VideoProvider#SESSION_EVENT_TX_START}, + * {@link Connection.VideoProvider#SESSION_EVENT_TX_STOP}, + * {@link Connection.VideoProvider#SESSION_EVENT_CAMERA_FAILURE}, + * {@link Connection.VideoProvider#SESSION_EVENT_CAMERA_READY}, + * {@link Connection.VideoProvider#SESSION_EVENT_CAMERA_PERMISSION_ERROR}. + */ + public abstract void onCallSessionEvent(int event); + + /** + * Handles a change to the video dimensions from the peer device. This could happen if, + * for example, the peer changes orientation of their device, or switches cameras. + * <p> + * Callback originates from + * {@link Connection.VideoProvider#changePeerDimensions(int, int)}. + * + * @param width The updated peer video width. + * @param height The updated peer video height. + */ + public abstract void onPeerDimensionsChanged(int width, int height); + + /** + * Handles a change to the video quality. + * <p> + * Callback originates from {@link Connection.VideoProvider#changeVideoQuality(int)}. + * + * @param videoQuality The updated peer video quality. Valid values: + * {@link VideoProfile#QUALITY_HIGH}, + * {@link VideoProfile#QUALITY_MEDIUM}, + * {@link VideoProfile#QUALITY_LOW}, + * {@link VideoProfile#QUALITY_DEFAULT}. + */ + public abstract void onVideoQualityChanged(int videoQuality); + + /** + * Handles an update to the total data used for the current video session. + * <p> + * Used by the {@link Connection.VideoProvider} in response to + * {@link VideoCall#requestCallDataUsage()}. May also be called periodically by the + * {@link Connection.VideoProvider}. + * <p> + * Callback originates from {@link Connection.VideoProvider#setCallDataUsage(long)}. + * + * @param dataUsage The updated data usage (in bytes). + */ + public abstract void onCallDataUsageChanged(long dataUsage); + + /** + * Handles a change in the capabilities of the currently selected camera. + * <p> + * Used by the {@link Connection.VideoProvider} in response to + * {@link VideoCall#requestCameraCapabilities()}. The {@link Connection.VideoProvider} + * may also report the camera capabilities after a call to + * {@link VideoCall#setCamera(String)}. + * <p> + * Callback originates from + * {@link Connection.VideoProvider#changeCameraCapabilities( + * VideoProfile.CameraCapabilities)}. + * + * @param cameraCapabilities The changed camera capabilities. + */ + public abstract void onCameraCapabilitiesChanged( + VideoProfile.CameraCapabilities cameraCapabilities); + } + } +} diff --git a/android/telecom/Log.java b/android/telecom/Log.java new file mode 100644 index 00000000..3361b5b6 --- /dev/null +++ b/android/telecom/Log.java @@ -0,0 +1,488 @@ +/* + * Copyright 2014, 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.content.Context; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Build; +import android.telecom.Logging.EventManager; +import android.telecom.Logging.Session; +import android.telecom.Logging.SessionManager; +import android.telephony.PhoneNumberUtils; +import android.text.TextUtils; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.IndentingPrintWriter; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.IllegalFormatException; +import java.util.Locale; + +/** + * Manages logging for the entire module. + * + * @hide + */ +public class Log { + + private static final long EXTENDED_LOGGING_DURATION_MILLIS = 60000 * 30; // 30 minutes + + private static final int EVENTS_TO_CACHE = 10; + private static final int EVENTS_TO_CACHE_DEBUG = 20; + + // Generic tag for all Telecom logging + @VisibleForTesting + public static String TAG = "TelecomFramework"; + public static boolean DEBUG = isLoggable(android.util.Log.DEBUG); + public static boolean INFO = isLoggable(android.util.Log.INFO); + public static boolean VERBOSE = isLoggable(android.util.Log.VERBOSE); + public static boolean WARN = isLoggable(android.util.Log.WARN); + public static boolean ERROR = isLoggable(android.util.Log.ERROR); + + private static final boolean FORCE_LOGGING = false; /* STOP SHIP if true */ + private static final boolean USER_BUILD = Build.IS_USER; + + // Used to synchronize singleton logging lazy initialization + private static final Object sSingletonSync = new Object(); + private static EventManager sEventManager; + private static SessionManager sSessionManager; + + /** + * Tracks whether user-activated extended logging is enabled. + */ + private static boolean sIsUserExtendedLoggingEnabled = false; + + /** + * The time when user-activated extended logging should be ended. Used to determine when + * extended logging should automatically be disabled. + */ + private static long sUserExtendedLoggingStopTime = 0; + + private Log() { + } + + public static void d(String prefix, String format, Object... args) { + if (sIsUserExtendedLoggingEnabled) { + maybeDisableLogging(); + android.util.Slog.i(TAG, buildMessage(prefix, format, args)); + } else if (DEBUG) { + android.util.Slog.d(TAG, buildMessage(prefix, format, args)); + } + } + + public static void d(Object objectPrefix, String format, Object... args) { + if (sIsUserExtendedLoggingEnabled) { + maybeDisableLogging(); + android.util.Slog.i(TAG, buildMessage(getPrefixFromObject(objectPrefix), format, args)); + } else if (DEBUG) { + android.util.Slog.d(TAG, buildMessage(getPrefixFromObject(objectPrefix), format, args)); + } + } + + public static void i(String prefix, String format, Object... args) { + if (INFO) { + android.util.Slog.i(TAG, buildMessage(prefix, format, args)); + } + } + + public static void i(Object objectPrefix, String format, Object... args) { + if (INFO) { + android.util.Slog.i(TAG, buildMessage(getPrefixFromObject(objectPrefix), format, args)); + } + } + + public static void v(String prefix, String format, Object... args) { + if (sIsUserExtendedLoggingEnabled) { + maybeDisableLogging(); + android.util.Slog.i(TAG, buildMessage(prefix, format, args)); + } else if (VERBOSE) { + android.util.Slog.v(TAG, buildMessage(prefix, format, args)); + } + } + + public static void v(Object objectPrefix, String format, Object... args) { + if (sIsUserExtendedLoggingEnabled) { + maybeDisableLogging(); + android.util.Slog.i(TAG, buildMessage(getPrefixFromObject(objectPrefix), format, args)); + } else if (VERBOSE) { + android.util.Slog.v(TAG, buildMessage(getPrefixFromObject(objectPrefix), format, args)); + } + } + + public static void w(String prefix, String format, Object... args) { + if (WARN) { + android.util.Slog.w(TAG, buildMessage(prefix, format, args)); + } + } + + public static void w(Object objectPrefix, String format, Object... args) { + if (WARN) { + android.util.Slog.w(TAG, buildMessage(getPrefixFromObject(objectPrefix), format, args)); + } + } + + public static void e(String prefix, Throwable tr, String format, Object... args) { + if (ERROR) { + android.util.Slog.e(TAG, buildMessage(prefix, format, args), tr); + } + } + + public static void e(Object objectPrefix, Throwable tr, String format, Object... args) { + if (ERROR) { + android.util.Slog.e(TAG, buildMessage(getPrefixFromObject(objectPrefix), format, args), + tr); + } + } + + public static void wtf(String prefix, Throwable tr, String format, Object... args) { + android.util.Slog.wtf(TAG, buildMessage(prefix, format, args), tr); + } + + public static void wtf(Object objectPrefix, Throwable tr, String format, Object... args) { + android.util.Slog.wtf(TAG, buildMessage(getPrefixFromObject(objectPrefix), format, args), + tr); + } + + public static void wtf(String prefix, String format, Object... args) { + String msg = buildMessage(prefix, format, args); + android.util.Slog.wtf(TAG, msg, new IllegalStateException(msg)); + } + + public static void wtf(Object objectPrefix, String format, Object... args) { + String msg = buildMessage(getPrefixFromObject(objectPrefix), format, args); + android.util.Slog.wtf(TAG, msg, new IllegalStateException(msg)); + } + + /** + * The ease of use methods below only act mostly as proxies to the Session and Event Loggers. + * They also control the lazy loaders of the singleton instances, which will never be loaded if + * the proxy methods aren't used. + * + * Please see each method's documentation inside of their respective implementations in the + * loggers. + */ + + public static void setSessionContext(Context context) { + getSessionManager().setContext(context); + } + + public static void startSession(String shortMethodName) { + getSessionManager().startSession(shortMethodName, null); + } + + public static void startSession(Session.Info info, String shortMethodName) { + getSessionManager().startSession(info, shortMethodName, null); + } + + public static void startSession(String shortMethodName, String callerIdentification) { + getSessionManager().startSession(shortMethodName, callerIdentification); + } + + public static void startSession(Session.Info info, String shortMethodName, + String callerIdentification) { + getSessionManager().startSession(info, shortMethodName, callerIdentification); + } + + public static Session createSubsession() { + return getSessionManager().createSubsession(); + } + + public static Session.Info getExternalSession() { + return getSessionManager().getExternalSession(); + } + + public static void cancelSubsession(Session subsession) { + getSessionManager().cancelSubsession(subsession); + } + + public static void continueSession(Session subsession, String shortMethodName) { + getSessionManager().continueSession(subsession, shortMethodName); + } + + public static void endSession() { + getSessionManager().endSession(); + } + + public static void registerSessionListener(SessionManager.ISessionListener l) { + getSessionManager().registerSessionListener(l); + } + + public static String getSessionId() { + // If the Session logger has not been initialized, then there have been no sessions logged. + // Don't load it now! + synchronized (sSingletonSync) { + if (sSessionManager != null) { + return getSessionManager().getSessionId(); + } else { + return ""; + } + } + } + + public static void addEvent(EventManager.Loggable recordEntry, String event) { + getEventManager().event(recordEntry, event, null); + } + + public static void addEvent(EventManager.Loggable recordEntry, String event, Object data) { + getEventManager().event(recordEntry, event, data); + } + + public static void addEvent(EventManager.Loggable recordEntry, String event, String format, + Object... args) { + getEventManager().event(recordEntry, event, format, args); + } + + public static void registerEventListener(EventManager.EventListener e) { + getEventManager().registerEventListener(e); + } + + public static void addRequestResponsePair(EventManager.TimedEventPair p) { + getEventManager().addRequestResponsePair(p); + } + + public static void dumpEvents(IndentingPrintWriter pw) { + // If the Events logger has not been initialized, then there have been no events logged. + // Don't load it now! + synchronized (sSingletonSync) { + if (sEventManager != null) { + getEventManager().dumpEvents(pw); + } else { + pw.println("No Historical Events Logged."); + } + } + } + + /** + * Dumps the events in a timeline format. + * @param pw The {@link IndentingPrintWriter} to write to. + * @hide + */ + public static void dumpEventsTimeline(IndentingPrintWriter pw) { + // If the Events logger has not been initialized, then there have been no events logged. + // Don't load it now! + synchronized (sSingletonSync) { + if (sEventManager != null) { + getEventManager().dumpEventsTimeline(pw); + } else { + pw.println("No Historical Events Logged."); + } + } + } + + /** + * Enable or disable extended telecom logging. + * + * @param isExtendedLoggingEnabled {@code true} if extended logging should be enabled, + * {@code false} if it should be disabled. + */ + public static void setIsExtendedLoggingEnabled(boolean isExtendedLoggingEnabled) { + // If the state hasn't changed, bail early. + if (sIsUserExtendedLoggingEnabled == isExtendedLoggingEnabled) { + return; + } + + if (sEventManager != null) { + sEventManager.changeEventCacheSize(isExtendedLoggingEnabled ? + EVENTS_TO_CACHE_DEBUG : EVENTS_TO_CACHE); + } + + sIsUserExtendedLoggingEnabled = isExtendedLoggingEnabled; + if (sIsUserExtendedLoggingEnabled) { + sUserExtendedLoggingStopTime = System.currentTimeMillis() + + EXTENDED_LOGGING_DURATION_MILLIS; + } else { + sUserExtendedLoggingStopTime = 0; + } + } + + private static EventManager getEventManager() { + // Checking for null again outside of synchronization because we only need to synchronize + // during the lazy loading of the events logger. We don't need to synchronize elsewhere. + if (sEventManager == null) { + synchronized (sSingletonSync) { + if (sEventManager == null) { + sEventManager = new EventManager(Log::getSessionId); + return sEventManager; + } + } + } + return sEventManager; + } + + @VisibleForTesting + public static SessionManager getSessionManager() { + // Checking for null again outside of synchronization because we only need to synchronize + // during the lazy loading of the session logger. We don't need to synchronize elsewhere. + if (sSessionManager == null) { + synchronized (sSingletonSync) { + if (sSessionManager == null) { + sSessionManager = new SessionManager(); + return sSessionManager; + } + } + } + return sSessionManager; + } + + private static MessageDigest sMessageDigest; + + public static void initMd5Sum() { + new AsyncTask<Void, Void, Void>() { + @Override + public Void doInBackground(Void... args) { + MessageDigest md; + try { + md = MessageDigest.getInstance("SHA-1"); + } catch (NoSuchAlgorithmException e) { + md = null; + } + sMessageDigest = md; + return null; + } + }.execute(); + } + + public static void setTag(String tag) { + TAG = tag; + DEBUG = isLoggable(android.util.Log.DEBUG); + INFO = isLoggable(android.util.Log.INFO); + VERBOSE = isLoggable(android.util.Log.VERBOSE); + WARN = isLoggable(android.util.Log.WARN); + ERROR = isLoggable(android.util.Log.ERROR); + } + + /** + * If user enabled extended logging is enabled and the time limit has passed, disables the + * extended logging. + */ + private static void maybeDisableLogging() { + if (!sIsUserExtendedLoggingEnabled) { + return; + } + + if (sUserExtendedLoggingStopTime < System.currentTimeMillis()) { + sUserExtendedLoggingStopTime = 0; + sIsUserExtendedLoggingEnabled = false; + } + } + + public static boolean isLoggable(int level) { + return FORCE_LOGGING || android.util.Log.isLoggable(TAG, level); + } + + public static String piiHandle(Object pii) { + if (pii == null || VERBOSE) { + return String.valueOf(pii); + } + + StringBuilder sb = new StringBuilder(); + if (pii instanceof Uri) { + Uri uri = (Uri) pii; + String scheme = uri.getScheme(); + + if (!TextUtils.isEmpty(scheme)) { + sb.append(scheme).append(":"); + } + + String textToObfuscate = uri.getSchemeSpecificPart(); + if (PhoneAccount.SCHEME_TEL.equals(scheme)) { + for (int i = 0; i < textToObfuscate.length(); i++) { + char c = textToObfuscate.charAt(i); + sb.append(PhoneNumberUtils.isDialable(c) ? "*" : c); + } + } else if (PhoneAccount.SCHEME_SIP.equals(scheme)) { + for (int i = 0; i < textToObfuscate.length(); i++) { + char c = textToObfuscate.charAt(i); + if (c != '@' && c != '.') { + c = '*'; + } + sb.append(c); + } + } else { + sb.append(pii(pii)); + } + } + + return sb.toString(); + } + + /** + * Redact personally identifiable information for production users. + * If we are running in verbose mode, return the original string, + * and return "****" if we are running on the user build, otherwise + * return a SHA-1 hash of the input string. + */ + public static String pii(Object pii) { + if (pii == null || VERBOSE) { + return String.valueOf(pii); + } + return "[" + secureHash(String.valueOf(pii).getBytes()) + "]"; + } + + private static String secureHash(byte[] input) { + // Refrain from logging user personal information in user build. + if (USER_BUILD) { + return "****"; + } + + if (sMessageDigest != null) { + sMessageDigest.reset(); + sMessageDigest.update(input); + byte[] result = sMessageDigest.digest(); + return encodeHex(result); + } else { + return "Uninitialized SHA1"; + } + } + + private static String encodeHex(byte[] bytes) { + StringBuffer hex = new StringBuffer(bytes.length * 2); + + for (int i = 0; i < bytes.length; i++) { + int byteIntValue = bytes[i] & 0xff; + if (byteIntValue < 0x10) { + hex.append("0"); + } + hex.append(Integer.toString(byteIntValue, 16)); + } + + return hex.toString(); + } + + private static String getPrefixFromObject(Object obj) { + return obj == null ? "<null>" : obj.getClass().getSimpleName(); + } + + private static String buildMessage(String prefix, String format, Object... args) { + // Incorporate thread ID and calling method into prefix + String sessionName = getSessionId(); + String sessionPostfix = TextUtils.isEmpty(sessionName) ? "" : ": " + sessionName; + + String msg; + try { + msg = (args == null || args.length == 0) ? format + : String.format(Locale.US, format, args); + } catch (IllegalFormatException ife) { + e(TAG, ife, "Log: IllegalFormatException: formatString='%s' numArgs=%d", format, + args.length); + msg = format + " (An error occurred while formatting the message.)"; + } + return String.format(Locale.US, "%s: %s%s", prefix, msg, sessionPostfix); + } +} diff --git a/android/telecom/Logging/EventManager.java b/android/telecom/Logging/EventManager.java new file mode 100644 index 00000000..4fc33853 --- /dev/null +++ b/android/telecom/Logging/EventManager.java @@ -0,0 +1,415 @@ +/* + * Copyright (C) 2016 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.Logging; + +import android.annotation.NonNull; +import android.telecom.Log; +import android.text.TextUtils; +import android.util.Pair; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.IndentingPrintWriter; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.HashMap; +import java.util.IllegalFormatException; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.TimeZone; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.stream.Collectors; + +/** + * A utility class that provides the ability to define Events that a subsystem deems important, and + * then relate those events to other events so that information can be extracted. For example, a + * START and FINISH event can be defined and when a START and then FINISH occurs in a sequence, the + * time it took to complete that sequence can be saved to be retrieved later. + * @hide + */ + +public class EventManager { + + public static final String TAG = "Logging.Events"; + @VisibleForTesting + public static final int DEFAULT_EVENTS_TO_CACHE = 10; // Arbitrarily chosen. + private final DateFormat sDateFormat = new SimpleDateFormat("HH:mm:ss.SSS"); + + public interface Loggable { + /** + * @return a unique String ID that will allow the Event to be recognized later in the logs. + */ + String getId(); + + /** + * @return Formatted information about the state that will be printed out later in the logs. + */ + String getDescription(); + } + + private final Map<Loggable, EventRecord> mCallEventRecordMap = new HashMap<>(); + private LinkedBlockingQueue<EventRecord> mEventRecords = + new LinkedBlockingQueue<>(DEFAULT_EVENTS_TO_CACHE); + + private List<EventListener> mEventListeners = new ArrayList<>(); + + public interface EventListener { + /** + * Notifies the implementation of this method that a new event record has been added. + * @param eventRecord Reference to the recently added EventRecord + */ + void eventRecordAdded(EventRecord eventRecord); + } + + private SessionManager.ISessionIdQueryHandler mSessionIdHandler; + /** + * Maps from request events to a list of possible response events. Used to track + * end-to-end timing for critical user-facing operations in Telecom. + */ + private final Map<String, List<TimedEventPair>> requestResponsePairs = new HashMap<>(); + + private static final Object mSync = new Object(); + + /** + * Stores the various events. + * Also stores all request-response pairs amongst the events. + */ + public static class TimedEventPair { + private static final long DEFAULT_TIMEOUT = 3000L; + + String mRequest; + String mResponse; + String mName; + long mTimeoutMillis = DEFAULT_TIMEOUT; + + public TimedEventPair(String request, String response, String name) { + this.mRequest = request; + this.mResponse = response; + this.mName = name; + } + + public TimedEventPair(String request, String response, String name, long timeoutMillis) { + this.mRequest = request; + this.mResponse = response; + this.mName = name; + this.mTimeoutMillis = timeoutMillis; + } + } + + public void addRequestResponsePair(TimedEventPair p) { + if (requestResponsePairs.containsKey(p.mRequest)) { + requestResponsePairs.get(p.mRequest).add(p); + } else { + ArrayList<TimedEventPair> responses = new ArrayList<>(); + responses.add(p); + requestResponsePairs.put(p.mRequest, responses); + } + } + + public static class Event { + public String eventId; + public String sessionId; + public long time; + public Object data; + + public Event(String eventId, String sessionId, long time, Object data) { + this.eventId = eventId; + this.sessionId = sessionId; + this.time = time; + this.data = data; + } + } + + public class EventRecord { + public class EventTiming extends TimedEvent<String> { + public String name; + public long time; + + public EventTiming(String name, long time) { + this.name = name; + this.time = time; + } + + public String getKey() { + return name; + } + + public long getTime() { + return time; + } + } + + private class PendingResponse { + String requestEventId; + long requestEventTimeMillis; + long timeoutMillis; + String name; + + public PendingResponse(String requestEventId, long requestEventTimeMillis, + long timeoutMillis, String name) { + this.requestEventId = requestEventId; + this.requestEventTimeMillis = requestEventTimeMillis; + this.timeoutMillis = timeoutMillis; + this.name = name; + } + } + + private final List<Event> mEvents = new LinkedList<>(); + private final Loggable mRecordEntry; + + public EventRecord(Loggable recordEntry) { + mRecordEntry = recordEntry; + } + + public Loggable getRecordEntry() { + return mRecordEntry; + } + + public void addEvent(String event, String sessionId, Object data) { + mEvents.add(new Event(event, sessionId, System.currentTimeMillis(), data)); + Log.i("Event", "RecordEntry %s: %s, %s", mRecordEntry.getId(), event, data); + } + + public List<Event> getEvents() { + return mEvents; + } + + public List<EventTiming> extractEventTimings() { + if (mEvents == null) { + return Collections.emptyList(); + } + + LinkedList<EventTiming> result = new LinkedList<>(); + Map<String, PendingResponse> pendingResponses = new HashMap<>(); + for (Event event : mEvents) { + if (requestResponsePairs.containsKey(event.eventId)) { + // This event expects a response, so add that expected response to the maps + // of pending events. + for (EventManager.TimedEventPair p : requestResponsePairs.get(event.eventId)) { + pendingResponses.put(p.mResponse, new PendingResponse(event.eventId, + event.time, p.mTimeoutMillis, p.mName)); + } + } + + PendingResponse pendingResponse = pendingResponses.remove(event.eventId); + if (pendingResponse != null) { + long elapsedTime = event.time - pendingResponse.requestEventTimeMillis; + if (elapsedTime < pendingResponse.timeoutMillis) { + result.add(new EventTiming(pendingResponse.name, elapsedTime)); + } + } + } + + return result; + } + + public void dump(IndentingPrintWriter pw) { + pw.print(mRecordEntry.getDescription()); + + pw.increaseIndent(); + for (Event event : mEvents) { + pw.print(sDateFormat.format(new Date(event.time))); + pw.print(" - "); + pw.print(event.eventId); + if (event.data != null) { + pw.print(" ("); + Object data = event.data; + + if (data instanceof Loggable) { + // If the data is another Loggable, then change the data to the + // Entry's Event ID instead. + EventRecord record = mCallEventRecordMap.get(data); + if (record != null) { + data = "RecordEntry " + record.mRecordEntry.getId(); + } + } + + pw.print(data); + pw.print(")"); + } + if (!TextUtils.isEmpty(event.sessionId)) { + pw.print(":"); + pw.print(event.sessionId); + } + pw.println(); + } + + pw.println("Timings (average for this call, milliseconds):"); + pw.increaseIndent(); + Map<String, Double> avgEventTimings = EventTiming.averageTimings(extractEventTimings()); + List<String> eventNames = new ArrayList<>(avgEventTimings.keySet()); + Collections.sort(eventNames); + for (String eventName : eventNames) { + pw.printf("%s: %.2f\n", eventName, avgEventTimings.get(eventName)); + } + pw.decreaseIndent(); + pw.decreaseIndent(); + } + } + + public EventManager(@NonNull SessionManager.ISessionIdQueryHandler l) { + mSessionIdHandler = l; + sDateFormat.setTimeZone(TimeZone.getDefault()); + } + + public void event(Loggable recordEntry, String event, Object data) { + String currentSessionID = mSessionIdHandler.getSessionId(); + + if (recordEntry == null) { + Log.i(TAG, "Non-call EVENT: %s, %s", event, data); + return; + } + synchronized (mEventRecords) { + if (!mCallEventRecordMap.containsKey(recordEntry)) { + EventRecord newRecord = new EventRecord(recordEntry); + addEventRecord(newRecord); + } + + EventRecord record = mCallEventRecordMap.get(recordEntry); + record.addEvent(event, currentSessionID, data); + } + } + + public void event(Loggable recordEntry, String event, String format, Object... args) { + String msg; + try { + msg = (args == null || args.length == 0) ? format + : String.format(Locale.US, format, args); + } catch (IllegalFormatException ife) { + Log.e(this, ife, "IllegalFormatException: formatString='%s' numArgs=%d", format, + args.length); + msg = format + " (An error occurred while formatting the message.)"; + } + + event(recordEntry, event, msg); + } + + public void dumpEvents(IndentingPrintWriter pw) { + pw.println("Historical Events:"); + pw.increaseIndent(); + for (EventRecord eventRecord : mEventRecords) { + eventRecord.dump(pw); + } + pw.decreaseIndent(); + } + + /** + * Dumps events in a timeline format. + * @param pw The {@link IndentingPrintWriter} to output the timeline to. + * @hide + */ + public void dumpEventsTimeline(IndentingPrintWriter pw) { + pw.println("Historical Events (sorted by time):"); + + // Flatten event records out for sorting. + List<Pair<Loggable, Event>> events = new ArrayList<>(); + for (EventRecord er : mEventRecords) { + for (Event ev : er.getEvents()) { + events.add(new Pair<>(er.getRecordEntry(), ev)); + } + } + + // Sort by event time. + Comparator<Pair<Loggable, Event>> byEventTime = (e1, e2) -> { + return Long.compare(e1.second.time, e2.second.time); + }; + events.sort(byEventTime); + + pw.increaseIndent(); + for (Pair<Loggable, Event> event : events) { + pw.print(sDateFormat.format(new Date(event.second.time))); + pw.print(","); + pw.print(event.first.getId()); + pw.print(","); + pw.print(event.second.eventId); + pw.print(","); + pw.println(event.second.data); + } + pw.decreaseIndent(); + } + + public void changeEventCacheSize(int newSize) { + + // Resize the event queue. + LinkedBlockingQueue<EventRecord> oldEventLog = mEventRecords; + mEventRecords = new LinkedBlockingQueue<>(newSize); + mCallEventRecordMap.clear(); + + oldEventLog.forEach((newRecord -> { + Loggable recordEntry = newRecord.getRecordEntry(); + // Copy the existing queue into the new one. + // First remove the oldest entry if no new ones exist. + if (mEventRecords.remainingCapacity() == 0) { + EventRecord record = mEventRecords.poll(); + if (record != null) { + mCallEventRecordMap.remove(record.getRecordEntry()); + } + } + + // Now add a new entry + mEventRecords.add(newRecord); + mCallEventRecordMap.put(recordEntry, newRecord); + + // Don't worry about notifying mEventListeners, since we are just resizing the records. + })); + } + + public void registerEventListener(EventListener e) { + if (e != null) { + synchronized (mSync) { + mEventListeners.add(e); + } + } + } + + @VisibleForTesting + public LinkedBlockingQueue<EventRecord> getEventRecords() { + return mEventRecords; + } + + @VisibleForTesting + public Map<Loggable, EventRecord> getCallEventRecordMap() { + return mCallEventRecordMap; + } + + private void addEventRecord(EventRecord newRecord) { + Loggable recordEntry = newRecord.getRecordEntry(); + + // First remove the oldest entry if no new ones exist. + if (mEventRecords.remainingCapacity() == 0) { + EventRecord record = mEventRecords.poll(); + if (record != null) { + mCallEventRecordMap.remove(record.getRecordEntry()); + } + } + + // Now add a new entry + mEventRecords.add(newRecord); + mCallEventRecordMap.put(recordEntry, newRecord); + synchronized (mSync) { + for (EventListener l : mEventListeners) { + l.eventRecordAdded(newRecord); + } + } + } +} diff --git a/android/telecom/Logging/Runnable.java b/android/telecom/Logging/Runnable.java new file mode 100644 index 00000000..6e810538 --- /dev/null +++ b/android/telecom/Logging/Runnable.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2016 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.Logging; + +import android.telecom.Log; + +/** + * Encapsulates session logging in a Runnable to reduce code duplication when continuing subsessions + * in a handler/thread. + * @hide + */ +public abstract class Runnable { + + private Session mSubsession; + private final String mSubsessionName; + private final Object mLock; + private final java.lang.Runnable mRunnable = new java.lang.Runnable() { + @Override + public void run() { + synchronized (mLock) { + try { + Log.continueSession(mSubsession, mSubsessionName); + loggedRun(); + } finally { + if (mSubsession != null) { + Log.endSession(); + mSubsession = null; + } + } + } + } + }; + + /** + * Creates a new Telecom Runnable that incorporates Session Logging into it. Useful for carrying + * Logging Sessions through different threads as well as through handlers. + * @param subsessionName The name that will be used in the Logs to mark this Session + * @param lock The synchronization lock that will be used to lock loggedRun(). + */ + public Runnable(String subsessionName, Object lock) { + if (lock == null) { + mLock = new Object(); + } else { + mLock = lock; + } + mSubsessionName = subsessionName; + } + + /** + * Return the runnable that will be canceled in the handler queue. + * @return Runnable object to cancel. + */ + public final java.lang.Runnable getRunnableToCancel() { + return mRunnable; + } + + /** + * Creates a Runnable and a logging subsession that can be used in a handler/thread. Be sure to + * call cancel() if this session is never going to be run (removed from a handler queue, for + * for example). + * @return A Java Runnable that can be used in a handler queue or thread. + */ + public java.lang.Runnable prepare() { + cancel(); + mSubsession = Log.createSubsession(); + return mRunnable; + } + + /** + * This method is used to clean up the active session if the Runnable gets removed from a + * handler and is never run. + */ + public void cancel() { + synchronized (mLock) { + Log.cancelSubsession(mSubsession); + mSubsession = null; + } + } + + /** + * The method that will be run in the handler/thread. + */ + abstract public void loggedRun(); + +}
\ No newline at end of file diff --git a/android/telecom/Logging/Session.java b/android/telecom/Logging/Session.java new file mode 100644 index 00000000..c45bd6b0 --- /dev/null +++ b/android/telecom/Logging/Session.java @@ -0,0 +1,392 @@ +/* + * Copyright (C) 2016 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.Logging; + +import android.annotation.NonNull; +import android.os.Parcel; +import android.os.Parcelable; +import android.telecom.Log; +import android.text.TextUtils; + +import com.android.internal.annotations.VisibleForTesting; + +import java.util.ArrayList; + +/** + * Stores information about a thread's point of entry into that should persist until that thread + * exits. + * @hide + */ +public class Session { + + public static final String START_SESSION = "START_SESSION"; + public static final String START_EXTERNAL_SESSION = "START_EXTERNAL_SESSION"; + public static final String CREATE_SUBSESSION = "CREATE_SUBSESSION"; + public static final String CONTINUE_SUBSESSION = "CONTINUE_SUBSESSION"; + public static final String END_SUBSESSION = "END_SUBSESSION"; + public static final String END_SESSION = "END_SESSION"; + + public static final String SUBSESSION_SEPARATION_CHAR = "->"; + public static final String SESSION_SEPARATION_CHAR_CHILD = "_"; + public static final String EXTERNAL_INDICATOR = "E-"; + public static final String TRUNCATE_STRING = "..."; + + /** + * Initial value of mExecutionEndTimeMs and the final value of {@link #getLocalExecutionTime()} + * if the Session is canceled. + */ + public static final int UNDEFINED = -1; + + public static class Info implements Parcelable { + public final String sessionId; + public final String methodPath; + + private Info(String id, String path) { + sessionId = id; + methodPath = path; + } + + public static Info getInfo (Session s) { + // Create Info based on the truncated method path if the session is external, so we do + // not get multiple stacking external sessions (unless we have DEBUG level logging or + // lower). + return new Info(s.getFullSessionId(), s.getFullMethodPath( + !Log.DEBUG && s.isSessionExternal())); + } + + /** Responsible for creating Info objects for deserialized Parcels. */ + public static final Parcelable.Creator<Info> CREATOR = + new Parcelable.Creator<Info> () { + @Override + public Info createFromParcel(Parcel source) { + String id = source.readString(); + String methodName = source.readString(); + return new Info(id, methodName); + } + + @Override + public Info[] newArray(int size) { + return new Info[size]; + } + }; + + /** {@inheritDoc} */ + @Override + public int describeContents() { + return 0; + } + + /** Writes Info object into a Parcel. */ + @Override + public void writeToParcel(Parcel destination, int flags) { + destination.writeString(sessionId); + destination.writeString(methodPath); + } + } + + private String mSessionId; + private String mShortMethodName; + private long mExecutionStartTimeMs; + private long mExecutionEndTimeMs = UNDEFINED; + private Session mParentSession; + private ArrayList<Session> mChildSessions; + private boolean mIsCompleted = false; + private boolean mIsExternal = false; + private int mChildCounter = 0; + // True if this is a subsession that has been started from the same thread as the parent + // session. This can happen if Log.startSession(...) is called multiple times on the same + // thread in the case of one Telecom entry point method calling another entry point method. + // In this case, we can just make this subsession "invisible," but still keep track of it so + // that the Log.endSession() calls match up. + private boolean mIsStartedFromActiveSession = false; + // Optionally provided info about the method/class/component that started the session in order + // to make Logging easier. This info will be provided in parentheses along with the session. + private String mOwnerInfo; + // Cache Full Method path so that recursive population of the full method path only needs to + // be calculated once. + private String mFullMethodPathCache; + + public Session(String sessionId, String shortMethodName, long startTimeMs, + boolean isStartedFromActiveSession, String ownerInfo) { + setSessionId(sessionId); + setShortMethodName(shortMethodName); + mExecutionStartTimeMs = startTimeMs; + mParentSession = null; + mChildSessions = new ArrayList<>(5); + mIsStartedFromActiveSession = isStartedFromActiveSession; + mOwnerInfo = ownerInfo; + } + + public void setSessionId(@NonNull String sessionId) { + if (sessionId == null) { + mSessionId = "?"; + } + mSessionId = sessionId; + } + + public String getShortMethodName() { + return mShortMethodName; + } + + public void setShortMethodName(String shortMethodName) { + if (shortMethodName == null) { + shortMethodName = ""; + } + mShortMethodName = shortMethodName; + } + + public void setIsExternal(boolean isExternal) { + mIsExternal = isExternal; + } + + public boolean isExternal() { + return mIsExternal; + } + + public void setParentSession(Session parentSession) { + mParentSession = parentSession; + } + + public void addChild(Session childSession) { + if (childSession != null) { + mChildSessions.add(childSession); + } + } + + public void removeChild(Session child) { + if (child != null) { + mChildSessions.remove(child); + } + } + + public long getExecutionStartTimeMilliseconds() { + return mExecutionStartTimeMs; + } + + public void setExecutionStartTimeMs(long startTimeMs) { + mExecutionStartTimeMs = startTimeMs; + } + + public Session getParentSession() { + return mParentSession; + } + + public ArrayList<Session> getChildSessions() { + return mChildSessions; + } + + public boolean isSessionCompleted() { + return mIsCompleted; + } + + public boolean isStartedFromActiveSession() { + return mIsStartedFromActiveSession; + } + + public Info getInfo() { + return Info.getInfo(this); + } + + @VisibleForTesting + public String getSessionId() { + return mSessionId; + } + + // Mark this session complete. This will be deleted by Log when all subsessions are complete + // as well. + public void markSessionCompleted(long executionEndTimeMs) { + mExecutionEndTimeMs = executionEndTimeMs; + mIsCompleted = true; + } + + public long getLocalExecutionTime() { + if (mExecutionEndTimeMs == UNDEFINED) { + return UNDEFINED; + } + return mExecutionEndTimeMs - mExecutionStartTimeMs; + } + + public synchronized String getNextChildId() { + return String.valueOf(mChildCounter++); + } + + // Builds full session id recursively + private String getFullSessionId() { + // Cache mParentSession locally to prevent a concurrency problem where + // Log.endParentSessions() is called while a logging statement is running (Log.i, for + // example) and setting mParentSession to null in a different thread after the null check + // occurred. + Session parentSession = mParentSession; + if (parentSession == null) { + return mSessionId; + } else { + if (Log.VERBOSE) { + return parentSession.getFullSessionId() + + // Append "_X" to subsession to show subsession designation. + SESSION_SEPARATION_CHAR_CHILD + mSessionId; + } else { + // Only worry about the base ID at the top of the tree. + return parentSession.getFullSessionId(); + } + + } + } + + // Print out the full Session tree from any subsession node + public String printFullSessionTree() { + // Get to the top of the tree + Session topNode = this; + while (topNode.getParentSession() != null) { + topNode = topNode.getParentSession(); + } + return topNode.printSessionTree(); + } + + // Recursively move down session tree using DFS, but print out each node when it is reached. + public String printSessionTree() { + StringBuilder sb = new StringBuilder(); + printSessionTree(0, sb); + return sb.toString(); + } + + private void printSessionTree(int tabI, StringBuilder sb) { + sb.append(toString()); + for (Session child : mChildSessions) { + sb.append("\n"); + for (int i = 0; i <= tabI; i++) { + sb.append("\t"); + } + child.printSessionTree(tabI + 1, sb); + } + } + + // Recursively concatenate mShortMethodName with the parent Sessions to create full method + // path. if truncatePath is set to true, all other external sessions (except for the most + // recent) will be truncated to "..." + public String getFullMethodPath(boolean truncatePath) { + StringBuilder sb = new StringBuilder(); + getFullMethodPath(sb, truncatePath); + return sb.toString(); + } + + private synchronized void getFullMethodPath(StringBuilder sb, boolean truncatePath) { + // Return cached value for method path. When returning the truncated path, recalculate the + // full path without using the cached value. + if (!TextUtils.isEmpty(mFullMethodPathCache) && !truncatePath) { + sb.append(mFullMethodPathCache); + return; + } + Session parentSession = getParentSession(); + boolean isSessionStarted = false; + if (parentSession != null) { + // Check to see if the session has been renamed yet. If it has not, then the session + // has not been continued. + isSessionStarted = !mShortMethodName.equals(parentSession.mShortMethodName); + parentSession.getFullMethodPath(sb, truncatePath); + sb.append(SUBSESSION_SEPARATION_CHAR); + } + // Encapsulate the external session's method name so it is obvious what part of the session + // is external or truncate it if we do not want the entire history. + if (isExternal()) { + if (truncatePath) { + sb.append(TRUNCATE_STRING); + } else { + sb.append("("); + sb.append(mShortMethodName); + sb.append(")"); + } + } else { + sb.append(mShortMethodName); + } + // If we are returning the truncated path, do not save that path as the full path. + if (isSessionStarted && !truncatePath) { + // Cache this value so that we do not have to do this work next time! + // We do not cache the value if the session being evaluated hasn't been continued yet. + mFullMethodPathCache = sb.toString(); + } + } + // Recursively move to the top of the tree to see if the parent session is external. + private boolean isSessionExternal() { + if (getParentSession() == null) { + return isExternal(); + } else { + return getParentSession().isSessionExternal(); + } + } + + @Override + public int hashCode() { + int result = mSessionId != null ? mSessionId.hashCode() : 0; + result = 31 * result + (mShortMethodName != null ? mShortMethodName.hashCode() : 0); + result = 31 * result + (int) (mExecutionStartTimeMs ^ (mExecutionStartTimeMs >>> 32)); + result = 31 * result + (int) (mExecutionEndTimeMs ^ (mExecutionEndTimeMs >>> 32)); + result = 31 * result + (mParentSession != null ? mParentSession.hashCode() : 0); + result = 31 * result + (mChildSessions != null ? mChildSessions.hashCode() : 0); + result = 31 * result + (mIsCompleted ? 1 : 0); + result = 31 * result + mChildCounter; + result = 31 * result + (mIsStartedFromActiveSession ? 1 : 0); + result = 31 * result + (mOwnerInfo != null ? mOwnerInfo.hashCode() : 0); + return result; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Session session = (Session) o; + + if (mExecutionStartTimeMs != session.mExecutionStartTimeMs) return false; + if (mExecutionEndTimeMs != session.mExecutionEndTimeMs) return false; + if (mIsCompleted != session.mIsCompleted) return false; + if (mChildCounter != session.mChildCounter) return false; + if (mIsStartedFromActiveSession != session.mIsStartedFromActiveSession) return false; + if (mSessionId != null ? + !mSessionId.equals(session.mSessionId) : session.mSessionId != null) + return false; + if (mShortMethodName != null ? !mShortMethodName.equals(session.mShortMethodName) + : session.mShortMethodName != null) + return false; + if (mParentSession != null ? !mParentSession.equals(session.mParentSession) + : session.mParentSession != null) + return false; + if (mChildSessions != null ? !mChildSessions.equals(session.mChildSessions) + : session.mChildSessions != null) + return false; + return mOwnerInfo != null ? mOwnerInfo.equals(session.mOwnerInfo) + : session.mOwnerInfo == null; + + } + + @Override + public String toString() { + if (mParentSession != null && mIsStartedFromActiveSession) { + // Log.startSession was called from within another active session. Use the parent's + // Id instead of the child to reduce confusion. + return mParentSession.toString(); + } else { + StringBuilder methodName = new StringBuilder(); + methodName.append(getFullMethodPath(false /*truncatePath*/)); + if (mOwnerInfo != null && !mOwnerInfo.isEmpty()) { + methodName.append("(InCall package: "); + methodName.append(mOwnerInfo); + methodName.append(")"); + } + return methodName.toString() + "@" + getFullSessionId(); + } + } +} diff --git a/android/telecom/Logging/SessionManager.java b/android/telecom/Logging/SessionManager.java new file mode 100644 index 00000000..949f7b7a --- /dev/null +++ b/android/telecom/Logging/SessionManager.java @@ -0,0 +1,428 @@ +/* + * Copyright (C) 2016 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.Logging; + +import android.content.Context; +import android.os.Handler; +import android.os.Looper; +import android.os.Process; +import android.provider.Settings; +import android.telecom.Log; +import android.util.Base64; + +import com.android.internal.annotations.VisibleForTesting; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; + +/** + * TODO: Create better Sessions Documentation + * @hide + */ + +public class SessionManager { + + // Currently using 3 letters, So don't exceed 64^3 + private static final long SESSION_ID_ROLLOVER_THRESHOLD = 262144; + // This parameter can be overridden in Telecom's Timeouts class. + private static final long DEFAULT_SESSION_TIMEOUT_MS = 30000L; // 30 seconds + private static final String LOGGING_TAG = "Logging"; + private static final String TIMEOUTS_PREFIX = "telecom."; + + // Synchronized in all method calls + private int sCodeEntryCounter = 0; + private Context mContext; + + @VisibleForTesting + public ConcurrentHashMap<Integer, Session> mSessionMapper = new ConcurrentHashMap<>(100); + @VisibleForTesting + public java.lang.Runnable mCleanStaleSessions = () -> + cleanupStaleSessions(getSessionCleanupTimeoutMs()); + private Handler mSessionCleanupHandler = new Handler(Looper.getMainLooper()); + + // Overridden in LogTest to skip query to ContentProvider + private interface ISessionCleanupTimeoutMs { + long get(); + } + + // Overridden in tests to provide test Thread IDs + public interface ICurrentThreadId { + int get(); + } + + @VisibleForTesting + public ICurrentThreadId mCurrentThreadId = Process::myTid; + + private ISessionCleanupTimeoutMs mSessionCleanupTimeoutMs = () -> { + // mContext may be null in some cases, such as testing. For these cases, use the + // default value. + if (mContext == null) { + return DEFAULT_SESSION_TIMEOUT_MS; + } + return getCleanupTimeout(mContext); + }; + + // Usage is synchronized on this class. + private List<ISessionListener> mSessionListeners = new ArrayList<>(); + + public interface ISessionListener { + /** + * This method is run when a full Session has completed. + * @param sessionName The name of the Session that has completed. + * @param timeMs The time it took to complete in ms. + */ + void sessionComplete(String sessionName, long timeMs); + } + + public interface ISessionIdQueryHandler { + String getSessionId(); + } + + public void setContext(Context context) { + mContext = context; + } + + public SessionManager() { + } + + private long getSessionCleanupTimeoutMs() { + return mSessionCleanupTimeoutMs.get(); + } + + private synchronized void resetStaleSessionTimer() { + mSessionCleanupHandler.removeCallbacksAndMessages(null); + // Will be null in Log Testing + if (mCleanStaleSessions != null) { + mSessionCleanupHandler.postDelayed(mCleanStaleSessions, getSessionCleanupTimeoutMs()); + } + } + + /** + * Determines whether or not to start a new session or continue an existing session based on + * the {@link Session.Info} info passed into startSession. If info is null, a new Session is + * created. This code must be accompanied by endSession() at the end of the Session. + */ + public synchronized void startSession(Session.Info info, String shortMethodName, + String callerIdentification) { + // Start a new session normally if the + if(info == null) { + startSession(shortMethodName, callerIdentification); + } else { + startExternalSession(info, shortMethodName); + } + } + + /** + * Call at an entry point to the Telecom code to track the session. This code must be + * accompanied by a Log.endSession(). + */ + public synchronized void startSession(String shortMethodName, + String callerIdentification) { + resetStaleSessionTimer(); + int threadId = getCallingThreadId(); + Session activeSession = mSessionMapper.get(threadId); + // We have called startSession within an active session that has not ended... Register this + // session as a subsession. + if (activeSession != null) { + Session childSession = createSubsession(true); + continueSession(childSession, shortMethodName); + return; + } else { + // Only Log that we are starting the parent session. + Log.d(LOGGING_TAG, Session.START_SESSION); + } + Session newSession = new Session(getNextSessionID(), shortMethodName, + System.currentTimeMillis(), false, callerIdentification); + mSessionMapper.put(threadId, newSession); + } + + /** + * Registers an external Session with the Manager using that external Session's sessionInfo. + * Log.endSession will still need to be called at the end of the session. + * @param sessionInfo Describes the external Session's information. + * @param shortMethodName The method name of the new session that is being started. + */ + public synchronized void startExternalSession(Session.Info sessionInfo, + String shortMethodName) { + if(sessionInfo == null) { + return; + } + + int threadId = getCallingThreadId(); + Session threadSession = mSessionMapper.get(threadId); + if (threadSession != null) { + // We should never get into a situation where there is already an active session AND + // an external session is added. We are just using that active session. + Log.w(LOGGING_TAG, "trying to start an external session with a session " + + "already active."); + return; + } + + // Create Session from Info and add to the sessionMapper under this ID. + Log.d(LOGGING_TAG, Session.START_EXTERNAL_SESSION); + Session externalSession = new Session(Session.EXTERNAL_INDICATOR + sessionInfo.sessionId, + sessionInfo.methodPath, System.currentTimeMillis(), + false /*isStartedFromActiveSession*/, null); + externalSession.setIsExternal(true); + // Mark the external session as already completed, since we have no way of knowing when + // the external session actually has completed. + externalSession.markSessionCompleted(Session.UNDEFINED); + // Track the external session with the SessionMapper so that we can create and continue + // an active subsession based on it. + mSessionMapper.put(threadId, externalSession); + // Create a subsession from this external Session parent node + Session childSession = createSubsession(); + continueSession(childSession, shortMethodName); + } + + /** + * Notifies the logging system that a subsession will be run at a later point and + * allocates the resources. Returns a session object that must be used in + * Log.continueSession(...) to start the subsession. + */ + public Session createSubsession() { + return createSubsession(false); + } + + private synchronized Session createSubsession(boolean isStartedFromActiveSession) { + int threadId = getCallingThreadId(); + Session threadSession = mSessionMapper.get(threadId); + if (threadSession == null) { + Log.d(LOGGING_TAG, "Log.createSubsession was called with no session " + + "active."); + return null; + } + // Start execution time of the session will be overwritten in continueSession(...). + Session newSubsession = new Session(threadSession.getNextChildId(), + threadSession.getShortMethodName(), System.currentTimeMillis(), + isStartedFromActiveSession, null); + threadSession.addChild(newSubsession); + newSubsession.setParentSession(threadSession); + + if (!isStartedFromActiveSession) { + Log.v(LOGGING_TAG, Session.CREATE_SUBSESSION + " " + + newSubsession.toString()); + } else { + Log.v(LOGGING_TAG, Session.CREATE_SUBSESSION + + " (Invisible subsession)"); + } + return newSubsession; + } + + /** + * Retrieve the information of the currently active Session. This information is parcelable and + * is used to create an external Session ({@link #startExternalSession(Session.Info, String)}). + * If there is no Session active, this method will return null. + */ + public synchronized Session.Info getExternalSession() { + int threadId = getCallingThreadId(); + Session threadSession = mSessionMapper.get(threadId); + if (threadSession == null) { + Log.d(LOGGING_TAG, "Log.getExternalSession was called with no session " + + "active."); + return null; + } + + return threadSession.getInfo(); + } + + /** + * Cancels a subsession that had Log.createSubsession() called on it, but will never have + * Log.continueSession(...) called on it due to an error. Allows the subsession to be cleaned + * gracefully instead of being removed by the mSessionCleanupHandler forcefully later. + */ + public synchronized void cancelSubsession(Session subsession) { + if (subsession == null) { + return; + } + + subsession.markSessionCompleted(Session.UNDEFINED); + endParentSessions(subsession); + } + + /** + * Starts the subsession that was created in Log.CreateSubsession. The Log.endSession() method + * must be called at the end of this method. The full session will complete when all + * subsessions are completed. + */ + public synchronized void continueSession(Session subsession, String shortMethodName) { + if (subsession == null) { + return; + } + resetStaleSessionTimer(); + subsession.setShortMethodName(shortMethodName); + subsession.setExecutionStartTimeMs(System.currentTimeMillis()); + Session parentSession = subsession.getParentSession(); + if (parentSession == null) { + Log.i(LOGGING_TAG, "Log.continueSession was called with no session " + + "active for method " + shortMethodName); + return; + } + + mSessionMapper.put(getCallingThreadId(), subsession); + if (!subsession.isStartedFromActiveSession()) { + Log.v(LOGGING_TAG, Session.CONTINUE_SUBSESSION); + } else { + Log.v(LOGGING_TAG, Session.CONTINUE_SUBSESSION + + " (Invisible Subsession) with Method " + shortMethodName); + } + } + + /** + * Ends the current session/subsession. Must be called after a Log.startSession(...) and + * Log.continueSession(...) call. + */ + public synchronized void endSession() { + int threadId = getCallingThreadId(); + Session completedSession = mSessionMapper.get(threadId); + if (completedSession == null) { + Log.w(LOGGING_TAG, "Log.endSession was called with no session active."); + return; + } + + completedSession.markSessionCompleted(System.currentTimeMillis()); + if (!completedSession.isStartedFromActiveSession()) { + Log.v(LOGGING_TAG, Session.END_SUBSESSION + " (dur: " + + completedSession.getLocalExecutionTime() + " mS)"); + } else { + Log.v(LOGGING_TAG, Session.END_SUBSESSION + + " (Invisible Subsession) (dur: " + completedSession.getLocalExecutionTime() + + " ms)"); + } + // Remove after completed so that reference still exists for logging the end events + Session parentSession = completedSession.getParentSession(); + mSessionMapper.remove(threadId); + endParentSessions(completedSession); + // If this subsession was started from a parent session using Log.startSession, return the + // ThreadID back to the parent after completion. + if (parentSession != null && !parentSession.isSessionCompleted() && + completedSession.isStartedFromActiveSession()) { + mSessionMapper.put(threadId, parentSession); + } + } + + // Recursively deletes all complete parent sessions of the current subsession if it is a leaf. + private void endParentSessions(Session subsession) { + // Session is not completed or not currently a leaf, so we can not remove because a child is + // still running + if (!subsession.isSessionCompleted() || subsession.getChildSessions().size() != 0) { + return; + } + Session parentSession = subsession.getParentSession(); + if (parentSession != null) { + subsession.setParentSession(null); + parentSession.removeChild(subsession); + // Report the child session of the external session as being complete to the listeners, + // not the external session itself. + if (parentSession.isExternal()) { + long fullSessionTimeMs = + System.currentTimeMillis() - subsession.getExecutionStartTimeMilliseconds(); + notifySessionCompleteListeners(subsession.getShortMethodName(), fullSessionTimeMs); + } + endParentSessions(parentSession); + } else { + // All of the subsessions have been completed and it is time to report on the full + // running time of the session. + long fullSessionTimeMs = + System.currentTimeMillis() - subsession.getExecutionStartTimeMilliseconds(); + Log.d(LOGGING_TAG, Session.END_SESSION + " (dur: " + fullSessionTimeMs + + " ms): " + subsession.toString()); + if (!subsession.isExternal()) { + notifySessionCompleteListeners(subsession.getShortMethodName(), fullSessionTimeMs); + } + } + } + + private void notifySessionCompleteListeners(String methodName, long sessionTimeMs) { + for (ISessionListener l : mSessionListeners) { + l.sessionComplete(methodName, sessionTimeMs); + } + } + + public String getSessionId() { + Session currentSession = mSessionMapper.get(getCallingThreadId()); + return currentSession != null ? currentSession.toString() : ""; + } + + public synchronized void registerSessionListener(ISessionListener l) { + if (l != null) { + mSessionListeners.add(l); + } + } + + private synchronized String getNextSessionID() { + Integer nextId = sCodeEntryCounter++; + if (nextId >= SESSION_ID_ROLLOVER_THRESHOLD) { + restartSessionCounter(); + nextId = sCodeEntryCounter++; + } + return getBase64Encoding(nextId); + } + + private synchronized void restartSessionCounter() { + sCodeEntryCounter = 0; + } + + private String getBase64Encoding(int number) { + byte[] idByteArray = ByteBuffer.allocate(4).putInt(number).array(); + idByteArray = Arrays.copyOfRange(idByteArray, 2, 4); + return Base64.encodeToString(idByteArray, Base64.NO_WRAP | Base64.NO_PADDING); + } + + private int getCallingThreadId() { + return mCurrentThreadId.get(); + } + + @VisibleForTesting + public synchronized void cleanupStaleSessions(long timeoutMs) { + String logMessage = "Stale Sessions Cleaned:\n"; + boolean isSessionsStale = false; + long currentTimeMs = System.currentTimeMillis(); + // Remove references that are in the Session Mapper (causing GC to occur) on + // sessions that are lasting longer than LOGGING_SESSION_TIMEOUT_MS. + // If this occurs, then there is most likely a Session active that never had + // Log.endSession called on it. + for (Iterator<ConcurrentHashMap.Entry<Integer, Session>> it = + mSessionMapper.entrySet().iterator(); it.hasNext(); ) { + ConcurrentHashMap.Entry<Integer, Session> entry = it.next(); + Session session = entry.getValue(); + if (currentTimeMs - session.getExecutionStartTimeMilliseconds() > timeoutMs) { + it.remove(); + logMessage += session.printFullSessionTree() + "\n"; + isSessionsStale = true; + } + } + if (isSessionsStale) { + Log.w(LOGGING_TAG, logMessage); + } else { + Log.v(LOGGING_TAG, "No stale logging sessions needed to be cleaned..."); + } + } + + /** + * Returns the amount of time after a Logging session has been started that Telecom is set to + * perform a sweep to check and make sure that the session is still not incomplete (stale). + */ + private long getCleanupTimeout(Context context) { + return Settings.Secure.getLong(context.getContentResolver(), TIMEOUTS_PREFIX + + "stale_session_cleanup_timeout_millis", DEFAULT_SESSION_TIMEOUT_MS); + } +} diff --git a/android/telecom/Logging/TimedEvent.java b/android/telecom/Logging/TimedEvent.java new file mode 100644 index 00000000..6785e92d --- /dev/null +++ b/android/telecom/Logging/TimedEvent.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2016 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.Logging; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +/** + * @hide + */ +public abstract class TimedEvent<T> { + public abstract long getTime(); + public abstract T getKey(); + + public static <T> Map<T, Double> averageTimings(Collection<? extends TimedEvent<T>> events) { + HashMap<T, Integer> counts = new HashMap<>(); + HashMap<T, Double> result = new HashMap<>(); + + for (TimedEvent<T> entry : events) { + if (counts.containsKey(entry.getKey())) { + counts.put(entry.getKey(), counts.get(entry.getKey()) + 1); + result.put(entry.getKey(), result.get(entry.getKey()) + entry.getTime()); + } else { + counts.put(entry.getKey(), 1); + result.put(entry.getKey(), (double) entry.getTime()); + } + } + + for (Map.Entry<T, Double> entry : result.entrySet()) { + result.put(entry.getKey(), entry.getValue() / counts.get(entry.getKey())); + } + + return result; + } +} diff --git a/android/telecom/ParcelableCall.java b/android/telecom/ParcelableCall.java new file mode 100644 index 00000000..6212a77f --- /dev/null +++ b/android/telecom/ParcelableCall.java @@ -0,0 +1,415 @@ +/* + * Copyright 2014, 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.net.Uri; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.RemoteException; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import com.android.internal.telecom.IVideoProvider; + +/** + * Information about a call that is used between InCallService and Telecom. + * @hide + */ +public final class ParcelableCall implements Parcelable { + private final String mId; + private final int mState; + private final DisconnectCause mDisconnectCause; + private final List<String> mCannedSmsResponses; + private final int mCapabilities; + private final int mProperties; + private final int mSupportedAudioRoutes; + private final long mConnectTimeMillis; + private final Uri mHandle; + private final int mHandlePresentation; + private final String mCallerDisplayName; + private final int mCallerDisplayNamePresentation; + private final GatewayInfo mGatewayInfo; + private final PhoneAccountHandle mAccountHandle; + private final boolean mIsVideoCallProviderChanged; + private final IVideoProvider mVideoCallProvider; + private VideoCallImpl mVideoCall; + private final boolean mIsRttCallChanged; + private final ParcelableRttCall mRttCall; + private final String mParentCallId; + private final List<String> mChildCallIds; + private final StatusHints mStatusHints; + private final int mVideoState; + private final List<String> mConferenceableCallIds; + private final Bundle mIntentExtras; + private final Bundle mExtras; + private final long mCreationTimeMillis; + + public ParcelableCall( + String id, + int state, + DisconnectCause disconnectCause, + List<String> cannedSmsResponses, + int capabilities, + int properties, + int supportedAudioRoutes, + long connectTimeMillis, + Uri handle, + int handlePresentation, + String callerDisplayName, + int callerDisplayNamePresentation, + GatewayInfo gatewayInfo, + PhoneAccountHandle accountHandle, + boolean isVideoCallProviderChanged, + IVideoProvider videoCallProvider, + boolean isRttCallChanged, + ParcelableRttCall rttCall, + String parentCallId, + List<String> childCallIds, + StatusHints statusHints, + int videoState, + List<String> conferenceableCallIds, + Bundle intentExtras, + Bundle extras, + long creationTimeMillis) { + mId = id; + mState = state; + mDisconnectCause = disconnectCause; + mCannedSmsResponses = cannedSmsResponses; + mCapabilities = capabilities; + mProperties = properties; + mSupportedAudioRoutes = supportedAudioRoutes; + mConnectTimeMillis = connectTimeMillis; + mHandle = handle; + mHandlePresentation = handlePresentation; + mCallerDisplayName = callerDisplayName; + mCallerDisplayNamePresentation = callerDisplayNamePresentation; + mGatewayInfo = gatewayInfo; + mAccountHandle = accountHandle; + mIsVideoCallProviderChanged = isVideoCallProviderChanged; + mVideoCallProvider = videoCallProvider; + mIsRttCallChanged = isRttCallChanged; + mRttCall = rttCall; + mParentCallId = parentCallId; + mChildCallIds = childCallIds; + mStatusHints = statusHints; + mVideoState = videoState; + mConferenceableCallIds = Collections.unmodifiableList(conferenceableCallIds); + mIntentExtras = intentExtras; + mExtras = extras; + mCreationTimeMillis = creationTimeMillis; + } + + /** The unique ID of the call. */ + public String getId() { + return mId; + } + + /** The current state of the call. */ + public int getState() { + return mState; + } + + /** + * Reason for disconnection, as described by {@link android.telecomm.DisconnectCause}. Valid + * when call state is {@link CallState#DISCONNECTED}. + */ + public DisconnectCause getDisconnectCause() { + return mDisconnectCause; + } + + /** + * The set of possible text message responses when this call is incoming. + */ + public List<String> getCannedSmsResponses() { + return mCannedSmsResponses; + } + + // Bit mask of actions a call supports, values are defined in {@link CallCapabilities}. + public int getCapabilities() { + return mCapabilities; + } + + /** Bitmask of properties of the call. */ + public int getProperties() { return mProperties; } + + /** Bitmask of supported routes of the call */ + public int getSupportedAudioRoutes() { + return mSupportedAudioRoutes; + } + + /** The time that the call switched to the active state. */ + public long getConnectTimeMillis() { + return mConnectTimeMillis; + } + + /** The endpoint to which the call is connected. */ + public Uri getHandle() { + return mHandle; + } + + /** + * The presentation requirements for the handle. See {@link TelecomManager} for valid values. + */ + public int getHandlePresentation() { + return mHandlePresentation; + } + + /** The endpoint to which the call is connected. */ + public String getCallerDisplayName() { + return mCallerDisplayName; + } + + /** + * The presentation requirements for the caller display name. + * See {@link TelecomManager} for valid values. + */ + public int getCallerDisplayNamePresentation() { + return mCallerDisplayNamePresentation; + } + + /** Gateway information for the call. */ + public GatewayInfo getGatewayInfo() { + return mGatewayInfo; + } + + /** PhoneAccountHandle information for the call. */ + public PhoneAccountHandle getAccountHandle() { + return mAccountHandle; + } + + /** + * Returns an object for remotely communicating through the video call provider's binder. + * + * @param callingPackageName the package name of the calling InCallService. + * @param targetSdkVersion the target SDK version of the calling InCallService. + * @return The video call. + */ + public VideoCallImpl getVideoCallImpl(String callingPackageName, int targetSdkVersion) { + if (mVideoCall == null && mVideoCallProvider != null) { + try { + mVideoCall = new VideoCallImpl(mVideoCallProvider, callingPackageName, + targetSdkVersion); + } catch (RemoteException ignored) { + // Ignore RemoteException. + } + } + + return mVideoCall; + } + + public boolean getIsRttCallChanged() { + return mIsRttCallChanged; + } + + /** + * RTT communication channel information + * @return The ParcelableRttCall + */ + public ParcelableRttCall getParcelableRttCall() { + return mRttCall; + } + + /** + * The conference call to which this call is conferenced. Null if not conferenced. + */ + public String getParentCallId() { + return mParentCallId; + } + + /** + * The child call-IDs if this call is a conference call. Returns an empty list if this is not + * a conference call or if the conference call contains no children. + */ + public List<String> getChildCallIds() { + return mChildCallIds; + } + + public List<String> getConferenceableCallIds() { + return mConferenceableCallIds; + } + + /** + * The status label and icon. + * + * @return Status hints. + */ + public StatusHints getStatusHints() { + return mStatusHints; + } + + /** + * The video state. + * @return The video state of the call. + */ + public int getVideoState() { + return mVideoState; + } + + /** + * Any extras associated with this call. + * + * @return a bundle of extras + */ + public Bundle getExtras() { + return mExtras; + } + + /** + * Extras passed in as part of the original call intent. + * + * @return The intent extras. + */ + public Bundle getIntentExtras() { + return mIntentExtras; + } + + /** + * Indicates to the receiver of the {@link ParcelableCall} whether a change has occurred in the + * {@link android.telecom.InCallService.VideoCall} associated with this call. Since + * {@link #getVideoCall()} creates a new {@link VideoCallImpl}, it is useful to know whether + * the provider has changed (which can influence whether it is accessed). + * + * @return {@code true} if the video call changed, {@code false} otherwise. + */ + public boolean isVideoCallProviderChanged() { + return mIsVideoCallProviderChanged; + } + + /** + * @return The time the call was created, in milliseconds since the epoch. + */ + public long getCreationTimeMillis() { + return mCreationTimeMillis; + } + + /** Responsible for creating ParcelableCall objects for deserialized Parcels. */ + public static final Parcelable.Creator<ParcelableCall> CREATOR = + new Parcelable.Creator<ParcelableCall> () { + @Override + public ParcelableCall createFromParcel(Parcel source) { + ClassLoader classLoader = ParcelableCall.class.getClassLoader(); + String id = source.readString(); + int state = source.readInt(); + DisconnectCause disconnectCause = source.readParcelable(classLoader); + List<String> cannedSmsResponses = new ArrayList<>(); + source.readList(cannedSmsResponses, classLoader); + int capabilities = source.readInt(); + int properties = source.readInt(); + long connectTimeMillis = source.readLong(); + Uri handle = source.readParcelable(classLoader); + int handlePresentation = source.readInt(); + String callerDisplayName = source.readString(); + int callerDisplayNamePresentation = source.readInt(); + GatewayInfo gatewayInfo = source.readParcelable(classLoader); + PhoneAccountHandle accountHandle = source.readParcelable(classLoader); + boolean isVideoCallProviderChanged = source.readByte() == 1; + IVideoProvider videoCallProvider = + IVideoProvider.Stub.asInterface(source.readStrongBinder()); + String parentCallId = source.readString(); + List<String> childCallIds = new ArrayList<>(); + source.readList(childCallIds, classLoader); + StatusHints statusHints = source.readParcelable(classLoader); + int videoState = source.readInt(); + List<String> conferenceableCallIds = new ArrayList<>(); + source.readList(conferenceableCallIds, classLoader); + Bundle intentExtras = source.readBundle(classLoader); + Bundle extras = source.readBundle(classLoader); + int supportedAudioRoutes = source.readInt(); + boolean isRttCallChanged = source.readByte() == 1; + ParcelableRttCall rttCall = source.readParcelable(classLoader); + long creationTimeMillis = source.readLong(); + return new ParcelableCall( + id, + state, + disconnectCause, + cannedSmsResponses, + capabilities, + properties, + supportedAudioRoutes, + connectTimeMillis, + handle, + handlePresentation, + callerDisplayName, + callerDisplayNamePresentation, + gatewayInfo, + accountHandle, + isVideoCallProviderChanged, + videoCallProvider, + isRttCallChanged, + rttCall, + parentCallId, + childCallIds, + statusHints, + videoState, + conferenceableCallIds, + intentExtras, + extras, + creationTimeMillis); + } + + @Override + public ParcelableCall[] newArray(int size) { + return new ParcelableCall[size]; + } + }; + + /** {@inheritDoc} */ + @Override + public int describeContents() { + return 0; + } + + /** Writes ParcelableCall object into a Parcel. */ + @Override + public void writeToParcel(Parcel destination, int flags) { + destination.writeString(mId); + destination.writeInt(mState); + destination.writeParcelable(mDisconnectCause, 0); + destination.writeList(mCannedSmsResponses); + destination.writeInt(mCapabilities); + destination.writeInt(mProperties); + destination.writeLong(mConnectTimeMillis); + destination.writeParcelable(mHandle, 0); + destination.writeInt(mHandlePresentation); + destination.writeString(mCallerDisplayName); + destination.writeInt(mCallerDisplayNamePresentation); + destination.writeParcelable(mGatewayInfo, 0); + destination.writeParcelable(mAccountHandle, 0); + destination.writeByte((byte) (mIsVideoCallProviderChanged ? 1 : 0)); + destination.writeStrongBinder( + mVideoCallProvider != null ? mVideoCallProvider.asBinder() : null); + destination.writeString(mParentCallId); + destination.writeList(mChildCallIds); + destination.writeParcelable(mStatusHints, 0); + destination.writeInt(mVideoState); + destination.writeList(mConferenceableCallIds); + destination.writeBundle(mIntentExtras); + destination.writeBundle(mExtras); + destination.writeInt(mSupportedAudioRoutes); + destination.writeByte((byte) (mIsRttCallChanged ? 1 : 0)); + destination.writeParcelable(mRttCall, 0); + destination.writeLong(mCreationTimeMillis); + } + + @Override + public String toString() { + return String.format("[%s, parent:%s, children:%s]", mId, mParentCallId, mChildCallIds); + } +} diff --git a/android/telecom/ParcelableCallAnalytics.java b/android/telecom/ParcelableCallAnalytics.java new file mode 100644 index 00000000..383d10ba --- /dev/null +++ b/android/telecom/ParcelableCallAnalytics.java @@ -0,0 +1,458 @@ +/* + * Copyright (C) 2016 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.SystemApi; +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; + +/** + * @hide + */ +@SystemApi +public class ParcelableCallAnalytics implements Parcelable { + /** {@hide} */ + public static final class VideoEvent implements Parcelable { + public static final int SEND_LOCAL_SESSION_MODIFY_REQUEST = 0; + public static final int SEND_LOCAL_SESSION_MODIFY_RESPONSE = 1; + public static final int RECEIVE_REMOTE_SESSION_MODIFY_REQUEST = 2; + public static final int RECEIVE_REMOTE_SESSION_MODIFY_RESPONSE = 3; + + public static final Parcelable.Creator<VideoEvent> CREATOR = + new Parcelable.Creator<VideoEvent> () { + + @Override + public VideoEvent createFromParcel(Parcel in) { + return new VideoEvent(in); + } + + @Override + public VideoEvent[] newArray(int size) { + return new VideoEvent[size]; + } + }; + + private int mEventName; + private long mTimeSinceLastEvent; + private int mVideoState; + + public VideoEvent(int eventName, long timeSinceLastEvent, int videoState) { + mEventName = eventName; + mTimeSinceLastEvent = timeSinceLastEvent; + mVideoState = videoState; + } + + VideoEvent(Parcel in) { + mEventName = in.readInt(); + mTimeSinceLastEvent = in.readLong(); + mVideoState = in.readInt(); + } + + public int getEventName() { + return mEventName; + } + + public long getTimeSinceLastEvent() { + return mTimeSinceLastEvent; + } + + public int getVideoState() { + return mVideoState; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeInt(mEventName); + out.writeLong(mTimeSinceLastEvent); + out.writeInt(mVideoState); + } + } + + public static final class AnalyticsEvent implements Parcelable { + public static final int SET_SELECT_PHONE_ACCOUNT = 0; + public static final int SET_ACTIVE = 1; + public static final int SET_DISCONNECTED = 2; + public static final int START_CONNECTION = 3; + public static final int SET_DIALING = 4; + public static final int BIND_CS = 5; + public static final int CS_BOUND = 6; + public static final int REQUEST_ACCEPT = 7; + public static final int REQUEST_REJECT = 8; + + public static final int SCREENING_SENT = 100; + public static final int SCREENING_COMPLETED = 101; + public static final int DIRECT_TO_VM_INITIATED = 102; + public static final int DIRECT_TO_VM_FINISHED = 103; + public static final int BLOCK_CHECK_INITIATED = 104; + public static final int BLOCK_CHECK_FINISHED = 105; + 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 SKIP_RINGING = 200; + public static final int SILENCE = 201; + public static final int MUTE = 202; + public static final int UNMUTE = 203; + public static final int AUDIO_ROUTE_BT = 204; + public static final int AUDIO_ROUTE_EARPIECE = 205; + public static final int AUDIO_ROUTE_HEADSET = 206; + public static final int AUDIO_ROUTE_SPEAKER = 207; + + public static final int CONFERENCE_WITH = 300; + public static final int SPLIT_CONFERENCE = 301; + public static final int SET_PARENT = 302; + + public static final int REQUEST_HOLD = 400; + public static final int REQUEST_UNHOLD = 401; + public static final int REMOTELY_HELD = 402; + public static final int REMOTELY_UNHELD = 403; + public static final int SET_HOLD = 404; + public static final int SWAP = 405; + + public static final int REQUEST_PULL = 500; + + + public static final Parcelable.Creator<AnalyticsEvent> CREATOR = + new Parcelable.Creator<AnalyticsEvent> () { + + @Override + public AnalyticsEvent createFromParcel(Parcel in) { + return new AnalyticsEvent(in); + } + + @Override + public AnalyticsEvent[] newArray(int size) { + return new AnalyticsEvent[size]; + } + }; + + private int mEventName; + private long mTimeSinceLastEvent; + + public AnalyticsEvent(int eventName, long timestamp) { + mEventName = eventName; + mTimeSinceLastEvent = timestamp; + } + + AnalyticsEvent(Parcel in) { + mEventName = in.readInt(); + mTimeSinceLastEvent = in.readLong(); + } + + public int getEventName() { + return mEventName; + } + + public long getTimeSinceLastEvent() { + return mTimeSinceLastEvent; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeInt(mEventName); + out.writeLong(mTimeSinceLastEvent); + } + } + + public static final class EventTiming implements Parcelable { + public static final int ACCEPT_TIMING = 0; + public static final int REJECT_TIMING = 1; + public static final int DISCONNECT_TIMING = 2; + public static final int HOLD_TIMING = 3; + public static final int UNHOLD_TIMING = 4; + public static final int OUTGOING_TIME_TO_DIALING_TIMING = 5; + public static final int BIND_CS_TIMING = 6; + public static final int SCREENING_COMPLETED_TIMING = 7; + public static final int DIRECT_TO_VM_FINISHED_TIMING = 8; + 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 INVALID = 999999; + + public static final Parcelable.Creator<EventTiming> CREATOR = + new Parcelable.Creator<EventTiming> () { + + @Override + public EventTiming createFromParcel(Parcel in) { + return new EventTiming(in); + } + + @Override + public EventTiming[] newArray(int size) { + return new EventTiming[size]; + } + }; + + private int mName; + private long mTime; + + public EventTiming(int name, long time) { + this.mName = name; + this.mTime = time; + } + + private EventTiming(Parcel in) { + mName = in.readInt(); + mTime = in.readLong(); + } + + public int getName() { + return mName; + } + + public long getTime() { + return mTime; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeInt(mName); + out.writeLong(mTime); + } + } + + public static final int CALLTYPE_UNKNOWN = 0; + public static final int CALLTYPE_INCOMING = 1; + public static final int CALLTYPE_OUTGOING = 2; + + // Constants for call technology + public static final int CDMA_PHONE = 0x1; + public static final int GSM_PHONE = 0x2; + public static final int IMS_PHONE = 0x4; + public static final int SIP_PHONE = 0x8; + public static final int THIRD_PARTY_PHONE = 0x10; + + public static final long MILLIS_IN_5_MINUTES = 1000 * 60 * 5; + public static final long MILLIS_IN_1_SECOND = 1000; + + public static final int STILL_CONNECTED = -1; + + public static final Parcelable.Creator<ParcelableCallAnalytics> CREATOR = + new Parcelable.Creator<ParcelableCallAnalytics> () { + + @Override + public ParcelableCallAnalytics createFromParcel(Parcel in) { + return new ParcelableCallAnalytics(in); + } + + @Override + public ParcelableCallAnalytics[] newArray(int size) { + return new ParcelableCallAnalytics[size]; + } + }; + + // The start time of the call in milliseconds since Jan. 1, 1970, rounded to the nearest + // 5 minute increment. + private final long startTimeMillis; + + // The duration of the call, in milliseconds. + private final long callDurationMillis; + + // ONE OF calltype_unknown, calltype_incoming, or calltype_outgoing + private final int callType; + + // true if the call came in while another call was in progress or if the user dialed this call + // while in the middle of another call. + private final boolean isAdditionalCall; + + // true if the call was interrupted by an incoming or outgoing call. + private final boolean isInterrupted; + + // bitmask denoting which technologies a call used. + private final int callTechnologies; + + // Any of the DisconnectCause codes, or STILL_CONNECTED. + private final int callTerminationCode; + + // Whether the call is an emergency call + private final boolean isEmergencyCall; + + // The package name of the connection service that this call used. + private final String connectionService; + + // Whether the call object was created from an existing connection. + private final boolean isCreatedFromExistingConnection; + + // A list of events that are associated with this call + private final List<AnalyticsEvent> analyticsEvents; + + // A map from event-pair names to their durations. + private final List<EventTiming> eventTimings; + + // Whether the call has ever been a video call. + private boolean isVideoCall = false; + + // A list of video events that have occurred. + private List<VideoEvent> videoEvents; + + public ParcelableCallAnalytics(long startTimeMillis, long callDurationMillis, int callType, + boolean isAdditionalCall, boolean isInterrupted, int callTechnologies, + int callTerminationCode, boolean isEmergencyCall, String connectionService, + boolean isCreatedFromExistingConnection, List<AnalyticsEvent> analyticsEvents, + List<EventTiming> eventTimings) { + this.startTimeMillis = startTimeMillis; + this.callDurationMillis = callDurationMillis; + this.callType = callType; + this.isAdditionalCall = isAdditionalCall; + this.isInterrupted = isInterrupted; + this.callTechnologies = callTechnologies; + this.callTerminationCode = callTerminationCode; + this.isEmergencyCall = isEmergencyCall; + this.connectionService = connectionService; + this.isCreatedFromExistingConnection = isCreatedFromExistingConnection; + this.analyticsEvents = analyticsEvents; + this.eventTimings = eventTimings; + } + + public ParcelableCallAnalytics(Parcel in) { + startTimeMillis = in.readLong(); + callDurationMillis = in.readLong(); + callType = in.readInt(); + isAdditionalCall = readByteAsBoolean(in); + isInterrupted = readByteAsBoolean(in); + callTechnologies = in.readInt(); + callTerminationCode = in.readInt(); + isEmergencyCall = readByteAsBoolean(in); + connectionService = in.readString(); + isCreatedFromExistingConnection = readByteAsBoolean(in); + analyticsEvents = new ArrayList<>(); + in.readTypedList(analyticsEvents, AnalyticsEvent.CREATOR); + eventTimings = new ArrayList<>(); + in.readTypedList(eventTimings, EventTiming.CREATOR); + isVideoCall = readByteAsBoolean(in); + videoEvents = new LinkedList<>(); + in.readTypedList(videoEvents, VideoEvent.CREATOR); + } + + public void writeToParcel(Parcel out, int flags) { + out.writeLong(startTimeMillis); + out.writeLong(callDurationMillis); + out.writeInt(callType); + writeBooleanAsByte(out, isAdditionalCall); + writeBooleanAsByte(out, isInterrupted); + out.writeInt(callTechnologies); + out.writeInt(callTerminationCode); + writeBooleanAsByte(out, isEmergencyCall); + out.writeString(connectionService); + writeBooleanAsByte(out, isCreatedFromExistingConnection); + out.writeTypedList(analyticsEvents); + out.writeTypedList(eventTimings); + writeBooleanAsByte(out, isVideoCall); + out.writeTypedList(videoEvents); + } + + /** {@hide} */ + public void setIsVideoCall(boolean isVideoCall) { + this.isVideoCall = isVideoCall; + } + + /** {@hide} */ + public void setVideoEvents(List<VideoEvent> videoEvents) { + this.videoEvents = videoEvents; + } + + public long getStartTimeMillis() { + return startTimeMillis; + } + + public long getCallDurationMillis() { + return callDurationMillis; + } + + public int getCallType() { + return callType; + } + + public boolean isAdditionalCall() { + return isAdditionalCall; + } + + public boolean isInterrupted() { + return isInterrupted; + } + + public int getCallTechnologies() { + return callTechnologies; + } + + public int getCallTerminationCode() { + return callTerminationCode; + } + + public boolean isEmergencyCall() { + return isEmergencyCall; + } + + public String getConnectionService() { + return connectionService; + } + + public boolean isCreatedFromExistingConnection() { + return isCreatedFromExistingConnection; + } + + public List<AnalyticsEvent> analyticsEvents() { + return analyticsEvents; + } + + public List<EventTiming> getEventTimings() { + return eventTimings; + } + + /** {@hide} */ + public boolean isVideoCall() { + return isVideoCall; + } + + /** {@hide} */ + public List<VideoEvent> getVideoEvents() { + return videoEvents; + } + + @Override + public int describeContents() { + return 0; + } + + private static void writeBooleanAsByte(Parcel out, boolean b) { + out.writeByte((byte) (b ? 1 : 0)); + } + + private static boolean readByteAsBoolean(Parcel in) { + return (in.readByte() == 1); + } +} diff --git a/android/telecom/ParcelableConference.java b/android/telecom/ParcelableConference.java new file mode 100644 index 00000000..a6221d4d --- /dev/null +++ b/android/telecom/ParcelableConference.java @@ -0,0 +1,189 @@ +/* + * Copyright 2014, 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.Bundle; +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.ArrayList; +import java.util.List; + +import com.android.internal.telecom.IVideoProvider; + +/** + * A parcelable representation of a conference connection. + * @hide + */ +public final class ParcelableConference implements Parcelable { + + private PhoneAccountHandle mPhoneAccount; + private int mState; + private int mConnectionCapabilities; + private int mConnectionProperties; + private List<String> mConnectionIds; + private long mConnectTimeMillis = Conference.CONNECT_TIME_NOT_SPECIFIED; + private final IVideoProvider mVideoProvider; + private final int mVideoState; + private StatusHints mStatusHints; + private Bundle mExtras; + private long mConnectElapsedTimeMillis = Conference.CONNECT_TIME_NOT_SPECIFIED; + + public ParcelableConference( + PhoneAccountHandle phoneAccount, + int state, + int connectionCapabilities, + int connectionProperties, + List<String> connectionIds, + IVideoProvider videoProvider, + int videoState, + long connectTimeMillis, + long connectElapsedTimeMillis, + StatusHints statusHints, + Bundle extras) { + mPhoneAccount = phoneAccount; + mState = state; + mConnectionCapabilities = connectionCapabilities; + mConnectionProperties = connectionProperties; + mConnectionIds = connectionIds; + mVideoProvider = videoProvider; + mVideoState = videoState; + mConnectTimeMillis = connectTimeMillis; + mStatusHints = statusHints; + mExtras = extras; + mConnectElapsedTimeMillis = connectElapsedTimeMillis; + } + + @Override + public String toString() { + return (new StringBuffer()) + .append("account: ") + .append(mPhoneAccount) + .append(", state: ") + .append(Connection.stateToString(mState)) + .append(", capabilities: ") + .append(Connection.capabilitiesToString(mConnectionCapabilities)) + .append(", properties: ") + .append(Connection.propertiesToString(mConnectionProperties)) + .append(", connectTime: ") + .append(mConnectTimeMillis) + .append(", children: ") + .append(mConnectionIds) + .append(", VideoState: ") + .append(mVideoState) + .append(", VideoProvider: ") + .append(mVideoProvider) + .toString(); + } + + public PhoneAccountHandle getPhoneAccount() { + return mPhoneAccount; + } + + public int getState() { + return mState; + } + + public int getConnectionCapabilities() { + return mConnectionCapabilities; + } + + public int getConnectionProperties() { + return mConnectionProperties; + } + + public List<String> getConnectionIds() { + return mConnectionIds; + } + + public long getConnectTimeMillis() { + return mConnectTimeMillis; + } + + public long getConnectElapsedTimeMillis() { + return mConnectElapsedTimeMillis; + } + + public IVideoProvider getVideoProvider() { + return mVideoProvider; + } + + public int getVideoState() { + return mVideoState; + } + + public StatusHints getStatusHints() { + return mStatusHints; + } + + public Bundle getExtras() { + return mExtras; + } + + public static final Parcelable.Creator<ParcelableConference> CREATOR = + new Parcelable.Creator<ParcelableConference> () { + @Override + public ParcelableConference createFromParcel(Parcel source) { + ClassLoader classLoader = ParcelableConference.class.getClassLoader(); + PhoneAccountHandle phoneAccount = source.readParcelable(classLoader); + int state = source.readInt(); + int capabilities = source.readInt(); + List<String> connectionIds = new ArrayList<>(2); + source.readList(connectionIds, classLoader); + long connectTimeMillis = source.readLong(); + IVideoProvider videoCallProvider = + IVideoProvider.Stub.asInterface(source.readStrongBinder()); + int videoState = source.readInt(); + StatusHints statusHints = source.readParcelable(classLoader); + Bundle extras = source.readBundle(classLoader); + int properties = source.readInt(); + long connectElapsedTimeMillis = source.readLong(); + + return new ParcelableConference(phoneAccount, state, capabilities, properties, + connectionIds, videoCallProvider, videoState, connectTimeMillis, + connectElapsedTimeMillis, statusHints, extras); + } + + @Override + public ParcelableConference[] newArray(int size) { + return new ParcelableConference[size]; + } + }; + + /** {@inheritDoc} */ + @Override + public int describeContents() { + return 0; + } + + /** Writes ParcelableConference object into a Parcel. */ + @Override + public void writeToParcel(Parcel destination, int flags) { + destination.writeParcelable(mPhoneAccount, 0); + destination.writeInt(mState); + destination.writeInt(mConnectionCapabilities); + destination.writeList(mConnectionIds); + destination.writeLong(mConnectTimeMillis); + destination.writeStrongBinder( + mVideoProvider != null ? mVideoProvider.asBinder() : null); + destination.writeInt(mVideoState); + destination.writeParcelable(mStatusHints, 0); + destination.writeBundle(mExtras); + destination.writeInt(mConnectionProperties); + destination.writeLong(mConnectElapsedTimeMillis); + } +} diff --git a/android/telecom/ParcelableConnection.java b/android/telecom/ParcelableConnection.java new file mode 100644 index 00000000..61d5a126 --- /dev/null +++ b/android/telecom/ParcelableConnection.java @@ -0,0 +1,329 @@ +/* + * Copyright 2014, 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.net.Uri; +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.List; + +/** + * Information about a connection that is used between Telecom and the ConnectionService. + * This is used to send initial Connection information to Telecom when the connection is + * first created. + * @hide + */ +public final class ParcelableConnection implements Parcelable { + private final PhoneAccountHandle mPhoneAccount; + private final int mState; + private final int mConnectionCapabilities; + private final int mConnectionProperties; + private final int mSupportedAudioRoutes; + private final Uri mAddress; + private final int mAddressPresentation; + private final String mCallerDisplayName; + private final int mCallerDisplayNamePresentation; + private final IVideoProvider mVideoProvider; + private final int mVideoState; + private final boolean mRingbackRequested; + private final boolean mIsVoipAudioMode; + private final long mConnectTimeMillis; + private final long mConnectElapsedTimeMillis; + private final StatusHints mStatusHints; + private final DisconnectCause mDisconnectCause; + private final List<String> mConferenceableConnectionIds; + private final Bundle mExtras; + private String mParentCallId; + + /** @hide */ + public ParcelableConnection( + PhoneAccountHandle phoneAccount, + int state, + int capabilities, + int properties, + int supportedAudioRoutes, + Uri address, + int addressPresentation, + String callerDisplayName, + int callerDisplayNamePresentation, + IVideoProvider videoProvider, + int videoState, + boolean ringbackRequested, + boolean isVoipAudioMode, + long connectTimeMillis, + long connectElapsedTimeMillis, + StatusHints statusHints, + DisconnectCause disconnectCause, + List<String> conferenceableConnectionIds, + Bundle extras, + String parentCallId) { + this(phoneAccount, state, capabilities, properties, supportedAudioRoutes, address, + addressPresentation, callerDisplayName, callerDisplayNamePresentation, + videoProvider, videoState, ringbackRequested, isVoipAudioMode, connectTimeMillis, + connectElapsedTimeMillis, statusHints, disconnectCause, conferenceableConnectionIds, + extras); + mParentCallId = parentCallId; + } + + /** @hide */ + public ParcelableConnection( + PhoneAccountHandle phoneAccount, + int state, + int capabilities, + int properties, + int supportedAudioRoutes, + Uri address, + int addressPresentation, + String callerDisplayName, + int callerDisplayNamePresentation, + IVideoProvider videoProvider, + int videoState, + boolean ringbackRequested, + boolean isVoipAudioMode, + long connectTimeMillis, + long connectElapsedTimeMillis, + StatusHints statusHints, + DisconnectCause disconnectCause, + List<String> conferenceableConnectionIds, + Bundle extras) { + mPhoneAccount = phoneAccount; + mState = state; + mConnectionCapabilities = capabilities; + mConnectionProperties = properties; + mSupportedAudioRoutes = supportedAudioRoutes; + mAddress = address; + mAddressPresentation = addressPresentation; + mCallerDisplayName = callerDisplayName; + mCallerDisplayNamePresentation = callerDisplayNamePresentation; + mVideoProvider = videoProvider; + mVideoState = videoState; + mRingbackRequested = ringbackRequested; + mIsVoipAudioMode = isVoipAudioMode; + mConnectTimeMillis = connectTimeMillis; + mConnectElapsedTimeMillis = connectElapsedTimeMillis; + mStatusHints = statusHints; + mDisconnectCause = disconnectCause; + mConferenceableConnectionIds = conferenceableConnectionIds; + mExtras = extras; + mParentCallId = null; + } + + public PhoneAccountHandle getPhoneAccount() { + return mPhoneAccount; + } + + public int getState() { + return mState; + } + + /** + * Returns the current connection capabilities bit-mask. Connection capabilities are defined as + * {@code CAPABILITY_*} constants in {@link Connection}. + * + * @return Bit-mask containing capabilities of the connection. + */ + public int getConnectionCapabilities() { + return mConnectionCapabilities; + } + + /** + * Returns the current connection properties bit-mask. Connection properties are defined as + * {@code PROPERTY_*} constants in {@link Connection}. + * + * @return Bit-mask containing properties of the connection. + */ + public int getConnectionProperties() { + return mConnectionProperties; + } + + public int getSupportedAudioRoutes() { + return mSupportedAudioRoutes; + } + + public Uri getHandle() { + return mAddress; + } + + public int getHandlePresentation() { + return mAddressPresentation; + } + + public String getCallerDisplayName() { + return mCallerDisplayName; + } + + public int getCallerDisplayNamePresentation() { + return mCallerDisplayNamePresentation; + } + + public IVideoProvider getVideoProvider() { + return mVideoProvider; + } + + public int getVideoState() { + return mVideoState; + } + + public boolean isRingbackRequested() { + return mRingbackRequested; + } + + public boolean getIsVoipAudioMode() { + return mIsVoipAudioMode; + } + + public long getConnectTimeMillis() { + return mConnectTimeMillis; + } + + public long getConnectElapsedTimeMillis() { + return mConnectElapsedTimeMillis; + } + + public final StatusHints getStatusHints() { + return mStatusHints; + } + + public final DisconnectCause getDisconnectCause() { + return mDisconnectCause; + } + + public final List<String> getConferenceableConnectionIds() { + return mConferenceableConnectionIds; + } + + public final Bundle getExtras() { + return mExtras; + } + + public final String getParentCallId() { + return mParentCallId; + } + + @Override + public String toString() { + return new StringBuilder() + .append("ParcelableConnection [act:") + .append(mPhoneAccount) + .append("], state:") + .append(mState) + .append(", capabilities:") + .append(Connection.capabilitiesToString(mConnectionCapabilities)) + .append(", properties:") + .append(Connection.propertiesToString(mConnectionProperties)) + .append(", extras:") + .append(mExtras) + .append(", parent:") + .append(mParentCallId) + .toString(); + } + + public static final Parcelable.Creator<ParcelableConnection> CREATOR = + new Parcelable.Creator<ParcelableConnection> () { + @Override + public ParcelableConnection createFromParcel(Parcel source) { + ClassLoader classLoader = ParcelableConnection.class.getClassLoader(); + + PhoneAccountHandle phoneAccount = source.readParcelable(classLoader); + int state = source.readInt(); + int capabilities = source.readInt(); + Uri address = source.readParcelable(classLoader); + int addressPresentation = source.readInt(); + String callerDisplayName = source.readString(); + int callerDisplayNamePresentation = source.readInt(); + IVideoProvider videoCallProvider = + IVideoProvider.Stub.asInterface(source.readStrongBinder()); + int videoState = source.readInt(); + boolean ringbackRequested = source.readByte() == 1; + boolean audioModeIsVoip = source.readByte() == 1; + long connectTimeMillis = source.readLong(); + StatusHints statusHints = source.readParcelable(classLoader); + DisconnectCause disconnectCause = source.readParcelable(classLoader); + List<String> conferenceableConnectionIds = new ArrayList<>(); + source.readStringList(conferenceableConnectionIds); + Bundle extras = Bundle.setDefusable(source.readBundle(classLoader), true); + int properties = source.readInt(); + int supportedAudioRoutes = source.readInt(); + String parentCallId = source.readString(); + long connectElapsedTimeMillis = source.readLong(); + + return new ParcelableConnection( + phoneAccount, + state, + capabilities, + properties, + supportedAudioRoutes, + address, + addressPresentation, + callerDisplayName, + callerDisplayNamePresentation, + videoCallProvider, + videoState, + ringbackRequested, + audioModeIsVoip, + connectTimeMillis, + connectElapsedTimeMillis, + statusHints, + disconnectCause, + conferenceableConnectionIds, + extras, + parentCallId); + } + + @Override + public ParcelableConnection[] newArray(int size) { + return new ParcelableConnection[size]; + } + }; + + /** {@inheritDoc} */ + @Override + public int describeContents() { + return 0; + } + + /** Writes ParcelableConnection object into a Parcel. */ + @Override + public void writeToParcel(Parcel destination, int flags) { + destination.writeParcelable(mPhoneAccount, 0); + destination.writeInt(mState); + destination.writeInt(mConnectionCapabilities); + destination.writeParcelable(mAddress, 0); + destination.writeInt(mAddressPresentation); + destination.writeString(mCallerDisplayName); + destination.writeInt(mCallerDisplayNamePresentation); + destination.writeStrongBinder( + mVideoProvider != null ? mVideoProvider.asBinder() : null); + destination.writeInt(mVideoState); + destination.writeByte((byte) (mRingbackRequested ? 1 : 0)); + destination.writeByte((byte) (mIsVoipAudioMode ? 1 : 0)); + destination.writeLong(mConnectTimeMillis); + destination.writeParcelable(mStatusHints, 0); + destination.writeParcelable(mDisconnectCause, 0); + destination.writeStringList(mConferenceableConnectionIds); + destination.writeBundle(mExtras); + destination.writeInt(mConnectionProperties); + destination.writeInt(mSupportedAudioRoutes); + destination.writeString(mParentCallId); + destination.writeLong(mConnectElapsedTimeMillis); + } +} diff --git a/android/telecom/ParcelableRttCall.java b/android/telecom/ParcelableRttCall.java new file mode 100644 index 00000000..763e48b1 --- /dev/null +++ b/android/telecom/ParcelableRttCall.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2017 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.Parcel; +import android.os.ParcelFileDescriptor; +import android.os.Parcelable; + +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.nio.charset.StandardCharsets; + +/** + * Data container for information associated with the RTT connection on a call. + * @hide + */ +public class ParcelableRttCall implements Parcelable { + private final int mRttMode; + private final ParcelFileDescriptor mTransmitStream; + private final ParcelFileDescriptor mReceiveStream; + + public ParcelableRttCall( + int rttMode, + ParcelFileDescriptor transmitStream, + ParcelFileDescriptor receiveStream) { + mRttMode = rttMode; + mTransmitStream = transmitStream; + mReceiveStream = receiveStream; + } + + protected ParcelableRttCall(Parcel in) { + mRttMode = in.readInt(); + mTransmitStream = in.readParcelable(ParcelFileDescriptor.class.getClassLoader()); + mReceiveStream = in.readParcelable(ParcelFileDescriptor.class.getClassLoader()); + } + + public static final Creator<ParcelableRttCall> CREATOR = new Creator<ParcelableRttCall>() { + @Override + public ParcelableRttCall createFromParcel(Parcel in) { + return new ParcelableRttCall(in); + } + + @Override + public ParcelableRttCall[] newArray(int size) { + return new ParcelableRttCall[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mRttMode); + dest.writeParcelable(mTransmitStream, flags); + dest.writeParcelable(mReceiveStream, flags); + } + + public int getRttMode() { + return mRttMode; + } + + public ParcelFileDescriptor getReceiveStream() { + return mReceiveStream; + } + + public ParcelFileDescriptor getTransmitStream() { + return mTransmitStream; + } +} diff --git a/android/telecom/Phone.java b/android/telecom/Phone.java new file mode 100644 index 00000000..066f6c26 --- /dev/null +++ b/android/telecom/Phone.java @@ -0,0 +1,391 @@ +/* + * Copyright (C) 2013 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.SystemApi; +import android.os.Bundle; +import android.util.ArrayMap; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * A unified virtual device providing a means of voice (and other) communication on a device. + * + * @hide + * @deprecated Use {@link InCallService} directly instead of using this class. + */ +@SystemApi +@Deprecated +public final class Phone { + + public abstract static class Listener { + /** + * Called when the audio state changes. + * + * @param phone The {@code Phone} calling this method. + * @param audioState The new {@link AudioState}. + * + * @deprecated Use {@link #onCallAudioStateChanged(Phone, CallAudioState)} instead. + */ + @Deprecated + public void onAudioStateChanged(Phone phone, AudioState audioState) { } + + /** + * Called when the audio state changes. + * + * @param phone The {@code Phone} calling this method. + * @param callAudioState The new {@link CallAudioState}. + */ + public void onCallAudioStateChanged(Phone phone, CallAudioState callAudioState) { } + + /** + * 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. + * + * @param phone The {@code Phone} calling this method. + * @param showDialpad If true, put up the dialpad when the screen is shown. + */ + public void onBringToForeground(Phone phone, boolean showDialpad) { } + + /** + * Called when a {@code Call} has been added to this in-call session. The in-call user + * experience should add necessary state listeners to the specified {@code Call} and + * immediately start to show the user information about the existence + * and nature of this {@code Call}. Subsequent invocations of {@link #getCalls()} will + * include this {@code Call}. + * + * @param phone The {@code Phone} calling this method. + * @param call A newly added {@code Call}. + */ + public void onCallAdded(Phone phone, Call call) { } + + /** + * Called when a {@code Call} has been removed from this in-call session. The in-call user + * experience should remove any state listeners from the specified {@code Call} and + * immediately stop displaying any information about this {@code Call}. + * Subsequent invocations of {@link #getCalls()} will no longer include this {@code Call}. + * + * @param phone The {@code Phone} calling this method. + * @param call A newly removed {@code Call}. + */ + public void onCallRemoved(Phone phone, Call call) { } + + /** + * Called when the {@code Phone} ability to add more calls changes. If the phone cannot + * support more calls then {@code canAddCall} is set to {@code false}. If it can, then it + * is set to {@code true}. + * + * @param phone The {@code Phone} calling this method. + * @param canAddCall Indicates whether an additional call can be added. + */ + public void onCanAddCallChanged(Phone phone, boolean canAddCall) { } + + /** + * Called to silence the ringer if a ringing call exists. + * + * @param phone The {@code Phone} calling this method. + */ + public void onSilenceRinger(Phone phone) { } + } + + // A Map allows us to track each Call by its Telecom-specified call ID + private final Map<String, Call> mCallByTelecomCallId = new ArrayMap<>(); + + // A List allows us to keep the Calls in a stable iteration order so that casually developed + // user interface components do not incur any spurious jank + private final List<Call> mCalls = new CopyOnWriteArrayList<>(); + + // An unmodifiable view of the above List can be safely shared with subclass implementations + private final List<Call> mUnmodifiableCalls = Collections.unmodifiableList(mCalls); + + private final InCallAdapter mInCallAdapter; + + private CallAudioState mCallAudioState; + + private final List<Listener> mListeners = new CopyOnWriteArrayList<>(); + + private boolean mCanAddCall = true; + + private final String mCallingPackage; + + /** + * The Target SDK version of the InCallService implementation. + */ + private final int mTargetSdkVersion; + + Phone(InCallAdapter adapter, String callingPackage, int targetSdkVersion) { + mInCallAdapter = adapter; + mCallingPackage = callingPackage; + mTargetSdkVersion = targetSdkVersion; + } + + final void internalAddCall(ParcelableCall parcelableCall) { + Call call = new Call(this, parcelableCall.getId(), mInCallAdapter, + parcelableCall.getState(), mCallingPackage, mTargetSdkVersion); + mCallByTelecomCallId.put(parcelableCall.getId(), call); + mCalls.add(call); + checkCallTree(parcelableCall); + call.internalUpdate(parcelableCall, mCallByTelecomCallId); + fireCallAdded(call); + } + + final void internalRemoveCall(Call call) { + mCallByTelecomCallId.remove(call.internalGetCallId()); + mCalls.remove(call); + + InCallService.VideoCall videoCall = call.getVideoCall(); + if (videoCall != null) { + videoCall.destroy(); + } + fireCallRemoved(call); + } + + final void internalUpdateCall(ParcelableCall parcelableCall) { + Call call = mCallByTelecomCallId.get(parcelableCall.getId()); + if (call != null) { + checkCallTree(parcelableCall); + call.internalUpdate(parcelableCall, mCallByTelecomCallId); + } + } + + final void internalSetPostDialWait(String telecomId, String remaining) { + Call call = mCallByTelecomCallId.get(telecomId); + if (call != null) { + call.internalSetPostDialWait(remaining); + } + } + + final void internalCallAudioStateChanged(CallAudioState callAudioState) { + if (!Objects.equals(mCallAudioState, callAudioState)) { + mCallAudioState = callAudioState; + fireCallAudioStateChanged(callAudioState); + } + } + + final Call internalGetCallByTelecomId(String telecomId) { + return mCallByTelecomCallId.get(telecomId); + } + + final void internalBringToForeground(boolean showDialpad) { + fireBringToForeground(showDialpad); + } + + final void internalSetCanAddCall(boolean canAddCall) { + if (mCanAddCall != canAddCall) { + mCanAddCall = canAddCall; + fireCanAddCallChanged(canAddCall); + } + } + + final void internalSilenceRinger() { + fireSilenceRinger(); + } + + final void internalOnConnectionEvent(String telecomId, String event, Bundle extras) { + Call call = mCallByTelecomCallId.get(telecomId); + if (call != null) { + call.internalOnConnectionEvent(event, extras); + } + } + + final void internalOnRttUpgradeRequest(String callId, int requestId) { + Call call = mCallByTelecomCallId.get(callId); + if (call != null) { + call.internalOnRttUpgradeRequest(requestId); + } + } + + final void internalOnRttInitiationFailure(String callId, int reason) { + Call call = mCallByTelecomCallId.get(callId); + if (call != null) { + call.internalOnRttInitiationFailure(reason); + } + } + + /** + * Called to destroy the phone and cleanup any lingering calls. + */ + final void destroy() { + for (Call call : mCalls) { + InCallService.VideoCall videoCall = call.getVideoCall(); + if (videoCall != null) { + videoCall.destroy(); + } + if (call.getState() != Call.STATE_DISCONNECTED) { + call.internalSetDisconnected(); + } + } + } + + /** + * Adds a listener to this {@code Phone}. + * + * @param listener A {@code Listener} object. + */ + public final void addListener(Listener listener) { + mListeners.add(listener); + } + + /** + * Removes a listener from this {@code Phone}. + * + * @param listener A {@code Listener} object. + */ + public final void removeListener(Listener listener) { + if (listener != null) { + mListeners.remove(listener); + } + } + + /** + * Obtains the current list of {@code Call}s to be displayed by this in-call experience. + * + * @return A list of the relevant {@code Call}s. + */ + public final List<Call> getCalls() { + return mUnmodifiableCalls; + } + + /** + * Returns if the {@code Phone} can support additional calls. + * + * @return Whether the phone supports adding more calls. + */ + public final boolean canAddCall() { + return mCanAddCall; + } + + /** + * Sets the microphone mute state. When this request is honored, there will be change to + * the {@link #getAudioState()}. + * + * @param state {@code true} if the microphone should be muted; {@code false} otherwise. + */ + public final void setMuted(boolean state) { + mInCallAdapter.mute(state); + } + + /** + * Sets the audio route (speaker, bluetooth, etc...). When this request is honored, there will + * be change to the {@link #getAudioState()}. + * + * @param route The audio route to use. + */ + public final void setAudioRoute(int route) { + mInCallAdapter.setAudioRoute(route); + } + + /** + * 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 + * that do not have a proximity sensor. + * + * @hide + */ + public final void setProximitySensorOn() { + mInCallAdapter.turnProximitySensorOn(); + } + + /** + * Turns the proximity sensor off. When this request is made, the proximity sensor will + * become inactive, and no longer affect the touch screen and display. This operation is a + * no-op on devices that do not have a proximity sensor. + * + * @param screenOnImmediately If true, the screen will be turned on immediately if it was + * previously off. Otherwise, the screen will only be turned on after the proximity sensor + * is no longer triggered. + * + * @hide + */ + public final void setProximitySensorOff(boolean screenOnImmediately) { + mInCallAdapter.turnProximitySensorOff(screenOnImmediately); + } + + /** + * Obtains the current phone call audio state of the {@code Phone}. + * + * @return An object encapsulating the audio state. + * @deprecated Use {@link #getCallAudioState()} instead. + */ + @Deprecated + public final AudioState getAudioState() { + return new AudioState(mCallAudioState); + } + + /** + * Obtains the current phone call audio state of the {@code Phone}. + * + * @return An object encapsulating the audio state. + */ + public final CallAudioState getCallAudioState() { + return mCallAudioState; + } + + private void fireCallAdded(Call call) { + for (Listener listener : mListeners) { + listener.onCallAdded(this, call); + } + } + + private void fireCallRemoved(Call call) { + for (Listener listener : mListeners) { + listener.onCallRemoved(this, call); + } + } + + private void fireCallAudioStateChanged(CallAudioState audioState) { + for (Listener listener : mListeners) { + listener.onCallAudioStateChanged(this, audioState); + listener.onAudioStateChanged(this, new AudioState(audioState)); + } + } + + private void fireBringToForeground(boolean showDialpad) { + for (Listener listener : mListeners) { + listener.onBringToForeground(this, showDialpad); + } + } + + private void fireCanAddCallChanged(boolean canAddCall) { + for (Listener listener : mListeners) { + listener.onCanAddCallChanged(this, canAddCall); + } + } + + private void fireSilenceRinger() { + for (Listener listener : mListeners) { + listener.onSilenceRinger(this); + } + } + + private void checkCallTree(ParcelableCall parcelableCall) { + if (parcelableCall.getChildCallIds() != null) { + for (int i = 0; i < parcelableCall.getChildCallIds().size(); i++) { + if (!mCallByTelecomCallId.containsKey(parcelableCall.getChildCallIds().get(i))) { + Log.wtf(this, "ParcelableCall %s has nonexistent child %s", + parcelableCall.getId(), parcelableCall.getChildCallIds().get(i)); + } + } + } + } +} diff --git a/android/telecom/PhoneAccount.java b/android/telecom/PhoneAccount.java new file mode 100644 index 00000000..691e7cf1 --- /dev/null +++ b/android/telecom/PhoneAccount.java @@ -0,0 +1,992 @@ +/* + * Copyright (C) 2014 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.SystemApi; +import android.graphics.drawable.Icon; +import android.net.Uri; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; + +import java.lang.String; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Represents a distinct method to place or receive a phone call. Apps which can place calls and + * want those calls to be integrated into the dialer and in-call UI should build an instance of + * this class and register it with the system using {@link TelecomManager}. + * <p> + * {@link TelecomManager} uses registered {@link PhoneAccount}s to present the user with + * alternative options when placing a phone call. When building a {@link PhoneAccount}, the app + * should supply a valid {@link PhoneAccountHandle} that references the connection service + * implementation Telecom will use to interact with the app. + */ +public final class PhoneAccount implements Parcelable { + + /** + * {@link PhoneAccount} extras key (see {@link PhoneAccount#getExtras()}) which determines the + * sort order for {@link PhoneAccount}s from the same + * {@link android.telecom.ConnectionService}. + * @hide + */ + public static final String EXTRA_SORT_ORDER = + "android.telecom.extra.SORT_ORDER"; + + /** + * {@link PhoneAccount} extras key (see {@link PhoneAccount#getExtras()}) which determines the + * maximum permitted length of a call subject specified via the + * {@link TelecomManager#EXTRA_CALL_SUBJECT} extra on an + * {@link android.content.Intent#ACTION_CALL} intent. Ultimately a {@link ConnectionService} is + * responsible for enforcing the maximum call subject length when sending the message, however + * this extra is provided so that the user interface can proactively limit the length of the + * call subject as the user types it. + */ + public static final String EXTRA_CALL_SUBJECT_MAX_LENGTH = + "android.telecom.extra.CALL_SUBJECT_MAX_LENGTH"; + + /** + * {@link PhoneAccount} extras key (see {@link PhoneAccount#getExtras()}) which determines the + * character encoding to be used when determining the length of messages. + * The user interface can use this when determining the number of characters the user may type + * in a call subject. If empty-string, the call subject message size limit will be enforced on + * a 1:1 basis. That is, each character will count towards the messages size limit as a single + * character. If a character encoding is specified, the message size limit will be based on the + * number of bytes in the message per the specified encoding. See + * {@link #EXTRA_CALL_SUBJECT_MAX_LENGTH} for more information on the call subject maximum + * length. + */ + public static final String EXTRA_CALL_SUBJECT_CHARACTER_ENCODING = + "android.telecom.extra.CALL_SUBJECT_CHARACTER_ENCODING"; + + /** + * Indicating flag for phone account whether to use voip audio mode for voip calls + * @hide + */ + public static final String EXTRA_ALWAYS_USE_VOIP_AUDIO_MODE = + "android.telecom.extra.ALWAYS_USE_VOIP_AUDIO_MODE"; + + /** + * Boolean {@link PhoneAccount} extras key (see {@link PhoneAccount#getExtras()}) which + * indicates whether this {@link PhoneAccount} is capable of supporting a request to handover a + * connection (see {@link android.telecom.Call#EVENT_REQUEST_HANDOVER}) to this + * {@link PhoneAccount} from a {@link PhoneAccount} specifying + * {@link #EXTRA_SUPPORTS_HANDOVER_FROM}. + * <p> + * A handover request is initiated by the user from the default dialer app to indicate a desire + * to handover a call from one {@link PhoneAccount}/{@link ConnectionService} to another. + * @hide + */ + public static final String EXTRA_SUPPORTS_HANDOVER_TO = + "android.telecom.extra.SUPPORTS_HANDOVER_TO"; + + /** + * Boolean {@link PhoneAccount} extras key (see {@link PhoneAccount#getExtras()}) which + * indicates whether this {@link PhoneAccount} supports using a fallback if video calling is + * not available. This extra is for device level support, {@link + * android.telephony.CarrierConfigManager#KEY_ALLOW_VIDEO_CALLING_FALLBACK_BOOL} should also + * be checked to ensure it is not disabled by individual carrier. + * + * @hide + */ + public static final String EXTRA_SUPPORTS_VIDEO_CALLING_FALLBACK = + "android.telecom.extra.SUPPORTS_VIDEO_CALLING_FALLBACK"; + + /** + * Boolean {@link PhoneAccount} extras key (see {@link PhoneAccount#getExtras()}) which + * indicates whether this {@link PhoneAccount} is capable of supporting a request to handover a + * connection from this {@link PhoneAccount} to another {@link PhoneAccount}. + * (see {@link android.telecom.Call#EVENT_REQUEST_HANDOVER}) which specifies + * {@link #EXTRA_SUPPORTS_HANDOVER_TO}. + * <p> + * A handover request is initiated by the user from the default dialer app to indicate a desire + * to handover a call from one {@link PhoneAccount}/{@link ConnectionService} to another. + * @hide + */ + public static final String EXTRA_SUPPORTS_HANDOVER_FROM = + "android.telecom.extra.SUPPORTS_HANDOVER_FROM"; + + + /** + * Boolean {@link PhoneAccount} extras key (see {@link PhoneAccount#getExtras()}) which + * indicates whether a Self-Managed {@link PhoneAccount} should log its calls to the call log. + * Self-Managed {@link PhoneAccount}s are responsible for their own notifications, so the system + * will not create a notification when a missed call is logged. + * <p> + * By default, Self-Managed {@link PhoneAccount}s do not log their calls to the call log. + * Setting this extra to {@code true} provides a means for them to log their calls. + * @hide + */ + public static final String EXTRA_LOG_SELF_MANAGED_CALLS = + "android.telecom.extra.LOG_SELF_MANAGED_CALLS"; + + /** + * Flag indicating that this {@code PhoneAccount} can act as a connection manager for + * other connections. The {@link ConnectionService} associated with this {@code PhoneAccount} + * will be allowed to manage phone calls including using its own proprietary phone-call + * implementation (like VoIP calling) to make calls instead of the telephony stack. + * <p> + * When a user opts to place a call using the SIM-based telephony stack, the + * {@link ConnectionService} associated with this {@code PhoneAccount} will be attempted first + * if the user has explicitly selected it to be used as the default connection manager. + * <p> + * See {@link #getCapabilities} + */ + public static final int CAPABILITY_CONNECTION_MANAGER = 0x1; + + /** + * Flag indicating that this {@code PhoneAccount} can make phone calls in place of + * traditional SIM-based telephony calls. This account will be treated as a distinct method + * for placing calls alongside the traditional SIM-based telephony stack. This flag is + * distinct from {@link #CAPABILITY_CONNECTION_MANAGER} in that it is not allowed to manage + * or place calls from the built-in telephony stack. + * <p> + * See {@link #getCapabilities} + * <p> + */ + public static final int CAPABILITY_CALL_PROVIDER = 0x2; + + /** + * Flag indicating that this {@code PhoneAccount} represents a built-in PSTN SIM + * subscription. + * <p> + * Only the Android framework can register a {@code PhoneAccount} having this capability. + * <p> + * See {@link #getCapabilities} + */ + public static final int CAPABILITY_SIM_SUBSCRIPTION = 0x4; + + /** + * Flag indicating that this {@code PhoneAccount} is currently able to place video calls. + * <p> + * See also {@link #CAPABILITY_SUPPORTS_VIDEO_CALLING} which indicates whether the + * {@code PhoneAccount} supports placing video calls. + * <p> + * See {@link #getCapabilities} + */ + public static final int CAPABILITY_VIDEO_CALLING = 0x8; + + /** + * Flag indicating that this {@code PhoneAccount} is capable of placing emergency calls. + * By default all PSTN {@code PhoneAccount}s are capable of placing emergency calls. + * <p> + * See {@link #getCapabilities} + */ + public static final int CAPABILITY_PLACE_EMERGENCY_CALLS = 0x10; + + /** + * Flag indicating that this {@code PhoneAccount} is capable of being used by all users. This + * should only be used by system apps (and will be ignored for all other apps trying to use it). + * <p> + * See {@link #getCapabilities} + * @hide + */ + @SystemApi + public static final int CAPABILITY_MULTI_USER = 0x20; + + /** + * Flag indicating that this {@code PhoneAccount} supports a subject for Calls. This means a + * caller is able to specify a short subject line for an outgoing call. A capable receiving + * device displays the call subject on the incoming call screen. + * <p> + * See {@link #getCapabilities} + */ + public static final int CAPABILITY_CALL_SUBJECT = 0x40; + + /** + * Flag indicating that this {@code PhoneAccount} should only be used for emergency calls. + * <p> + * See {@link #getCapabilities} + * @hide + */ + public static final int CAPABILITY_EMERGENCY_CALLS_ONLY = 0x80; + + /** + * Flag indicating that for this {@code PhoneAccount}, the ability to make a video call to a + * number relies on presence. Should only be set if the {@code PhoneAccount} also has + * {@link #CAPABILITY_VIDEO_CALLING}. + * <p> + * When set, the {@link ConnectionService} is responsible for toggling the + * {@link android.provider.ContactsContract.Data#CARRIER_PRESENCE_VT_CAPABLE} bit on the + * {@link android.provider.ContactsContract.Data#CARRIER_PRESENCE} column to indicate whether + * a contact's phone number supports video calling. + * <p> + * See {@link #getCapabilities} + */ + public static final int CAPABILITY_VIDEO_CALLING_RELIES_ON_PRESENCE = 0x100; + + /** + * Flag indicating that for this {@link PhoneAccount}, emergency video calling is allowed. + * <p> + * When set, Telecom will allow emergency video calls to be placed. When not set, Telecom will + * convert all outgoing video calls to emergency numbers to audio-only. + * @hide + */ + public static final int CAPABILITY_EMERGENCY_VIDEO_CALLING = 0x200; + + /** + * Flag indicating that this {@link PhoneAccount} supports video calling. + * This is not an indication that the {@link PhoneAccount} is currently able to make a video + * call, but rather that it has the ability to make video calls (but not necessarily at this + * time). + * <p> + * Whether a {@link PhoneAccount} can make a video call is ultimately controlled by + * {@link #CAPABILITY_VIDEO_CALLING}, which indicates whether the {@link PhoneAccount} is + * currently capable of making a video call. Consider a case where, for example, a + * {@link PhoneAccount} supports making video calls (e.g. + * {@link #CAPABILITY_SUPPORTS_VIDEO_CALLING}), but a current lack of network connectivity + * prevents video calls from being made (e.g. {@link #CAPABILITY_VIDEO_CALLING}). + * <p> + * See {@link #getCapabilities} + */ + public static final int CAPABILITY_SUPPORTS_VIDEO_CALLING = 0x400; + + /** + * Flag indicating that this {@link PhoneAccount} is responsible for managing its own + * {@link Connection}s. This type of {@link PhoneAccount} is ideal for use with standalone + * calling apps which do not wish to use the default phone app for {@link Connection} UX, + * but which want to leverage the call and audio routing capabilities of the Telecom framework. + * <p> + * When set, {@link Connection}s created by the self-managed {@link ConnectionService} will not + * be surfaced to implementations of the {@link InCallService} API. Thus it is the + * responsibility of a self-managed {@link ConnectionService} to provide a user interface for + * its {@link Connection}s. + * <p> + * Self-managed {@link Connection}s will, however, be displayed on connected Bluetooth devices. + */ + public static final int CAPABILITY_SELF_MANAGED = 0x800; + + /** + * Flag indicating that this {@link PhoneAccount} is capable of making a call with an + * RTT (Real-time text) session. + * When set, Telecom will attempt to open an RTT session on outgoing calls that specify + * that they should be placed with an RTT session , and the in-call app will be displayed + * with text entry fields for RTT. Likewise, the in-call app can request that an RTT + * session be opened during a call if this bit is set. + */ + public static final int CAPABILITY_RTT = 0x1000; + + /* NEXT CAPABILITY: 0x2000 */ + + /** + * URI scheme for telephone number URIs. + */ + public static final String SCHEME_TEL = "tel"; + + /** + * URI scheme for voicemail URIs. + */ + public static final String SCHEME_VOICEMAIL = "voicemail"; + + /** + * URI scheme for SIP URIs. + */ + public static final String SCHEME_SIP = "sip"; + + /** + * Indicating no icon tint is set. + * @hide + */ + public static final int NO_ICON_TINT = 0; + + /** + * Indicating no hightlight color is set. + */ + public static final int NO_HIGHLIGHT_COLOR = 0; + + /** + * Indicating no resource ID is set. + */ + public static final int NO_RESOURCE_ID = -1; + + private final PhoneAccountHandle mAccountHandle; + private final Uri mAddress; + private final Uri mSubscriptionAddress; + private final int mCapabilities; + private final int mHighlightColor; + private final CharSequence mLabel; + private final CharSequence mShortDescription; + private final List<String> mSupportedUriSchemes; + private final int mSupportedAudioRoutes; + private final Icon mIcon; + private final Bundle mExtras; + private boolean mIsEnabled; + private String mGroupId; + + /** + * Helper class for creating a {@link PhoneAccount}. + */ + public static class Builder { + + private PhoneAccountHandle mAccountHandle; + private Uri mAddress; + private Uri mSubscriptionAddress; + private int mCapabilities; + private int mSupportedAudioRoutes = CallAudioState.ROUTE_ALL; + private int mHighlightColor = NO_HIGHLIGHT_COLOR; + private CharSequence mLabel; + private CharSequence mShortDescription; + private List<String> mSupportedUriSchemes = new ArrayList<String>(); + private Icon mIcon; + private Bundle mExtras; + private boolean mIsEnabled = false; + private String mGroupId = ""; + + /** + * Creates a builder with the specified {@link PhoneAccountHandle} and label. + */ + public Builder(PhoneAccountHandle accountHandle, CharSequence label) { + this.mAccountHandle = accountHandle; + this.mLabel = label; + } + + /** + * Creates an instance of the {@link PhoneAccount.Builder} from an existing + * {@link PhoneAccount}. + * + * @param phoneAccount The {@link PhoneAccount} used to initialize the builder. + */ + public Builder(PhoneAccount phoneAccount) { + mAccountHandle = phoneAccount.getAccountHandle(); + mAddress = phoneAccount.getAddress(); + mSubscriptionAddress = phoneAccount.getSubscriptionAddress(); + mCapabilities = phoneAccount.getCapabilities(); + mHighlightColor = phoneAccount.getHighlightColor(); + mLabel = phoneAccount.getLabel(); + mShortDescription = phoneAccount.getShortDescription(); + mSupportedUriSchemes.addAll(phoneAccount.getSupportedUriSchemes()); + mIcon = phoneAccount.getIcon(); + mIsEnabled = phoneAccount.isEnabled(); + mExtras = phoneAccount.getExtras(); + mGroupId = phoneAccount.getGroupId(); + mSupportedAudioRoutes = phoneAccount.getSupportedAudioRoutes(); + } + + /** + * Sets the label. See {@link PhoneAccount#getLabel()}. + * + * @param label The label of the phone account. + * @return The builder. + * @hide + */ + public Builder setLabel(CharSequence label) { + this.mLabel = label; + return this; + } + + /** + * Sets the address. See {@link PhoneAccount#getAddress}. + * + * @param value The address of the phone account. + * @return The builder. + */ + public Builder setAddress(Uri value) { + this.mAddress = value; + return this; + } + + /** + * Sets the subscription address. See {@link PhoneAccount#getSubscriptionAddress}. + * + * @param value The subscription address. + * @return The builder. + */ + public Builder setSubscriptionAddress(Uri value) { + this.mSubscriptionAddress = value; + return this; + } + + /** + * Sets the capabilities. See {@link PhoneAccount#getCapabilities}. + * + * @param value The capabilities to set. + * @return The builder. + */ + public Builder setCapabilities(int value) { + this.mCapabilities = value; + return this; + } + + /** + * Sets the icon. See {@link PhoneAccount#getIcon}. + * + * @param icon The icon to set. + */ + public Builder setIcon(Icon icon) { + mIcon = icon; + return this; + } + + /** + * Sets the highlight color. See {@link PhoneAccount#getHighlightColor}. + * + * @param value The highlight color. + * @return The builder. + */ + public Builder setHighlightColor(int value) { + this.mHighlightColor = value; + return this; + } + + /** + * Sets the short description. See {@link PhoneAccount#getShortDescription}. + * + * @param value The short description. + * @return The builder. + */ + public Builder setShortDescription(CharSequence value) { + this.mShortDescription = value; + return this; + } + + /** + * Specifies an additional URI scheme supported by the {@link PhoneAccount}. + * + * @param uriScheme The URI scheme. + * @return The builder. + */ + public Builder addSupportedUriScheme(String uriScheme) { + if (!TextUtils.isEmpty(uriScheme) && !mSupportedUriSchemes.contains(uriScheme)) { + this.mSupportedUriSchemes.add(uriScheme); + } + return this; + } + + /** + * Specifies the URI schemes supported by the {@link PhoneAccount}. + * + * @param uriSchemes The URI schemes. + * @return The builder. + */ + public Builder setSupportedUriSchemes(List<String> uriSchemes) { + mSupportedUriSchemes.clear(); + + if (uriSchemes != null && !uriSchemes.isEmpty()) { + for (String uriScheme : uriSchemes) { + addSupportedUriScheme(uriScheme); + } + } + return this; + } + + /** + * Specifies the extras associated with the {@link PhoneAccount}. + * <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. + * + * @param extras + * @return + */ + public Builder setExtras(Bundle extras) { + mExtras = extras; + return this; + } + + /** + * Sets the enabled state of the phone account. + * + * @param isEnabled The enabled state. + * @return The builder. + * @hide + */ + public Builder setIsEnabled(boolean isEnabled) { + mIsEnabled = isEnabled; + return this; + } + + /** + * Sets the group Id of the {@link PhoneAccount}. When a new {@link PhoneAccount} is + * registered to Telecom, it will replace another {@link PhoneAccount} that is already + * registered in Telecom and take on the current user defaults and enabled status. There can + * only be one {@link PhoneAccount} with a non-empty group number registered to Telecom at a + * time. By default, there is no group Id for a {@link PhoneAccount} (an empty String). Only + * grouped {@link PhoneAccount}s with the same {@link ConnectionService} can be replaced. + * @param groupId The group Id of the {@link PhoneAccount} that will replace any other + * registered {@link PhoneAccount} in Telecom with the same Group Id. + * @return The builder + * @hide + */ + public Builder setGroupId(String groupId) { + if (groupId != null) { + mGroupId = groupId; + } else { + mGroupId = ""; + } + return this; + } + + /** + * Sets the audio routes supported by this {@link PhoneAccount}. + * + * @param routes bit mask of available routes. + * @return The builder. + * @hide + */ + public Builder setSupportedAudioRoutes(int routes) { + mSupportedAudioRoutes = routes; + return this; + } + + /** + * Creates an instance of a {@link PhoneAccount} based on the current builder settings. + * + * @return The {@link PhoneAccount}. + */ + public PhoneAccount build() { + // If no supported URI schemes were defined, assume "tel" is supported. + if (mSupportedUriSchemes.isEmpty()) { + addSupportedUriScheme(SCHEME_TEL); + } + + return new PhoneAccount( + mAccountHandle, + mAddress, + mSubscriptionAddress, + mCapabilities, + mIcon, + mHighlightColor, + mLabel, + mShortDescription, + mSupportedUriSchemes, + mExtras, + mSupportedAudioRoutes, + mIsEnabled, + mGroupId); + } + } + + private PhoneAccount( + PhoneAccountHandle account, + Uri address, + Uri subscriptionAddress, + int capabilities, + Icon icon, + int highlightColor, + CharSequence label, + CharSequence shortDescription, + List<String> supportedUriSchemes, + Bundle extras, + int supportedAudioRoutes, + boolean isEnabled, + String groupId) { + mAccountHandle = account; + mAddress = address; + mSubscriptionAddress = subscriptionAddress; + mCapabilities = capabilities; + mIcon = icon; + mHighlightColor = highlightColor; + mLabel = label; + mShortDescription = shortDescription; + mSupportedUriSchemes = Collections.unmodifiableList(supportedUriSchemes); + mExtras = extras; + mSupportedAudioRoutes = supportedAudioRoutes; + mIsEnabled = isEnabled; + mGroupId = groupId; + } + + public static Builder builder( + PhoneAccountHandle accountHandle, + CharSequence label) { + return new Builder(accountHandle, label); + } + + /** + * Returns a builder initialized with the current {@link PhoneAccount} instance. + * + * @return The builder. + */ + public Builder toBuilder() { return new Builder(this); } + + /** + * The unique identifier of this {@code PhoneAccount}. + * + * @return A {@code PhoneAccountHandle}. + */ + public PhoneAccountHandle getAccountHandle() { + return mAccountHandle; + } + + /** + * The address (e.g., a phone number) associated with this {@code PhoneAccount}. This + * represents the destination from which outgoing calls using this {@code PhoneAccount} + * will appear to come, if applicable, and the destination to which incoming calls using this + * {@code PhoneAccount} may be addressed. + * + * @return A address expressed as a {@code Uri}, for example, a phone number. + */ + public Uri getAddress() { + return mAddress; + } + + /** + * The raw callback number used for this {@code PhoneAccount}, as distinct from + * {@link #getAddress()}. For the majority of {@code PhoneAccount}s this should be registered + * as {@code null}. It is used by the system for SIM-based {@code PhoneAccount} registration + * where {@link android.telephony.TelephonyManager#setLine1NumberForDisplay(String, String)} + * has been used to alter the callback number. + * <p> + * + * @return The subscription number, suitable for display to the user. + */ + public Uri getSubscriptionAddress() { + return mSubscriptionAddress; + } + + /** + * The capabilities of this {@code PhoneAccount}. + * + * @return A bit field of flags describing this {@code PhoneAccount}'s capabilities. + */ + public int getCapabilities() { + return mCapabilities; + } + + /** + * Determines if this {@code PhoneAccount} has a capabilities specified by the passed in + * bit mask. + * + * @param capability The capabilities to check. + * @return {@code true} if the phone account has the capability. + */ + public boolean hasCapabilities(int capability) { + return (mCapabilities & capability) == capability; + } + + /** + * Determines if this {@code PhoneAccount} has routes specified by the passed in bit mask. + * + * @param route The routes to check. + * @return {@code true} if the phone account has the routes. + * @hide + */ + public boolean hasAudioRoutes(int routes) { + return (mSupportedAudioRoutes & routes) == routes; + } + + /** + * A short label describing a {@code PhoneAccount}. + * + * @return A label for this {@code PhoneAccount}. + */ + public CharSequence getLabel() { + return mLabel; + } + + /** + * A short paragraph describing this {@code PhoneAccount}. + * + * @return A description for this {@code PhoneAccount}. + */ + public CharSequence getShortDescription() { + return mShortDescription; + } + + /** + * The URI schemes supported by this {@code PhoneAccount}. + * + * @return The URI schemes. + */ + public List<String> getSupportedUriSchemes() { + return mSupportedUriSchemes; + } + + /** + * The extras associated with this {@code PhoneAccount}. + * <p> + * A {@link ConnectionService} may provide implementation specific information about the + * {@link PhoneAccount} via the extras. + * + * @return The extras. + */ + public Bundle getExtras() { + return mExtras; + } + + /** + * The audio routes supported by this {@code PhoneAccount}. + * + * @hide + */ + public int getSupportedAudioRoutes() { + return mSupportedAudioRoutes; + } + + /** + * The icon to represent this {@code PhoneAccount}. + * + * @return The icon. + */ + public Icon getIcon() { + return mIcon; + } + + /** + * Indicates whether the user has enabled this {@code PhoneAccount} or not. This value is only + * populated for {@code PhoneAccount}s returned by {@link TelecomManager#getPhoneAccount}. + * + * @return {@code true} if the account is enabled by the user, {@code false} otherwise. + */ + public boolean isEnabled() { + return mIsEnabled; + } + + /** + * A non-empty {@link String} representing the group that A {@link PhoneAccount} is in or an + * empty {@link String} if the {@link PhoneAccount} is not in a group. If this + * {@link PhoneAccount} is in a group, this new {@link PhoneAccount} will replace a registered + * {@link PhoneAccount} that is in the same group. When the {@link PhoneAccount} is replaced, + * its user defined defaults and enabled status will also pass to this new {@link PhoneAccount}. + * Only {@link PhoneAccount}s that share the same {@link ConnectionService} can be replaced. + * + * @return A non-empty String Id if this {@link PhoneAccount} belongs to a group. + * @hide + */ + public String getGroupId() { + return mGroupId; + } + + /** + * Determines if the {@link PhoneAccount} supports calls to/from addresses with a specified URI + * scheme. + * + * @param uriScheme The URI scheme to check. + * @return {@code true} if the {@code PhoneAccount} supports calls to/from addresses with the + * specified URI scheme. + */ + public boolean supportsUriScheme(String uriScheme) { + if (mSupportedUriSchemes == null || uriScheme == null) { + return false; + } + + for (String scheme : mSupportedUriSchemes) { + if (scheme != null && scheme.equals(uriScheme)) { + return true; + } + } + return false; + } + + /** + * A highlight color to use in displaying information about this {@code PhoneAccount}. + * + * @return A hexadecimal color value. + */ + public int getHighlightColor() { + return mHighlightColor; + } + + /** + * Sets the enabled state of the phone account. + * @hide + */ + public void setIsEnabled(boolean isEnabled) { + mIsEnabled = isEnabled; + } + + /** + * @return {@code true} if the {@link PhoneAccount} is self-managed, {@code false} otherwise. + * @hide + */ + public boolean isSelfManaged() { + return (mCapabilities & CAPABILITY_SELF_MANAGED) == CAPABILITY_SELF_MANAGED; + } + + // + // Parcelable implementation + // + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + if (mAccountHandle == null) { + out.writeInt(0); + } else { + out.writeInt(1); + mAccountHandle.writeToParcel(out, flags); + } + if (mAddress == null) { + out.writeInt(0); + } else { + out.writeInt(1); + mAddress.writeToParcel(out, flags); + } + if (mSubscriptionAddress == null) { + out.writeInt(0); + } else { + out.writeInt(1); + mSubscriptionAddress.writeToParcel(out, flags); + } + out.writeInt(mCapabilities); + out.writeInt(mHighlightColor); + out.writeCharSequence(mLabel); + out.writeCharSequence(mShortDescription); + out.writeStringList(mSupportedUriSchemes); + + if (mIcon == null) { + out.writeInt(0); + } else { + out.writeInt(1); + mIcon.writeToParcel(out, flags); + } + out.writeByte((byte) (mIsEnabled ? 1 : 0)); + out.writeBundle(mExtras); + out.writeString(mGroupId); + out.writeInt(mSupportedAudioRoutes); + } + + public static final Creator<PhoneAccount> CREATOR + = new Creator<PhoneAccount>() { + @Override + public PhoneAccount createFromParcel(Parcel in) { + return new PhoneAccount(in); + } + + @Override + public PhoneAccount[] newArray(int size) { + return new PhoneAccount[size]; + } + }; + + private PhoneAccount(Parcel in) { + if (in.readInt() > 0) { + mAccountHandle = PhoneAccountHandle.CREATOR.createFromParcel(in); + } else { + mAccountHandle = null; + } + if (in.readInt() > 0) { + mAddress = Uri.CREATOR.createFromParcel(in); + } else { + mAddress = null; + } + if (in.readInt() > 0) { + mSubscriptionAddress = Uri.CREATOR.createFromParcel(in); + } else { + mSubscriptionAddress = null; + } + mCapabilities = in.readInt(); + mHighlightColor = in.readInt(); + mLabel = in.readCharSequence(); + mShortDescription = in.readCharSequence(); + mSupportedUriSchemes = Collections.unmodifiableList(in.createStringArrayList()); + if (in.readInt() > 0) { + mIcon = Icon.CREATOR.createFromParcel(in); + } else { + mIcon = null; + } + mIsEnabled = in.readByte() == 1; + mExtras = in.readBundle(); + mGroupId = in.readString(); + mSupportedAudioRoutes = in.readInt(); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder().append("[[") + .append(mIsEnabled ? 'X' : ' ') + .append("] PhoneAccount: ") + .append(mAccountHandle) + .append(" Capabilities: ") + .append(capabilitiesToString()) + .append(" Audio Routes: ") + .append(audioRoutesToString()) + .append(" Schemes: "); + for (String scheme : mSupportedUriSchemes) { + sb.append(scheme) + .append(" "); + } + sb.append(" Extras: "); + sb.append(mExtras); + sb.append(" GroupId: "); + sb.append(Log.pii(mGroupId)); + sb.append("]"); + return sb.toString(); + } + + /** + * Generates a string representation of a capabilities bitmask. + * + * @param capabilities The capabilities bitmask. + * @return String representation of the capabilities bitmask. + */ + private String capabilitiesToString() { + StringBuilder sb = new StringBuilder(); + if (hasCapabilities(CAPABILITY_SELF_MANAGED)) { + sb.append("SelfManaged "); + } + if (hasCapabilities(CAPABILITY_SUPPORTS_VIDEO_CALLING)) { + sb.append("SuppVideo "); + } + if (hasCapabilities(CAPABILITY_VIDEO_CALLING)) { + sb.append("Video "); + } + if (hasCapabilities(CAPABILITY_VIDEO_CALLING_RELIES_ON_PRESENCE)) { + sb.append("Presence "); + } + if (hasCapabilities(CAPABILITY_CALL_PROVIDER)) { + sb.append("CallProvider "); + } + if (hasCapabilities(CAPABILITY_CALL_SUBJECT)) { + sb.append("CallSubject "); + } + if (hasCapabilities(CAPABILITY_CONNECTION_MANAGER)) { + sb.append("ConnectionMgr "); + } + if (hasCapabilities(CAPABILITY_EMERGENCY_CALLS_ONLY)) { + sb.append("EmergOnly "); + } + if (hasCapabilities(CAPABILITY_MULTI_USER)) { + sb.append("MultiUser "); + } + if (hasCapabilities(CAPABILITY_PLACE_EMERGENCY_CALLS)) { + sb.append("PlaceEmerg "); + } + if (hasCapabilities(CAPABILITY_EMERGENCY_VIDEO_CALLING)) { + sb.append("EmergVideo "); + } + if (hasCapabilities(CAPABILITY_SIM_SUBSCRIPTION)) { + sb.append("SimSub "); + } + return sb.toString(); + } + + private String audioRoutesToString() { + StringBuilder sb = new StringBuilder(); + + if (hasAudioRoutes(CallAudioState.ROUTE_BLUETOOTH)) { + sb.append("B"); + } + if (hasAudioRoutes(CallAudioState.ROUTE_EARPIECE)) { + sb.append("E"); + } + if (hasAudioRoutes(CallAudioState.ROUTE_SPEAKER)) { + sb.append("S"); + } + if (hasAudioRoutes(CallAudioState.ROUTE_WIRED_HEADSET)) { + sb.append("W"); + } + + return sb.toString(); + } +} diff --git a/android/telecom/PhoneAccountHandle.java b/android/telecom/PhoneAccountHandle.java new file mode 100644 index 00000000..77b510df --- /dev/null +++ b/android/telecom/PhoneAccountHandle.java @@ -0,0 +1,172 @@ +/* + * Copyright (C) 2014 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.content.ComponentName; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.Process; +import android.os.UserHandle; + +import java.util.Objects; + +/** + * The unique identifier for a {@link PhoneAccount}. A {@code PhoneAccountHandle} is made of two + * parts: + * <ul> + * <li>The component name of the associated connection service.</li> + * <li>A string identifier that is unique across {@code PhoneAccountHandle}s with the same + * component name.</li> + * </ul> + * + * Note: This Class requires a non-null {@link ComponentName} and {@link UserHandle} to operate + * properly. Passing in invalid parameters will generate a log warning. + * + * See {@link PhoneAccount}, {@link TelecomManager}. + */ +public final class PhoneAccountHandle implements Parcelable { + private final ComponentName mComponentName; + private final String mId; + private final UserHandle mUserHandle; + + public PhoneAccountHandle( + @NonNull ComponentName componentName, + @NonNull String id) { + this(componentName, id, Process.myUserHandle()); + } + + public PhoneAccountHandle( + @NonNull ComponentName componentName, + @NonNull String id, + @NonNull UserHandle userHandle) { + checkParameters(componentName, userHandle); + mComponentName = componentName; + mId = id; + mUserHandle = userHandle; + } + + /** + * The {@code ComponentName} of the connection service which is responsible for making phone + * calls using this {@code PhoneAccountHandle}. + * + * @return A suitable {@code ComponentName}. + */ + public ComponentName getComponentName() { + return mComponentName; + } + + /** + * A string that uniquely distinguishes this particular {@code PhoneAccountHandle} from all the + * others supported by the connection service that created it. + * <p> + * A connection service must select identifiers that are stable for the lifetime of + * their users' relationship with their service, across many Android devices. For example, a + * good set of identifiers might be the email addresses with which with users registered for + * their accounts with a particular service. Depending on how a service chooses to operate, + * a bad set of identifiers might be an increasing series of integers + * ({@code 0}, {@code 1}, {@code 2}, ...) that are generated locally on each phone and could + * collide with values generated on other phones or after a data wipe of a given phone. + * + * Important: A non-unique identifier could cause non-deterministic call-log backup/restore + * behavior. + * + * @return A service-specific unique identifier for this {@code PhoneAccountHandle}. + */ + public String getId() { + return mId; + } + + /** + * @return the {@link UserHandle} to use when connecting to this PhoneAccount. + */ + public UserHandle getUserHandle() { + return mUserHandle; + } + + @Override + public int hashCode() { + return Objects.hash(mComponentName, mId, mUserHandle); + } + + @Override + public String toString() { + // Note: Log.pii called for mId as it can contain personally identifying phone account + // information such as SIP account IDs. + return new StringBuilder().append(mComponentName) + .append(", ") + .append(Log.pii(mId)) + .append(", ") + .append(mUserHandle) + .toString(); + } + + @Override + public boolean equals(Object other) { + return other != null && + other instanceof PhoneAccountHandle && + Objects.equals(((PhoneAccountHandle) other).getComponentName(), + getComponentName()) && + Objects.equals(((PhoneAccountHandle) other).getId(), getId()) && + Objects.equals(((PhoneAccountHandle) other).getUserHandle(), getUserHandle()); + } + + // + // Parcelable implementation. + // + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + mComponentName.writeToParcel(out, flags); + out.writeString(mId); + mUserHandle.writeToParcel(out, flags); + } + + private void checkParameters(ComponentName componentName, UserHandle userHandle) { + if(componentName == null) { + android.util.Log.w("PhoneAccountHandle", new Exception("PhoneAccountHandle has " + + "been created with null ComponentName!")); + } + if(userHandle == null) { + android.util.Log.w("PhoneAccountHandle", new Exception("PhoneAccountHandle has " + + "been created with null UserHandle!")); + } + } + + public static final Creator<PhoneAccountHandle> CREATOR = new Creator<PhoneAccountHandle>() { + @Override + public PhoneAccountHandle createFromParcel(Parcel in) { + return new PhoneAccountHandle(in); + } + + @Override + public PhoneAccountHandle[] newArray(int size) { + return new PhoneAccountHandle[size]; + } + }; + + private PhoneAccountHandle(Parcel in) { + this(ComponentName.CREATOR.createFromParcel(in), + in.readString(), + UserHandle.CREATOR.createFromParcel(in)); + } +} diff --git a/android/telecom/RemoteConference.java b/android/telecom/RemoteConference.java new file mode 100644 index 00000000..502b7c01 --- /dev/null +++ b/android/telecom/RemoteConference.java @@ -0,0 +1,586 @@ +/* + * Copyright (C) 2014 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 com.android.internal.telecom.IConnectionService; + +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.os.Bundle; +import android.os.Handler; +import android.os.RemoteException; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.CopyOnWriteArraySet; + +/** + * A conference provided to a {@link ConnectionService} by another {@code ConnectionService} through + * {@link ConnectionService#conferenceRemoteConnections}. Once created, a {@code RemoteConference} + * can be used to control the conference call or monitor changes through + * {@link RemoteConnection.Callback}. + * + * @see ConnectionService#onRemoteConferenceAdded + */ +public final class RemoteConference { + + /** + * Callback base class for {@link RemoteConference}. + */ + public abstract static class Callback { + /** + * Invoked when the state of this {@code RemoteConferece} has changed. See + * {@link #getState()}. + * + * @param conference The {@code RemoteConference} invoking this method. + * @param oldState The previous state of the {@code RemoteConference}. + * @param newState The new state of the {@code RemoteConference}. + */ + public void onStateChanged(RemoteConference conference, int oldState, int newState) {} + + /** + * Invoked when this {@code RemoteConference} is disconnected. + * + * @param conference The {@code RemoteConference} invoking this method. + * @param disconnectCause The ({@see DisconnectCause}) associated with this failed + * conference. + */ + public void onDisconnected(RemoteConference conference, DisconnectCause disconnectCause) {} + + /** + * Invoked when a {@link RemoteConnection} is added to the conference call. + * + * @param conference The {@code RemoteConference} invoking this method. + * @param connection The {@link RemoteConnection} being added. + */ + public void onConnectionAdded(RemoteConference conference, RemoteConnection connection) {} + + /** + * Invoked when a {@link RemoteConnection} is removed from the conference call. + * + * @param conference The {@code RemoteConference} invoking this method. + * @param connection The {@link RemoteConnection} being removed. + */ + public void onConnectionRemoved(RemoteConference conference, RemoteConnection connection) {} + + /** + * Indicates that the call capabilities of this {@code RemoteConference} have changed. + * See {@link #getConnectionCapabilities()}. + * + * @param conference The {@code RemoteConference} invoking this method. + * @param connectionCapabilities The new capabilities of the {@code RemoteConference}. + */ + public void onConnectionCapabilitiesChanged( + RemoteConference conference, + int connectionCapabilities) {} + + /** + * Indicates that the call properties of this {@code RemoteConference} have changed. + * See {@link #getConnectionProperties()}. + * + * @param conference The {@code RemoteConference} invoking this method. + * @param connectionProperties The new properties of the {@code RemoteConference}. + */ + public void onConnectionPropertiesChanged( + RemoteConference conference, + int connectionProperties) {} + + + /** + * Invoked when the set of {@link RemoteConnection}s which can be added to this conference + * call have changed. + * + * @param conference The {@code RemoteConference} invoking this method. + * @param conferenceableConnections The list of conferenceable {@link RemoteConnection}s. + */ + public void onConferenceableConnectionsChanged( + RemoteConference conference, + List<RemoteConnection> conferenceableConnections) {} + + /** + * Indicates that this {@code RemoteConference} has been destroyed. No further requests + * should be made to the {@code RemoteConference}, and references to it should be cleared. + * + * @param conference The {@code RemoteConference} invoking this method. + */ + public void onDestroyed(RemoteConference conference) {} + + /** + * Handles changes to the {@code RemoteConference} extras. + * + * @param conference The {@code RemoteConference} invoking this method. + * @param extras The extras containing other information associated with the conference. + */ + public void onExtrasChanged(RemoteConference conference, @Nullable Bundle extras) {} + } + + private final String mId; + private final IConnectionService mConnectionService; + + private final Set<CallbackRecord<Callback>> mCallbackRecords = new CopyOnWriteArraySet<>(); + private final List<RemoteConnection> mChildConnections = new CopyOnWriteArrayList<>(); + private final List<RemoteConnection> mUnmodifiableChildConnections = + Collections.unmodifiableList(mChildConnections); + private final List<RemoteConnection> mConferenceableConnections = new ArrayList<>(); + private final List<RemoteConnection> mUnmodifiableConferenceableConnections = + Collections.unmodifiableList(mConferenceableConnections); + + private int mState = Connection.STATE_NEW; + private DisconnectCause mDisconnectCause; + private int mConnectionCapabilities; + private int mConnectionProperties; + private Bundle mExtras; + + /** @hide */ + RemoteConference(String id, IConnectionService connectionService) { + mId = id; + mConnectionService = connectionService; + } + + /** @hide */ + String getId() { + return mId; + } + + /** @hide */ + void setDestroyed() { + for (RemoteConnection connection : mChildConnections) { + connection.setConference(null); + } + for (CallbackRecord<Callback> record : mCallbackRecords) { + final RemoteConference conference = this; + final Callback callback = record.getCallback(); + record.getHandler().post(new Runnable() { + @Override + public void run() { + callback.onDestroyed(conference); + } + }); + } + } + + /** @hide */ + void setState(final int newState) { + if (newState != Connection.STATE_ACTIVE && + newState != Connection.STATE_HOLDING && + newState != Connection.STATE_DISCONNECTED) { + Log.w(this, "Unsupported state transition for Conference call.", + Connection.stateToString(newState)); + return; + } + + if (mState != newState) { + final int oldState = mState; + mState = newState; + for (CallbackRecord<Callback> record : mCallbackRecords) { + final RemoteConference conference = this; + final Callback callback = record.getCallback(); + record.getHandler().post(new Runnable() { + @Override + public void run() { + callback.onStateChanged(conference, oldState, newState); + } + }); + } + } + } + + /** @hide */ + void addConnection(final RemoteConnection connection) { + if (!mChildConnections.contains(connection)) { + mChildConnections.add(connection); + connection.setConference(this); + for (CallbackRecord<Callback> record : mCallbackRecords) { + final RemoteConference conference = this; + final Callback callback = record.getCallback(); + record.getHandler().post(new Runnable() { + @Override + public void run() { + callback.onConnectionAdded(conference, connection); + } + }); + } + } + } + + /** @hide */ + void removeConnection(final RemoteConnection connection) { + if (mChildConnections.contains(connection)) { + mChildConnections.remove(connection); + connection.setConference(null); + for (CallbackRecord<Callback> record : mCallbackRecords) { + final RemoteConference conference = this; + final Callback callback = record.getCallback(); + record.getHandler().post(new Runnable() { + @Override + public void run() { + callback.onConnectionRemoved(conference, connection); + } + }); + } + } + } + + /** @hide */ + void setConnectionCapabilities(final int connectionCapabilities) { + if (mConnectionCapabilities != connectionCapabilities) { + mConnectionCapabilities = connectionCapabilities; + for (CallbackRecord<Callback> record : mCallbackRecords) { + final RemoteConference conference = this; + final Callback callback = record.getCallback(); + record.getHandler().post(new Runnable() { + @Override + public void run() { + callback.onConnectionCapabilitiesChanged( + conference, mConnectionCapabilities); + } + }); + } + } + } + + /** @hide */ + void setConnectionProperties(final int connectionProperties) { + if (mConnectionProperties != connectionProperties) { + mConnectionProperties = connectionProperties; + for (CallbackRecord<Callback> record : mCallbackRecords) { + final RemoteConference conference = this; + final Callback callback = record.getCallback(); + record.getHandler().post(new Runnable() { + @Override + public void run() { + callback.onConnectionPropertiesChanged( + conference, mConnectionProperties); + } + }); + } + } + } + + /** @hide */ + void setConferenceableConnections(List<RemoteConnection> conferenceableConnections) { + mConferenceableConnections.clear(); + mConferenceableConnections.addAll(conferenceableConnections); + for (CallbackRecord<Callback> record : mCallbackRecords) { + final RemoteConference conference = this; + final Callback callback = record.getCallback(); + record.getHandler().post(new Runnable() { + @Override + public void run() { + callback.onConferenceableConnectionsChanged( + conference, mUnmodifiableConferenceableConnections); + } + }); + } + } + + /** @hide */ + void setDisconnected(final DisconnectCause disconnectCause) { + if (mState != Connection.STATE_DISCONNECTED) { + mDisconnectCause = disconnectCause; + setState(Connection.STATE_DISCONNECTED); + for (CallbackRecord<Callback> record : mCallbackRecords) { + final RemoteConference conference = this; + final Callback callback = record.getCallback(); + record.getHandler().post(new Runnable() { + @Override + public void run() { + callback.onDisconnected(conference, disconnectCause); + } + }); + } + } + } + + /** @hide */ + void putExtras(final Bundle extras) { + if (extras == null) { + return; + } + if (mExtras == null) { + mExtras = new Bundle(); + } + mExtras.putAll(extras); + + notifyExtrasChanged(); + } + + /** @hide */ + void removeExtras(List<String> keys) { + if (mExtras == null || keys == null || keys.isEmpty()) { + return; + } + for (String key : keys) { + mExtras.remove(key); + } + + notifyExtrasChanged(); + } + + private void notifyExtrasChanged() { + for (CallbackRecord<Callback> record : mCallbackRecords) { + final RemoteConference conference = this; + final Callback callback = record.getCallback(); + record.getHandler().post(new Runnable() { + @Override + public void run() { + callback.onExtrasChanged(conference, mExtras); + } + }); + } + } + + /** + * Returns the list of {@link RemoteConnection}s contained in this conference. + * + * @return A list of child connections. + */ + public final List<RemoteConnection> getConnections() { + return mUnmodifiableChildConnections; + } + + /** + * Gets the state of the conference call. See {@link Connection} for valid values. + * + * @return A constant representing the state the conference call is currently in. + */ + public final int getState() { + return mState; + } + + /** + * Returns the capabilities of the conference. See {@code CAPABILITY_*} constants in class + * {@link Connection} for valid values. + * + * @return A bitmask of the capabilities of the conference call. + */ + public final int getConnectionCapabilities() { + return mConnectionCapabilities; + } + + /** + * Returns the properties of the conference. See {@code PROPERTY_*} constants in class + * {@link Connection} for valid values. + * + * @return A bitmask of the properties of the conference call. + */ + public final int getConnectionProperties() { + return mConnectionProperties; + } + + /** + * Obtain the extras associated with this {@code RemoteConnection}. + * + * @return The extras for this connection. + */ + public final Bundle getExtras() { + return mExtras; + } + + /** + * Disconnects the conference call as well as the child {@link RemoteConnection}s. + */ + public void disconnect() { + try { + mConnectionService.disconnect(mId, null /*Session.Info*/); + } catch (RemoteException e) { + } + } + + /** + * Removes the specified {@link RemoteConnection} from the conference. This causes the + * {@link RemoteConnection} to become a standalone connection. This is a no-op if the + * {@link RemoteConnection} does not belong to this conference. + * + * @param connection The remote-connection to remove. + */ + public void separate(RemoteConnection connection) { + if (mChildConnections.contains(connection)) { + try { + mConnectionService.splitFromConference(connection.getId(), null /*Session.Info*/); + } catch (RemoteException e) { + } + } + } + + /** + * Merges all {@link RemoteConnection}s of this conference into a single call. This should be + * invoked only if the conference contains the capability + * {@link Connection#CAPABILITY_MERGE_CONFERENCE}, otherwise it is a no-op. The presence of said + * capability indicates that the connections of this conference, despite being part of the + * same conference object, are yet to have their audio streams merged; this is a common pattern + * for CDMA conference calls, but the capability is not used for GSM and SIP conference calls. + * Invoking this method will cause the unmerged child connections to merge their audio + * streams. + */ + public void merge() { + try { + mConnectionService.mergeConference(mId, null /*Session.Info*/); + } catch (RemoteException e) { + } + } + + /** + * Swaps the active audio stream between the conference's child {@link RemoteConnection}s. + * This should be invoked only if the conference contains the capability + * {@link Connection#CAPABILITY_SWAP_CONFERENCE}, otherwise it is a no-op. This is only used by + * {@link ConnectionService}s that create conferences for connections that do not yet have + * their audio streams merged; this is a common pattern for CDMA conference calls, but the + * capability is not used for GSM and SIP conference calls. Invoking this method will change the + * active audio stream to a different child connection. + */ + public void swap() { + try { + mConnectionService.swapConference(mId, null /*Session.Info*/); + } catch (RemoteException e) { + } + } + + /** + * Puts the conference on hold. + */ + public void hold() { + try { + mConnectionService.hold(mId, null /*Session.Info*/); + } catch (RemoteException e) { + } + } + + /** + * Unholds the conference call. + */ + public void unhold() { + try { + mConnectionService.unhold(mId, null /*Session.Info*/); + } catch (RemoteException e) { + } + } + + /** + * Returns the {@link DisconnectCause} for the conference if it is in the state + * {@link Connection#STATE_DISCONNECTED}. If the conference is not disconnected, this will + * return null. + * + * @return The disconnect cause. + */ + public DisconnectCause getDisconnectCause() { + return mDisconnectCause; + } + + /** + * Requests that the conference start playing the specified DTMF tone. + * + * @param digit The digit for which to play a DTMF tone. + */ + public void playDtmfTone(char digit) { + try { + mConnectionService.playDtmfTone(mId, digit, null /*Session.Info*/); + } catch (RemoteException e) { + } + } + + /** + * Stops the most recent request to play a DTMF tone. + * + * @see #playDtmfTone + */ + public void stopDtmfTone() { + try { + mConnectionService.stopDtmfTone(mId, null /*Session.Info*/); + } catch (RemoteException e) { + } + } + + /** + * Request to change the conference's audio routing to the specified state. The specified state + * can include audio routing (Bluetooth, Speaker, etc) and muting state. + * + * @see android.telecom.AudioState + * @deprecated Use {@link #setCallAudioState(CallAudioState)} instead. + * @hide + */ + @SystemApi + @Deprecated + public void setAudioState(AudioState state) { + setCallAudioState(new CallAudioState(state)); + } + + /** + * Request to change the conference's audio routing to the specified state. The specified state + * can include audio routing (Bluetooth, Speaker, etc) and muting state. + */ + public void setCallAudioState(CallAudioState state) { + try { + mConnectionService.onCallAudioStateChanged(mId, state, null /*Session.Info*/); + } catch (RemoteException e) { + } + } + + + /** + * Returns a list of independent connections that can me merged with this conference. + * + * @return A list of conferenceable connections. + */ + public List<RemoteConnection> getConferenceableConnections() { + return mUnmodifiableConferenceableConnections; + } + + /** + * Register a callback through which to receive state updates for this conference. + * + * @param callback The callback to notify of state changes. + */ + public final void registerCallback(Callback callback) { + registerCallback(callback, new Handler()); + } + + /** + * Registers a callback through which to receive state updates for this conference. + * Callbacks will be notified using the specified handler, if provided. + * + * @param callback The callback to notify of state changes. + * @param handler The handler on which to execute the callbacks. + */ + public final void registerCallback(Callback callback, Handler handler) { + unregisterCallback(callback); + if (callback != null && handler != null) { + mCallbackRecords.add(new CallbackRecord(callback, handler)); + } + } + + /** + * Unregisters a previously registered callback. + * + * @see #registerCallback + * + * @param callback The callback to unregister. + */ + public final void unregisterCallback(Callback callback) { + if (callback != null) { + for (CallbackRecord<Callback> record : mCallbackRecords) { + if (record.getCallback() == callback) { + mCallbackRecords.remove(record); + break; + } + } + } + } +} diff --git a/android/telecom/RemoteConnection.java b/android/telecom/RemoteConnection.java new file mode 100644 index 00000000..f30d7bde --- /dev/null +++ b/android/telecom/RemoteConnection.java @@ -0,0 +1,1587 @@ +/* + * Copyright (C) 2014 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 com.android.internal.telecom.IConnectionService; +import com.android.internal.telecom.IVideoCallback; +import com.android.internal.telecom.IVideoProvider; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.hardware.camera2.CameraManager; +import android.net.Uri; +import android.os.BadParcelableException; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.RemoteException; +import android.view.Surface; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +/** + * A connection provided to a {@link ConnectionService} by another {@code ConnectionService} + * running in a different process. + * + * @see ConnectionService#createRemoteOutgoingConnection(PhoneAccountHandle, ConnectionRequest) + * @see ConnectionService#createRemoteIncomingConnection(PhoneAccountHandle, ConnectionRequest) + */ +public final class RemoteConnection { + + /** + * Callback base class for {@link RemoteConnection}. + */ + public static abstract class Callback { + /** + * Invoked when the state of this {@code RemoteConnection} has changed. See + * {@link #getState()}. + * + * @param connection The {@code RemoteConnection} invoking this method. + * @param state The new state of the {@code RemoteConnection}. + */ + public void onStateChanged(RemoteConnection connection, int state) {} + + /** + * Invoked when this {@code RemoteConnection} is disconnected. + * + * @param connection The {@code RemoteConnection} invoking this method. + * @param disconnectCause The ({@see DisconnectCause}) associated with this failed + * connection. + */ + public void onDisconnected( + RemoteConnection connection, + DisconnectCause disconnectCause) {} + + /** + * Invoked when this {@code RemoteConnection} is requesting ringback. See + * {@link #isRingbackRequested()}. + * + * @param connection The {@code RemoteConnection} invoking this method. + * @param ringback Whether the {@code RemoteConnection} is requesting ringback. + */ + public void onRingbackRequested(RemoteConnection connection, boolean ringback) {} + + /** + * Indicates that the call capabilities of this {@code RemoteConnection} have changed. + * See {@link #getConnectionCapabilities()}. + * + * @param connection The {@code RemoteConnection} invoking this method. + * @param connectionCapabilities The new capabilities of the {@code RemoteConnection}. + */ + public void onConnectionCapabilitiesChanged( + RemoteConnection connection, + int connectionCapabilities) {} + + /** + * Indicates that the call properties of this {@code RemoteConnection} have changed. + * See {@link #getConnectionProperties()}. + * + * @param connection The {@code RemoteConnection} invoking this method. + * @param connectionProperties The new properties of the {@code RemoteConnection}. + */ + public void onConnectionPropertiesChanged( + RemoteConnection connection, + int connectionProperties) {} + + /** + * Invoked when the post-dial sequence in the outgoing {@code Connection} has reached a + * pause character. This causes the post-dial signals to stop pending user confirmation. An + * implementation should present this choice to the user and invoke + * {@link RemoteConnection#postDialContinue(boolean)} when the user makes the choice. + * + * @param connection The {@code RemoteConnection} invoking this method. + * @param remainingPostDialSequence The post-dial characters that remain to be sent. + */ + public void onPostDialWait(RemoteConnection connection, String remainingPostDialSequence) {} + + /** + * Invoked when the post-dial sequence in the outgoing {@code Connection} has processed + * a character. + * + * @param connection The {@code RemoteConnection} invoking this method. + * @param nextChar The character being processed. + */ + public void onPostDialChar(RemoteConnection connection, char nextChar) {} + + /** + * Indicates that the VOIP audio status of this {@code RemoteConnection} has changed. + * See {@link #isVoipAudioMode()}. + * + * @param connection The {@code RemoteConnection} invoking this method. + * @param isVoip Whether the new audio state of the {@code RemoteConnection} is VOIP. + */ + public void onVoipAudioChanged(RemoteConnection connection, boolean isVoip) {} + + /** + * Indicates that the status hints of this {@code RemoteConnection} have changed. See + * {@link #getStatusHints()} ()}. + * + * @param connection The {@code RemoteConnection} invoking this method. + * @param statusHints The new status hints of the {@code RemoteConnection}. + */ + public void onStatusHintsChanged(RemoteConnection connection, StatusHints statusHints) {} + + /** + * Indicates that the address (e.g., phone number) of this {@code RemoteConnection} has + * changed. See {@link #getAddress()} and {@link #getAddressPresentation()}. + * + * @param connection The {@code RemoteConnection} invoking this method. + * @param address The new address of the {@code RemoteConnection}. + * @param presentation The presentation requirements for the address. + * See {@link TelecomManager} for valid values. + */ + public void onAddressChanged(RemoteConnection connection, Uri address, int presentation) {} + + /** + * Indicates that the caller display name of this {@code RemoteConnection} has changed. + * See {@link #getCallerDisplayName()} and {@link #getCallerDisplayNamePresentation()}. + * + * @param connection The {@code RemoteConnection} invoking this method. + * @param callerDisplayName The new caller display name of the {@code RemoteConnection}. + * @param presentation The presentation requirements for the handle. + * See {@link TelecomManager} for valid values. + */ + public void onCallerDisplayNameChanged( + RemoteConnection connection, String callerDisplayName, int presentation) {} + + /** + * Indicates that the video state of this {@code RemoteConnection} has changed. + * See {@link #getVideoState()}. + * + * @param connection The {@code RemoteConnection} invoking this method. + * @param videoState The new video state of the {@code RemoteConnection}. + */ + public void onVideoStateChanged(RemoteConnection connection, int videoState) {} + + /** + * Indicates that this {@code RemoteConnection} has been destroyed. No further requests + * should be made to the {@code RemoteConnection}, and references to it should be cleared. + * + * @param connection The {@code RemoteConnection} invoking this method. + */ + public void onDestroyed(RemoteConnection connection) {} + + /** + * Indicates that the {@code RemoteConnection}s with which this {@code RemoteConnection} + * may be asked to create a conference has changed. + * + * @param connection The {@code RemoteConnection} invoking this method. + * @param conferenceableConnections The {@code RemoteConnection}s with which this + * {@code RemoteConnection} may be asked to create a conference. + */ + public void onConferenceableConnectionsChanged( + RemoteConnection connection, + List<RemoteConnection> conferenceableConnections) {} + + /** + * Indicates that the {@code VideoProvider} associated with this {@code RemoteConnection} + * has changed. + * + * @param connection The {@code RemoteConnection} invoking this method. + * @param videoProvider The new {@code VideoProvider} associated with this + * {@code RemoteConnection}. + */ + public void onVideoProviderChanged( + RemoteConnection connection, VideoProvider videoProvider) {} + + /** + * Indicates that the {@code RemoteConference} that this {@code RemoteConnection} is a part + * of has changed. + * + * @param connection The {@code RemoteConnection} invoking this method. + * @param conference The {@code RemoteConference} of which this {@code RemoteConnection} is + * a part, which may be {@code null}. + */ + public void onConferenceChanged( + RemoteConnection connection, + RemoteConference conference) {} + + /** + * Handles changes to the {@code RemoteConnection} extras. + * + * @param connection The {@code RemoteConnection} invoking this method. + * @param extras The extras containing other information associated with the connection. + */ + public void onExtrasChanged(RemoteConnection connection, @Nullable Bundle extras) {} + + /** + * Handles a connection event propagated to this {@link RemoteConnection}. + * <p> + * Connection events originate from {@link Connection#sendConnectionEvent(String, Bundle)}. + * + * @param connection The {@code RemoteConnection} invoking this method. + * @param event The connection event. + * @param extras Extras associated with the event. + */ + public void onConnectionEvent(RemoteConnection connection, String event, Bundle extras) {} + + /** + * Indicates that a RTT session was successfully established on this + * {@link RemoteConnection}. See {@link Connection#sendRttInitiationSuccess()}. + * @hide + * @param connection The {@code RemoteConnection} invoking this method. + */ + public void onRttInitiationSuccess(RemoteConnection connection) {} + + /** + * Indicates that a RTT session failed to be established on this + * {@link RemoteConnection}. See {@link Connection#sendRttInitiationFailure()}. + * @hide + * @param connection The {@code RemoteConnection} invoking this method. + * @param reason One of the reason codes defined in {@link Connection.RttModifyStatus}, + * with the exception of + * {@link Connection.RttModifyStatus#SESSION_MODIFY_REQUEST_SUCCESS}. + */ + public void onRttInitiationFailure(RemoteConnection connection, int reason) {} + + /** + * Indicates that an established RTT session was terminated remotely on this + * {@link RemoteConnection}. See {@link Connection#sendRttSessionRemotelyTerminated()} + * @hide + * @param connection The {@code RemoteConnection} invoking this method. + */ + public void onRttSessionRemotelyTerminated(RemoteConnection connection) {} + + /** + * Indicates that the remote user on this {@link RemoteConnection} has requested an upgrade + * to an RTT session. See {@link Connection#sendRemoteRttRequest()} + * @hide + * @param connection The {@code RemoteConnection} invoking this method. + */ + public void onRemoteRttRequest(RemoteConnection connection) {} + } + + /** + * {@link RemoteConnection.VideoProvider} associated with a {@link RemoteConnection}. Used to + * receive video related events and control the video associated with a + * {@link RemoteConnection}. + * + * @see Connection.VideoProvider + */ + public static class VideoProvider { + + /** + * Callback class used by the {@link RemoteConnection.VideoProvider} to relay events from + * the {@link Connection.VideoProvider}. + */ + public abstract static class Callback { + /** + * Reports a session modification request received from the + * {@link Connection.VideoProvider} associated with a {@link RemoteConnection}. + * + * @param videoProvider The {@link RemoteConnection.VideoProvider} invoking this method. + * @param videoProfile The requested video call profile. + * @see InCallService.VideoCall.Callback#onSessionModifyRequestReceived(VideoProfile) + * @see Connection.VideoProvider#receiveSessionModifyRequest(VideoProfile) + */ + public void onSessionModifyRequestReceived( + VideoProvider videoProvider, + VideoProfile videoProfile) {} + + /** + * Reports a session modification response received from the + * {@link Connection.VideoProvider} associated with a {@link RemoteConnection}. + * + * @param videoProvider The {@link RemoteConnection.VideoProvider} invoking this method. + * @param status Status of the session modify request. + * @param requestedProfile The original request which was sent to the peer device. + * @param responseProfile The actual profile changes made by the peer device. + * @see InCallService.VideoCall.Callback#onSessionModifyResponseReceived(int, + * VideoProfile, VideoProfile) + * @see Connection.VideoProvider#receiveSessionModifyResponse(int, VideoProfile, + * VideoProfile) + */ + public void onSessionModifyResponseReceived( + VideoProvider videoProvider, + int status, + VideoProfile requestedProfile, + VideoProfile responseProfile) {} + + /** + * Reports a call session event received from the {@link Connection.VideoProvider} + * associated with a {@link RemoteConnection}. + * + * @param videoProvider The {@link RemoteConnection.VideoProvider} invoking this method. + * @param event The event. + * @see InCallService.VideoCall.Callback#onCallSessionEvent(int) + * @see Connection.VideoProvider#handleCallSessionEvent(int) + */ + public void onCallSessionEvent(VideoProvider videoProvider, int event) {} + + /** + * Reports a change in the peer video dimensions received from the + * {@link Connection.VideoProvider} associated with a {@link RemoteConnection}. + * + * @param videoProvider The {@link RemoteConnection.VideoProvider} invoking this method. + * @param width The updated peer video width. + * @param height The updated peer video height. + * @see InCallService.VideoCall.Callback#onPeerDimensionsChanged(int, int) + * @see Connection.VideoProvider#changePeerDimensions(int, int) + */ + public void onPeerDimensionsChanged(VideoProvider videoProvider, int width, + int height) {} + + /** + * Reports a change in the data usage (in bytes) received from the + * {@link Connection.VideoProvider} associated with a {@link RemoteConnection}. + * + * @param videoProvider The {@link RemoteConnection.VideoProvider} invoking this method. + * @param dataUsage The updated data usage (in bytes). + * @see InCallService.VideoCall.Callback#onCallDataUsageChanged(long) + * @see Connection.VideoProvider#setCallDataUsage(long) + */ + public void onCallDataUsageChanged(VideoProvider videoProvider, long dataUsage) {} + + /** + * Reports a change in the capabilities of the current camera, received from the + * {@link Connection.VideoProvider} associated with a {@link RemoteConnection}. + * + * @param videoProvider The {@link RemoteConnection.VideoProvider} invoking this method. + * @param cameraCapabilities The changed camera capabilities. + * @see InCallService.VideoCall.Callback#onCameraCapabilitiesChanged( + * VideoProfile.CameraCapabilities) + * @see Connection.VideoProvider#changeCameraCapabilities( + * VideoProfile.CameraCapabilities) + */ + public void onCameraCapabilitiesChanged( + VideoProvider videoProvider, + VideoProfile.CameraCapabilities cameraCapabilities) {} + + /** + * Reports a change in the video quality received from the + * {@link Connection.VideoProvider} associated with a {@link RemoteConnection}. + * + * @param videoProvider The {@link RemoteConnection.VideoProvider} invoking this method. + * @param videoQuality The updated peer video quality. + * @see InCallService.VideoCall.Callback#onVideoQualityChanged(int) + * @see Connection.VideoProvider#changeVideoQuality(int) + */ + public void onVideoQualityChanged(VideoProvider videoProvider, int videoQuality) {} + } + + private final IVideoCallback mVideoCallbackDelegate = new IVideoCallback() { + @Override + public void receiveSessionModifyRequest(VideoProfile videoProfile) { + for (Callback l : mCallbacks) { + l.onSessionModifyRequestReceived(VideoProvider.this, videoProfile); + } + } + + @Override + public void receiveSessionModifyResponse(int status, VideoProfile requestedProfile, + VideoProfile responseProfile) { + for (Callback l : mCallbacks) { + l.onSessionModifyResponseReceived( + VideoProvider.this, + status, + requestedProfile, + responseProfile); + } + } + + @Override + public void handleCallSessionEvent(int event) { + for (Callback l : mCallbacks) { + l.onCallSessionEvent(VideoProvider.this, event); + } + } + + @Override + public void changePeerDimensions(int width, int height) { + for (Callback l : mCallbacks) { + l.onPeerDimensionsChanged(VideoProvider.this, width, height); + } + } + + @Override + public void changeCallDataUsage(long dataUsage) { + for (Callback l : mCallbacks) { + l.onCallDataUsageChanged(VideoProvider.this, dataUsage); + } + } + + @Override + public void changeCameraCapabilities( + VideoProfile.CameraCapabilities cameraCapabilities) { + for (Callback l : mCallbacks) { + l.onCameraCapabilitiesChanged(VideoProvider.this, cameraCapabilities); + } + } + + @Override + public void changeVideoQuality(int videoQuality) { + for (Callback l : mCallbacks) { + l.onVideoQualityChanged(VideoProvider.this, videoQuality); + } + } + + @Override + public IBinder asBinder() { + return null; + } + }; + + private final VideoCallbackServant mVideoCallbackServant = + new VideoCallbackServant(mVideoCallbackDelegate); + + private final IVideoProvider mVideoProviderBinder; + + private final String mCallingPackage; + + private final int mTargetSdkVersion; + + /** + * ConcurrentHashMap constructor params: 8 is initial table size, 0.9f is + * load factor before resizing, 1 means we only expect a single thread to + * access the map so make only a single shard + */ + private final Set<Callback> mCallbacks = Collections.newSetFromMap( + new ConcurrentHashMap<Callback, Boolean>(8, 0.9f, 1)); + + VideoProvider(IVideoProvider videoProviderBinder, String callingPackage, + int targetSdkVersion) { + + mVideoProviderBinder = videoProviderBinder; + mCallingPackage = callingPackage; + mTargetSdkVersion = targetSdkVersion; + try { + mVideoProviderBinder.addVideoCallback(mVideoCallbackServant.getStub().asBinder()); + } catch (RemoteException e) { + } + } + + /** + * Registers a callback to receive commands and state changes for video calls. + * + * @param l The video call callback. + */ + public void registerCallback(Callback l) { + mCallbacks.add(l); + } + + /** + * Clears the video call callback set via {@link #registerCallback}. + * + * @param l The video call callback to clear. + */ + public void unregisterCallback(Callback l) { + mCallbacks.remove(l); + } + + /** + * Sets the camera to be used for the outgoing video for the + * {@link RemoteConnection.VideoProvider}. + * + * @param cameraId The id of the camera (use ids as reported by + * {@link CameraManager#getCameraIdList()}). + * @see Connection.VideoProvider#onSetCamera(String) + */ + public void setCamera(String cameraId) { + try { + mVideoProviderBinder.setCamera(cameraId, mCallingPackage, mTargetSdkVersion); + } catch (RemoteException e) { + } + } + + /** + * Sets the surface to be used for displaying a preview of what the user's camera is + * currently capturing for the {@link RemoteConnection.VideoProvider}. + * + * @param surface The {@link Surface}. + * @see Connection.VideoProvider#onSetPreviewSurface(Surface) + */ + public void setPreviewSurface(Surface surface) { + try { + mVideoProviderBinder.setPreviewSurface(surface); + } catch (RemoteException e) { + } + } + + /** + * Sets the surface to be used for displaying the video received from the remote device for + * the {@link RemoteConnection.VideoProvider}. + * + * @param surface The {@link Surface}. + * @see Connection.VideoProvider#onSetDisplaySurface(Surface) + */ + public void setDisplaySurface(Surface surface) { + try { + mVideoProviderBinder.setDisplaySurface(surface); + } catch (RemoteException e) { + } + } + + /** + * Sets the device orientation, in degrees, for the {@link RemoteConnection.VideoProvider}. + * Assumes that a standard portrait orientation of the device is 0 degrees. + * + * @param rotation The device orientation, in degrees. + * @see Connection.VideoProvider#onSetDeviceOrientation(int) + */ + public void setDeviceOrientation(int rotation) { + try { + mVideoProviderBinder.setDeviceOrientation(rotation); + } catch (RemoteException e) { + } + } + + /** + * Sets camera zoom ratio for the {@link RemoteConnection.VideoProvider}. + * + * @param value The camera zoom ratio. + * @see Connection.VideoProvider#onSetZoom(float) + */ + public void setZoom(float value) { + try { + mVideoProviderBinder.setZoom(value); + } catch (RemoteException e) { + } + } + + /** + * Issues a request to modify the properties of the current video session for the + * {@link RemoteConnection.VideoProvider}. + * + * @param fromProfile The video profile prior to the request. + * @param toProfile The video profile with the requested changes made. + * @see Connection.VideoProvider#onSendSessionModifyRequest(VideoProfile, VideoProfile) + */ + public void sendSessionModifyRequest(VideoProfile fromProfile, VideoProfile toProfile) { + try { + mVideoProviderBinder.sendSessionModifyRequest(fromProfile, toProfile); + } catch (RemoteException e) { + } + } + + /** + * Provides a response to a request to change the current call video session + * properties for the {@link RemoteConnection.VideoProvider}. + * + * @param responseProfile The response call video properties. + * @see Connection.VideoProvider#onSendSessionModifyResponse(VideoProfile) + */ + public void sendSessionModifyResponse(VideoProfile responseProfile) { + try { + mVideoProviderBinder.sendSessionModifyResponse(responseProfile); + } catch (RemoteException e) { + } + } + + /** + * Issues a request to retrieve the capabilities of the current camera for the + * {@link RemoteConnection.VideoProvider}. + * + * @see Connection.VideoProvider#onRequestCameraCapabilities() + */ + public void requestCameraCapabilities() { + try { + mVideoProviderBinder.requestCameraCapabilities(); + } catch (RemoteException e) { + } + } + + /** + * Issues a request to retrieve the data usage (in bytes) of the video portion of the + * {@link RemoteConnection} for the {@link RemoteConnection.VideoProvider}. + * + * @see Connection.VideoProvider#onRequestConnectionDataUsage() + */ + public void requestCallDataUsage() { + try { + mVideoProviderBinder.requestCallDataUsage(); + } catch (RemoteException e) { + } + } + + /** + * Sets the {@link Uri} of an image to be displayed to the peer device when the video signal + * is paused, for the {@link RemoteConnection.VideoProvider}. + * + * @see Connection.VideoProvider#onSetPauseImage(Uri) + */ + public void setPauseImage(Uri uri) { + try { + mVideoProviderBinder.setPauseImage(uri); + } catch (RemoteException e) { + } + } + } + + private IConnectionService mConnectionService; + private final String mConnectionId; + /** + * ConcurrentHashMap constructor params: 8 is initial table size, 0.9f is + * load factor before resizing, 1 means we only expect a single thread to + * access the map so make only a single shard + */ + private final Set<CallbackRecord> mCallbackRecords = Collections.newSetFromMap( + new ConcurrentHashMap<CallbackRecord, Boolean>(8, 0.9f, 1)); + private final List<RemoteConnection> mConferenceableConnections = new ArrayList<>(); + private final List<RemoteConnection> mUnmodifiableconferenceableConnections = + Collections.unmodifiableList(mConferenceableConnections); + + private int mState = Connection.STATE_NEW; + private DisconnectCause mDisconnectCause; + private boolean mRingbackRequested; + private boolean mConnected; + private int mConnectionCapabilities; + private int mConnectionProperties; + private int mVideoState; + private VideoProvider mVideoProvider; + private boolean mIsVoipAudioMode; + private StatusHints mStatusHints; + private Uri mAddress; + private int mAddressPresentation; + private String mCallerDisplayName; + private int mCallerDisplayNamePresentation; + private RemoteConference mConference; + private Bundle mExtras; + + /** + * @hide + */ + RemoteConnection( + String id, + IConnectionService connectionService, + ConnectionRequest request) { + mConnectionId = id; + mConnectionService = connectionService; + mConnected = true; + mState = Connection.STATE_INITIALIZING; + } + + /** + * @hide + */ + RemoteConnection(String callId, IConnectionService connectionService, + ParcelableConnection connection, String callingPackage, int targetSdkVersion) { + mConnectionId = callId; + mConnectionService = connectionService; + mConnected = true; + mState = connection.getState(); + mDisconnectCause = connection.getDisconnectCause(); + mRingbackRequested = connection.isRingbackRequested(); + mConnectionCapabilities = connection.getConnectionCapabilities(); + mConnectionProperties = connection.getConnectionProperties(); + mVideoState = connection.getVideoState(); + IVideoProvider videoProvider = connection.getVideoProvider(); + if (videoProvider != null) { + mVideoProvider = new RemoteConnection.VideoProvider(videoProvider, callingPackage, + targetSdkVersion); + } else { + mVideoProvider = null; + } + mIsVoipAudioMode = connection.getIsVoipAudioMode(); + mStatusHints = connection.getStatusHints(); + mAddress = connection.getHandle(); + mAddressPresentation = connection.getHandlePresentation(); + mCallerDisplayName = connection.getCallerDisplayName(); + mCallerDisplayNamePresentation = connection.getCallerDisplayNamePresentation(); + mConference = null; + putExtras(connection.getExtras()); + + // Stash the original connection ID as it exists in the source ConnectionService. + // Telecom will use this to avoid adding duplicates later. + // See comments on Connection.EXTRA_ORIGINAL_CONNECTION_ID for more information. + Bundle newExtras = new Bundle(); + newExtras.putString(Connection.EXTRA_ORIGINAL_CONNECTION_ID, callId); + putExtras(newExtras); + } + + /** + * Create a RemoteConnection which is used for failed connections. Note that using it for any + * "real" purpose will almost certainly fail. Callers should note the failure and act + * accordingly (moving on to another RemoteConnection, for example) + * + * @param disconnectCause The reason for the failed connection. + * @hide + */ + RemoteConnection(DisconnectCause disconnectCause) { + mConnectionId = "NULL"; + mConnected = false; + mState = Connection.STATE_DISCONNECTED; + mDisconnectCause = disconnectCause; + } + + /** + * Adds a callback to this {@code RemoteConnection}. + * + * @param callback A {@code Callback}. + */ + public void registerCallback(Callback callback) { + registerCallback(callback, new Handler()); + } + + /** + * Adds a callback to this {@code RemoteConnection}. + * + * @param callback A {@code Callback}. + * @param handler A {@code Handler} which command and status changes will be delivered to. + */ + public void registerCallback(Callback callback, Handler handler) { + unregisterCallback(callback); + if (callback != null && handler != null) { + mCallbackRecords.add(new CallbackRecord(callback, handler)); + } + } + + /** + * Removes a callback from this {@code RemoteConnection}. + * + * @param callback A {@code Callback}. + */ + public void unregisterCallback(Callback callback) { + if (callback != null) { + for (CallbackRecord record : mCallbackRecords) { + if (record.getCallback() == callback) { + mCallbackRecords.remove(record); + break; + } + } + } + } + + /** + * Obtains the state of this {@code RemoteConnection}. + * + * @return A state value, chosen from the {@code STATE_*} constants. + */ + public int getState() { + return mState; + } + + /** + * Obtains the reason why this {@code RemoteConnection} may have been disconnected. + * + * @return For a {@link Connection#STATE_DISCONNECTED} {@code RemoteConnection}, the + * disconnect cause expressed as a code chosen from among those declared in + * {@link DisconnectCause}. + */ + public DisconnectCause getDisconnectCause() { + return mDisconnectCause; + } + + /** + * Obtains the capabilities of this {@code RemoteConnection}. + * + * @return A bitmask of the capabilities of the {@code RemoteConnection}, as defined in + * the {@code CAPABILITY_*} constants in class {@link Connection}. + */ + public int getConnectionCapabilities() { + return mConnectionCapabilities; + } + + /** + * Obtains the properties of this {@code RemoteConnection}. + * + * @return A bitmask of the properties of the {@code RemoteConnection}, as defined in the + * {@code PROPERTY_*} constants in class {@link Connection}. + */ + public int getConnectionProperties() { + return mConnectionProperties; + } + + /** + * Determines if the audio mode of this {@code RemoteConnection} is VOIP. + * + * @return {@code true} if the {@code RemoteConnection}'s current audio mode is VOIP. + */ + public boolean isVoipAudioMode() { + return mIsVoipAudioMode; + } + + /** + * Obtains status hints pertaining to this {@code RemoteConnection}. + * + * @return The current {@link StatusHints} of this {@code RemoteConnection}, + * or {@code null} if none have been set. + */ + public StatusHints getStatusHints() { + return mStatusHints; + } + + /** + * Obtains the address of this {@code RemoteConnection}. + * + * @return The address (e.g., phone number) to which the {@code RemoteConnection} + * is currently connected. + */ + public Uri getAddress() { + return mAddress; + } + + /** + * Obtains the presentation requirements for the address of this {@code RemoteConnection}. + * + * @return The presentation requirements for the address. See + * {@link TelecomManager} for valid values. + */ + public int getAddressPresentation() { + return mAddressPresentation; + } + + /** + * Obtains the display name for this {@code RemoteConnection}'s caller. + * + * @return The display name for the caller. + */ + public CharSequence getCallerDisplayName() { + return mCallerDisplayName; + } + + /** + * Obtains the presentation requirements for this {@code RemoteConnection}'s + * caller's display name. + * + * @return The presentation requirements for the caller display name. See + * {@link TelecomManager} for valid values. + */ + public int getCallerDisplayNamePresentation() { + return mCallerDisplayNamePresentation; + } + + /** + * Obtains the video state of this {@code RemoteConnection}. + * + * @return The video state of the {@code RemoteConnection}. See {@link VideoProfile}. + */ + public int getVideoState() { + return mVideoState; + } + + /** + * Obtains the video provider of this {@code RemoteConnection}. + * @return The video provider associated with this {@code RemoteConnection}. + */ + public final VideoProvider getVideoProvider() { + return mVideoProvider; + } + + /** + * Obtain the extras associated with this {@code RemoteConnection}. + * + * @return The extras for this connection. + */ + public final Bundle getExtras() { + return mExtras; + } + + /** + * Determines whether this {@code RemoteConnection} is requesting ringback. + * + * @return Whether the {@code RemoteConnection} is requesting that the framework play a + * ringback tone on its behalf. + */ + public boolean isRingbackRequested() { + return mRingbackRequested; + } + + /** + * Instructs this {@code RemoteConnection} to abort. + */ + public void abort() { + try { + if (mConnected) { + mConnectionService.abort(mConnectionId, null /*Session.Info*/); + } + } catch (RemoteException ignored) { + } + } + + /** + * Instructs this {@link Connection#STATE_RINGING} {@code RemoteConnection} to answer. + */ + public void answer() { + try { + if (mConnected) { + mConnectionService.answer(mConnectionId, null /*Session.Info*/); + } + } catch (RemoteException ignored) { + } + } + + /** + * Instructs this {@link Connection#STATE_RINGING} {@code RemoteConnection} to answer. + * @param videoState The video state in which to answer the call. + * @hide + */ + public void answer(int videoState) { + try { + if (mConnected) { + mConnectionService.answerVideo(mConnectionId, videoState, null /*Session.Info*/); + } + } catch (RemoteException ignored) { + } + } + + /** + * Instructs this {@link Connection#STATE_RINGING} {@code RemoteConnection} to reject. + */ + public void reject() { + try { + if (mConnected) { + mConnectionService.reject(mConnectionId, null /*Session.Info*/); + } + } catch (RemoteException ignored) { + } + } + + /** + * Instructs this {@code RemoteConnection} to go on hold. + */ + public void hold() { + try { + if (mConnected) { + mConnectionService.hold(mConnectionId, null /*Session.Info*/); + } + } catch (RemoteException ignored) { + } + } + + /** + * Instructs this {@link Connection#STATE_HOLDING} call to release from hold. + */ + public void unhold() { + try { + if (mConnected) { + mConnectionService.unhold(mConnectionId, null /*Session.Info*/); + } + } catch (RemoteException ignored) { + } + } + + /** + * Instructs this {@code RemoteConnection} to disconnect. + */ + public void disconnect() { + try { + if (mConnected) { + mConnectionService.disconnect(mConnectionId, null /*Session.Info*/); + } + } catch (RemoteException ignored) { + } + } + + /** + * Instructs this {@code RemoteConnection} to play a dual-tone multi-frequency signaling + * (DTMF) tone. + * + * Any other currently playing DTMF tone in the specified call is immediately stopped. + * + * @param digit A character representing the DTMF digit for which to play the tone. This + * value must be one of {@code '0'} through {@code '9'}, {@code '*'} or {@code '#'}. + */ + public void playDtmfTone(char digit) { + try { + if (mConnected) { + mConnectionService.playDtmfTone(mConnectionId, digit, null /*Session.Info*/); + } + } catch (RemoteException ignored) { + } + } + + /** + * Instructs this {@code RemoteConnection} to stop any dual-tone multi-frequency signaling + * (DTMF) tone currently playing. + * + * DTMF tones are played by calling {@link #playDtmfTone(char)}. If no DTMF tone is + * currently playing, this method will do nothing. + */ + public void stopDtmfTone() { + try { + if (mConnected) { + mConnectionService.stopDtmfTone(mConnectionId, null /*Session.Info*/); + } + } catch (RemoteException ignored) { + } + } + + /** + * Instructs this {@code RemoteConnection} to continue playing a post-dial DTMF string. + * + * A post-dial DTMF string is a string of digits following the first instance of either + * {@link TelecomManager#DTMF_CHARACTER_WAIT} or {@link TelecomManager#DTMF_CHARACTER_PAUSE}. + * These digits are immediately sent as DTMF tones to the recipient as soon as the + * connection is made. + * + * If the DTMF string contains a {@link TelecomManager#DTMF_CHARACTER_PAUSE} symbol, this + * {@code RemoteConnection} will temporarily pause playing the tones for a pre-defined period + * of time. + * + * If the DTMF string contains a {@link TelecomManager#DTMF_CHARACTER_WAIT} symbol, this + * {@code RemoteConnection} will pause playing the tones and notify callbacks via + * {@link Callback#onPostDialWait(RemoteConnection, String)}. At this point, the in-call app + * should display to the user an indication of this state and an affordance to continue + * the postdial sequence. When the user decides to continue the postdial sequence, the in-call + * app should invoke the {@link #postDialContinue(boolean)} method. + * + * @param proceed Whether or not to continue with the post-dial sequence. + */ + public void postDialContinue(boolean proceed) { + try { + if (mConnected) { + mConnectionService.onPostDialContinue(mConnectionId, proceed, + null /*Session.Info*/); + } + } catch (RemoteException ignored) { + } + } + + /** + * Instructs this {@link RemoteConnection} to pull itself to the local device. + * <p> + * See {@link Call#pullExternalCall()} for more information. + */ + public void pullExternalCall() { + try { + if (mConnected) { + mConnectionService.pullExternalCall(mConnectionId, null /*Session.Info*/); + } + } catch (RemoteException ignored) { + } + } + + /** + * Set the audio state of this {@code RemoteConnection}. + * + * @param state The audio state of this {@code RemoteConnection}. + * @hide + * @deprecated Use {@link #setCallAudioState(CallAudioState) instead. + */ + @SystemApi + @Deprecated + public void setAudioState(AudioState state) { + setCallAudioState(new CallAudioState(state)); + } + + /** + * Set the audio state of this {@code RemoteConnection}. + * + * @param state The audio state of this {@code RemoteConnection}. + */ + public void setCallAudioState(CallAudioState state) { + try { + if (mConnected) { + mConnectionService.onCallAudioStateChanged(mConnectionId, state, + null /*Session.Info*/); + } + } catch (RemoteException ignored) { + } + } + + /** + * Notifies this {@link RemoteConnection} that the user has requested an RTT session. + * @param rttTextStream The object that should be used to send text to or receive text from + * the in-call app. + * @hide + */ + public void startRtt(@NonNull Connection.RttTextStream rttTextStream) { + try { + if (mConnected) { + mConnectionService.startRtt(mConnectionId, rttTextStream.getFdFromInCall(), + rttTextStream.getFdToInCall(), null /*Session.Info*/); + } + } catch (RemoteException ignored) { + } + } + + /** + * Notifies this {@link RemoteConnection} that it should terminate any existing RTT + * session. No response to Telecom is needed for this method. + * @hide + */ + public void stopRtt() { + try { + if (mConnected) { + mConnectionService.stopRtt(mConnectionId, null /*Session.Info*/); + } + } catch (RemoteException ignored) { + } + } + + /** + * Notifies this {@link RemoteConnection} of a response to a previous remotely-initiated RTT + * upgrade request sent via {@link Connection#sendRemoteRttRequest}. + * Acceptance of the request is indicated by the supplied {@link RttTextStream} being non-null, + * and rejection is indicated by {@code rttTextStream} being {@code null} + * @hide + * @param rttTextStream The object that should be used to send text to or receive text from + * the in-call app. + */ + public void sendRttUpgradeResponse(@Nullable Connection.RttTextStream rttTextStream) { + try { + if (mConnected) { + if (rttTextStream == null) { + mConnectionService.respondToRttUpgradeRequest(mConnectionId, + null, null, null /*Session.Info*/); + } else { + mConnectionService.respondToRttUpgradeRequest(mConnectionId, + rttTextStream.getFdFromInCall(), rttTextStream.getFdToInCall(), + null /*Session.Info*/); + } + } + } catch (RemoteException ignored) { + } + } + + /** + * Obtain the {@code RemoteConnection}s with which this {@code RemoteConnection} may be + * successfully asked to create a conference with. + * + * @return The {@code RemoteConnection}s with which this {@code RemoteConnection} may be + * merged into a {@link RemoteConference}. + */ + public List<RemoteConnection> getConferenceableConnections() { + return mUnmodifiableconferenceableConnections; + } + + /** + * Obtain the {@code RemoteConference} that this {@code RemoteConnection} may be a part + * of, or {@code null} if there is no such {@code RemoteConference}. + * + * @return A {@code RemoteConference} or {@code null}; + */ + public RemoteConference getConference() { + return mConference; + } + + /** {@hide} */ + String getId() { + return mConnectionId; + } + + /** {@hide} */ + IConnectionService getConnectionService() { + return mConnectionService; + } + + /** + * @hide + */ + void setState(final int state) { + if (mState != state) { + mState = state; + for (CallbackRecord record: mCallbackRecords) { + final RemoteConnection connection = this; + final Callback callback = record.getCallback(); + record.getHandler().post(new Runnable() { + @Override + public void run() { + callback.onStateChanged(connection, state); + } + }); + } + } + } + + /** + * @hide + */ + void setDisconnected(final DisconnectCause disconnectCause) { + if (mState != Connection.STATE_DISCONNECTED) { + mState = Connection.STATE_DISCONNECTED; + mDisconnectCause = disconnectCause; + + for (CallbackRecord record : mCallbackRecords) { + final RemoteConnection connection = this; + final Callback callback = record.getCallback(); + record.getHandler().post(new Runnable() { + @Override + public void run() { + callback.onDisconnected(connection, disconnectCause); + } + }); + } + } + } + + /** + * @hide + */ + void setRingbackRequested(final boolean ringback) { + if (mRingbackRequested != ringback) { + mRingbackRequested = ringback; + for (CallbackRecord record : mCallbackRecords) { + final RemoteConnection connection = this; + final Callback callback = record.getCallback(); + record.getHandler().post(new Runnable() { + @Override + public void run() { + callback.onRingbackRequested(connection, ringback); + } + }); + } + } + } + + /** + * @hide + */ + void setConnectionCapabilities(final int connectionCapabilities) { + mConnectionCapabilities = connectionCapabilities; + for (CallbackRecord record : mCallbackRecords) { + final RemoteConnection connection = this; + final Callback callback = record.getCallback(); + record.getHandler().post(new Runnable() { + @Override + public void run() { + callback.onConnectionCapabilitiesChanged(connection, connectionCapabilities); + } + }); + } + } + + /** + * @hide + */ + void setConnectionProperties(final int connectionProperties) { + mConnectionProperties = connectionProperties; + for (CallbackRecord record : mCallbackRecords) { + final RemoteConnection connection = this; + final Callback callback = record.getCallback(); + record.getHandler().post(new Runnable() { + @Override + public void run() { + callback.onConnectionPropertiesChanged(connection, connectionProperties); + } + }); + } + } + + /** + * @hide + */ + void setDestroyed() { + if (!mCallbackRecords.isEmpty()) { + // Make sure that the callbacks are notified that the call is destroyed first. + if (mState != Connection.STATE_DISCONNECTED) { + setDisconnected( + new DisconnectCause(DisconnectCause.ERROR, "Connection destroyed.")); + } + + for (CallbackRecord record : mCallbackRecords) { + final RemoteConnection connection = this; + final Callback callback = record.getCallback(); + record.getHandler().post(new Runnable() { + @Override + public void run() { + callback.onDestroyed(connection); + } + }); + } + mCallbackRecords.clear(); + + mConnected = false; + } + } + + /** + * @hide + */ + void setPostDialWait(final String remainingDigits) { + for (CallbackRecord record : mCallbackRecords) { + final RemoteConnection connection = this; + final Callback callback = record.getCallback(); + record.getHandler().post(new Runnable() { + @Override + public void run() { + callback.onPostDialWait(connection, remainingDigits); + } + }); + } + } + + /** + * @hide + */ + void onPostDialChar(final char nextChar) { + for (CallbackRecord record : mCallbackRecords) { + final RemoteConnection connection = this; + final Callback callback = record.getCallback(); + record.getHandler().post(new Runnable() { + @Override + public void run() { + callback.onPostDialChar(connection, nextChar); + } + }); + } + } + + /** + * @hide + */ + void setVideoState(final int videoState) { + mVideoState = videoState; + for (CallbackRecord record : mCallbackRecords) { + final RemoteConnection connection = this; + final Callback callback = record.getCallback(); + record.getHandler().post(new Runnable() { + @Override + public void run() { + callback.onVideoStateChanged(connection, videoState); + } + }); + } + } + + /** + * @hide + */ + void setVideoProvider(final VideoProvider videoProvider) { + mVideoProvider = videoProvider; + for (CallbackRecord record : mCallbackRecords) { + final RemoteConnection connection = this; + final Callback callback = record.getCallback(); + record.getHandler().post(new Runnable() { + @Override + public void run() { + callback.onVideoProviderChanged(connection, videoProvider); + } + }); + } + } + + /** @hide */ + void setIsVoipAudioMode(final boolean isVoip) { + mIsVoipAudioMode = isVoip; + for (CallbackRecord record : mCallbackRecords) { + final RemoteConnection connection = this; + final Callback callback = record.getCallback(); + record.getHandler().post(new Runnable() { + @Override + public void run() { + callback.onVoipAudioChanged(connection, isVoip); + } + }); + } + } + + /** @hide */ + void setStatusHints(final StatusHints statusHints) { + mStatusHints = statusHints; + for (CallbackRecord record : mCallbackRecords) { + final RemoteConnection connection = this; + final Callback callback = record.getCallback(); + record.getHandler().post(new Runnable() { + @Override + public void run() { + callback.onStatusHintsChanged(connection, statusHints); + } + }); + } + } + + /** @hide */ + void setAddress(final Uri address, final int presentation) { + mAddress = address; + mAddressPresentation = presentation; + for (CallbackRecord record : mCallbackRecords) { + final RemoteConnection connection = this; + final Callback callback = record.getCallback(); + record.getHandler().post(new Runnable() { + @Override + public void run() { + callback.onAddressChanged(connection, address, presentation); + } + }); + } + } + + /** @hide */ + void setCallerDisplayName(final String callerDisplayName, final int presentation) { + mCallerDisplayName = callerDisplayName; + mCallerDisplayNamePresentation = presentation; + for (CallbackRecord record : mCallbackRecords) { + final RemoteConnection connection = this; + final Callback callback = record.getCallback(); + record.getHandler().post(new Runnable() { + @Override + public void run() { + callback.onCallerDisplayNameChanged( + connection, callerDisplayName, presentation); + } + }); + } + } + + /** @hide */ + void setConferenceableConnections(final List<RemoteConnection> conferenceableConnections) { + mConferenceableConnections.clear(); + mConferenceableConnections.addAll(conferenceableConnections); + for (CallbackRecord record : mCallbackRecords) { + final RemoteConnection connection = this; + final Callback callback = record.getCallback(); + record.getHandler().post(new Runnable() { + @Override + public void run() { + callback.onConferenceableConnectionsChanged( + connection, mUnmodifiableconferenceableConnections); + } + }); + } + } + + /** @hide */ + void setConference(final RemoteConference conference) { + if (mConference != conference) { + mConference = conference; + for (CallbackRecord record : mCallbackRecords) { + final RemoteConnection connection = this; + final Callback callback = record.getCallback(); + record.getHandler().post(new Runnable() { + @Override + public void run() { + callback.onConferenceChanged(connection, conference); + } + }); + } + } + } + + /** @hide */ + void putExtras(final Bundle extras) { + if (extras == null) { + return; + } + if (mExtras == null) { + mExtras = new Bundle(); + } + try { + mExtras.putAll(extras); + } catch (BadParcelableException bpe) { + Log.w(this, "putExtras: could not unmarshal extras; exception = " + bpe); + } + + notifyExtrasChanged(); + } + + /** @hide */ + void removeExtras(List<String> keys) { + if (mExtras == null || keys == null || keys.isEmpty()) { + return; + } + for (String key : keys) { + mExtras.remove(key); + } + + notifyExtrasChanged(); + } + + private void notifyExtrasChanged() { + for (CallbackRecord record : mCallbackRecords) { + final RemoteConnection connection = this; + final Callback callback = record.getCallback(); + record.getHandler().post(new Runnable() { + @Override + public void run() { + callback.onExtrasChanged(connection, mExtras); + } + }); + } + } + + /** @hide */ + void onConnectionEvent(final String event, final Bundle extras) { + for (CallbackRecord record : mCallbackRecords) { + final RemoteConnection connection = this; + final Callback callback = record.getCallback(); + record.getHandler().post(new Runnable() { + @Override + public void run() { + callback.onConnectionEvent(connection, event, extras); + } + }); + } + } + + /** @hide */ + void onRttInitiationSuccess() { + for (CallbackRecord record : mCallbackRecords) { + final RemoteConnection connection = this; + final Callback callback = record.getCallback(); + record.getHandler().post( + () -> callback.onRttInitiationSuccess(connection)); + } + } + + /** @hide */ + void onRttInitiationFailure(int reason) { + for (CallbackRecord record : mCallbackRecords) { + final RemoteConnection connection = this; + final Callback callback = record.getCallback(); + record.getHandler().post( + () -> callback.onRttInitiationFailure(connection, reason)); + } + } + + /** @hide */ + void onRttSessionRemotelyTerminated() { + for (CallbackRecord record : mCallbackRecords) { + final RemoteConnection connection = this; + final Callback callback = record.getCallback(); + record.getHandler().post( + () -> callback.onRttSessionRemotelyTerminated(connection)); + } + } + + /** @hide */ + void onRemoteRttRequest() { + for (CallbackRecord record : mCallbackRecords) { + final RemoteConnection connection = this; + final Callback callback = record.getCallback(); + record.getHandler().post( + () -> callback.onRemoteRttRequest(connection)); + } + } + + /** + /** + * Create a RemoteConnection represents a failure, and which will be in + * {@link Connection#STATE_DISCONNECTED}. Attempting to use it for anything will almost + * certainly result in bad things happening. Do not do this. + * + * @return a failed {@link RemoteConnection} + * + * @hide + */ + public static RemoteConnection failure(DisconnectCause disconnectCause) { + return new RemoteConnection(disconnectCause); + } + + private static final class CallbackRecord extends Callback { + private final Callback mCallback; + private final Handler mHandler; + + public CallbackRecord(Callback callback, Handler handler) { + mCallback = callback; + mHandler = handler; + } + + public Callback getCallback() { + return mCallback; + } + + public Handler getHandler() { + return mHandler; + } + } +} diff --git a/android/telecom/RemoteConnectionManager.java b/android/telecom/RemoteConnectionManager.java new file mode 100644 index 00000000..0322218d --- /dev/null +++ b/android/telecom/RemoteConnectionManager.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2014 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 + R* limitations under the License. + */ + +package android.telecom; + +import android.content.ComponentName; +import android.os.RemoteException; + +import com.android.internal.telecom.IConnectionService; + +import java.util.HashMap; +import java.util.Map; + +/** + * @hide + */ +public class RemoteConnectionManager { + private final Map<ComponentName, RemoteConnectionService> mRemoteConnectionServices = + new HashMap<>(); + private final ConnectionService mOurConnectionServiceImpl; + + public RemoteConnectionManager(ConnectionService ourConnectionServiceImpl) { + mOurConnectionServiceImpl = ourConnectionServiceImpl; + } + + void addConnectionService( + ComponentName componentName, + IConnectionService outgoingConnectionServiceRpc) { + if (!mRemoteConnectionServices.containsKey(componentName)) { + try { + RemoteConnectionService remoteConnectionService = new RemoteConnectionService( + outgoingConnectionServiceRpc, + mOurConnectionServiceImpl); + mRemoteConnectionServices.put(componentName, remoteConnectionService); + } catch (RemoteException ignored) { + } + } + } + + public RemoteConnection createRemoteConnection( + PhoneAccountHandle connectionManagerPhoneAccount, + ConnectionRequest request, + boolean isIncoming) { + PhoneAccountHandle accountHandle = request.getAccountHandle(); + if (accountHandle == null) { + throw new IllegalArgumentException("accountHandle must be specified."); + } + + ComponentName componentName = request.getAccountHandle().getComponentName(); + if (!mRemoteConnectionServices.containsKey(componentName)) { + throw new UnsupportedOperationException("accountHandle not supported: " + + componentName); + } + + RemoteConnectionService remoteService = mRemoteConnectionServices.get(componentName); + if (remoteService != null) { + return remoteService.createRemoteConnection( + connectionManagerPhoneAccount, request, isIncoming); + } + return null; + } + + public void conferenceRemoteConnections(RemoteConnection a, RemoteConnection b) { + if (a.getConnectionService() == b.getConnectionService()) { + try { + a.getConnectionService().conference(a.getId(), b.getId(), null /*Session.Info*/); + } catch (RemoteException e) { + } + } else { + Log.w(this, "Request to conference incompatible remote connections (%s,%s) (%s,%s)", + a.getConnectionService(), a.getId(), + b.getConnectionService(), b.getId()); + } + } +} diff --git a/android/telecom/RemoteConnectionService.java b/android/telecom/RemoteConnectionService.java new file mode 100644 index 00000000..2cc43143 --- /dev/null +++ b/android/telecom/RemoteConnectionService.java @@ -0,0 +1,575 @@ +/* + * Copyright (C) 2014 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.net.Uri; +import android.os.Bundle; +import android.os.IBinder; +import android.os.IBinder.DeathRecipient; +import android.os.RemoteException; +import android.telecom.Logging.Session; + +import com.android.internal.telecom.IConnectionService; +import com.android.internal.telecom.IConnectionServiceAdapter; +import com.android.internal.telecom.IVideoProvider; +import com.android.internal.telecom.RemoteServiceCallback; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.List; +import java.util.UUID; + +/** + * Remote connection service which other connection services can use to place calls on their behalf. + * + * @hide + */ +final class RemoteConnectionService { + + // Note: Casting null to avoid ambiguous constructor reference. + private static final RemoteConnection NULL_CONNECTION = + new RemoteConnection("NULL", null, (ConnectionRequest) null); + + private static final RemoteConference NULL_CONFERENCE = + new RemoteConference("NULL", null); + + private final IConnectionServiceAdapter mServantDelegate = new IConnectionServiceAdapter() { + @Override + public void handleCreateConnectionComplete( + String id, + ConnectionRequest request, + ParcelableConnection parcel, + Session.Info info) { + RemoteConnection connection = + findConnectionForAction(id, "handleCreateConnectionSuccessful"); + if (connection != NULL_CONNECTION && mPendingConnections.contains(connection)) { + mPendingConnections.remove(connection); + // Unconditionally initialize the connection ... + connection.setConnectionCapabilities(parcel.getConnectionCapabilities()); + connection.setConnectionProperties(parcel.getConnectionProperties()); + if (parcel.getHandle() != null + || parcel.getState() != Connection.STATE_DISCONNECTED) { + connection.setAddress(parcel.getHandle(), parcel.getHandlePresentation()); + } + if (parcel.getCallerDisplayName() != null + || parcel.getState() != Connection.STATE_DISCONNECTED) { + connection.setCallerDisplayName( + parcel.getCallerDisplayName(), + parcel.getCallerDisplayNamePresentation()); + } + // Set state after handle so that the client can identify the connection. + if (parcel.getState() == Connection.STATE_DISCONNECTED) { + connection.setDisconnected(parcel.getDisconnectCause()); + } else { + connection.setState(parcel.getState()); + } + List<RemoteConnection> conferenceable = new ArrayList<>(); + for (String confId : parcel.getConferenceableConnectionIds()) { + if (mConnectionById.containsKey(confId)) { + conferenceable.add(mConnectionById.get(confId)); + } + } + connection.setConferenceableConnections(conferenceable); + connection.setVideoState(parcel.getVideoState()); + if (connection.getState() == Connection.STATE_DISCONNECTED) { + // ... then, if it was created in a disconnected state, that indicates + // failure on the providing end, so immediately mark it destroyed + connection.setDestroyed(); + } + } + } + + @Override + public void setActive(String callId, Session.Info sessionInfo) { + if (mConnectionById.containsKey(callId)) { + findConnectionForAction(callId, "setActive") + .setState(Connection.STATE_ACTIVE); + } else { + findConferenceForAction(callId, "setActive") + .setState(Connection.STATE_ACTIVE); + } + } + + @Override + public void setRinging(String callId, Session.Info sessionInfo) { + findConnectionForAction(callId, "setRinging") + .setState(Connection.STATE_RINGING); + } + + @Override + public void setDialing(String callId, Session.Info sessionInfo) { + findConnectionForAction(callId, "setDialing") + .setState(Connection.STATE_DIALING); + } + + @Override + public void setPulling(String callId, Session.Info sessionInfo) { + findConnectionForAction(callId, "setPulling") + .setState(Connection.STATE_PULLING_CALL); + } + + @Override + public void setDisconnected(String callId, DisconnectCause disconnectCause, + Session.Info sessionInfo) { + if (mConnectionById.containsKey(callId)) { + findConnectionForAction(callId, "setDisconnected") + .setDisconnected(disconnectCause); + } else { + findConferenceForAction(callId, "setDisconnected") + .setDisconnected(disconnectCause); + } + } + + @Override + public void setOnHold(String callId, Session.Info sessionInfo) { + if (mConnectionById.containsKey(callId)) { + findConnectionForAction(callId, "setOnHold") + .setState(Connection.STATE_HOLDING); + } else { + findConferenceForAction(callId, "setOnHold") + .setState(Connection.STATE_HOLDING); + } + } + + @Override + public void setRingbackRequested(String callId, boolean ringing, Session.Info sessionInfo) { + findConnectionForAction(callId, "setRingbackRequested") + .setRingbackRequested(ringing); + } + + @Override + public void setConnectionCapabilities(String callId, int connectionCapabilities, + Session.Info sessionInfo) { + if (mConnectionById.containsKey(callId)) { + findConnectionForAction(callId, "setConnectionCapabilities") + .setConnectionCapabilities(connectionCapabilities); + } else { + findConferenceForAction(callId, "setConnectionCapabilities") + .setConnectionCapabilities(connectionCapabilities); + } + } + + @Override + public void setConnectionProperties(String callId, int connectionProperties, + Session.Info sessionInfo) { + if (mConnectionById.containsKey(callId)) { + findConnectionForAction(callId, "setConnectionProperties") + .setConnectionProperties(connectionProperties); + } else { + findConferenceForAction(callId, "setConnectionProperties") + .setConnectionProperties(connectionProperties); + } + } + + @Override + public void setIsConferenced(String callId, String conferenceCallId, + Session.Info sessionInfo) { + // Note: callId should not be null; conferenceCallId may be null + RemoteConnection connection = + findConnectionForAction(callId, "setIsConferenced"); + if (connection != NULL_CONNECTION) { + if (conferenceCallId == null) { + // 'connection' is being split from its conference + if (connection.getConference() != null) { + connection.getConference().removeConnection(connection); + } + } else { + RemoteConference conference = + findConferenceForAction(conferenceCallId, "setIsConferenced"); + if (conference != NULL_CONFERENCE) { + conference.addConnection(connection); + } + } + } + } + + @Override + public void setConferenceMergeFailed(String callId, Session.Info sessionInfo) { + // Nothing to do here. + // The event has already been handled and there is no state to update + // in the underlying connection or conference objects + } + + @Override + public void onPhoneAccountChanged(String callId, PhoneAccountHandle pHandle, + Session.Info sessionInfo) { + } + + @Override + public void addConferenceCall( + final String callId, ParcelableConference parcel, Session.Info sessionInfo) { + RemoteConference conference = new RemoteConference(callId, + mOutgoingConnectionServiceRpc); + + for (String id : parcel.getConnectionIds()) { + RemoteConnection c = mConnectionById.get(id); + if (c != null) { + conference.addConnection(c); + } + } + if (conference.getConnections().size() == 0) { + // A conference was created, but none of its connections are ones that have been + // created by, and therefore being tracked by, this remote connection service. It + // is of no interest to us. + Log.d(this, "addConferenceCall - skipping"); + return; + } + + conference.setState(parcel.getState()); + conference.setConnectionCapabilities(parcel.getConnectionCapabilities()); + conference.setConnectionProperties(parcel.getConnectionProperties()); + conference.putExtras(parcel.getExtras()); + mConferenceById.put(callId, conference); + + // Stash the original connection ID as it exists in the source ConnectionService. + // Telecom will use this to avoid adding duplicates later. + // See comments on Connection.EXTRA_ORIGINAL_CONNECTION_ID for more information. + Bundle newExtras = new Bundle(); + newExtras.putString(Connection.EXTRA_ORIGINAL_CONNECTION_ID, callId); + conference.putExtras(newExtras); + + conference.registerCallback(new RemoteConference.Callback() { + @Override + public void onDestroyed(RemoteConference c) { + mConferenceById.remove(callId); + maybeDisconnectAdapter(); + } + }); + + mOurConnectionServiceImpl.addRemoteConference(conference); + } + + @Override + public void removeCall(String callId, Session.Info sessionInfo) { + if (mConnectionById.containsKey(callId)) { + findConnectionForAction(callId, "removeCall") + .setDestroyed(); + } else { + findConferenceForAction(callId, "removeCall") + .setDestroyed(); + } + } + + @Override + public void onPostDialWait(String callId, String remaining, Session.Info sessionInfo) { + findConnectionForAction(callId, "onPostDialWait") + .setPostDialWait(remaining); + } + + @Override + public void onPostDialChar(String callId, char nextChar, Session.Info sessionInfo) { + findConnectionForAction(callId, "onPostDialChar") + .onPostDialChar(nextChar); + } + + @Override + public void queryRemoteConnectionServices(RemoteServiceCallback callback, + Session.Info sessionInfo) { + // Not supported from remote connection service. + } + + @Override + public void setVideoProvider(String callId, IVideoProvider videoProvider, + Session.Info sessionInfo) { + + String callingPackage = mOurConnectionServiceImpl.getApplicationContext() + .getOpPackageName(); + int targetSdkVersion = mOurConnectionServiceImpl.getApplicationInfo().targetSdkVersion; + RemoteConnection.VideoProvider remoteVideoProvider = null; + if (videoProvider != null) { + remoteVideoProvider = new RemoteConnection.VideoProvider(videoProvider, + callingPackage, targetSdkVersion); + } + findConnectionForAction(callId, "setVideoProvider") + .setVideoProvider(remoteVideoProvider); + } + + @Override + public void setVideoState(String callId, int videoState, Session.Info sessionInfo) { + findConnectionForAction(callId, "setVideoState") + .setVideoState(videoState); + } + + @Override + public void setIsVoipAudioMode(String callId, boolean isVoip, Session.Info sessionInfo) { + findConnectionForAction(callId, "setIsVoipAudioMode") + .setIsVoipAudioMode(isVoip); + } + + @Override + public void setStatusHints(String callId, StatusHints statusHints, + Session.Info sessionInfo) { + findConnectionForAction(callId, "setStatusHints") + .setStatusHints(statusHints); + } + + @Override + public void setAddress(String callId, Uri address, int presentation, + Session.Info sessionInfo) { + findConnectionForAction(callId, "setAddress") + .setAddress(address, presentation); + } + + @Override + public void setCallerDisplayName(String callId, String callerDisplayName, + int presentation, Session.Info sessionInfo) { + findConnectionForAction(callId, "setCallerDisplayName") + .setCallerDisplayName(callerDisplayName, presentation); + } + + @Override + public IBinder asBinder() { + throw new UnsupportedOperationException(); + } + + @Override + public final void setConferenceableConnections(String callId, + List<String> conferenceableConnectionIds, Session.Info sessionInfo) { + List<RemoteConnection> conferenceable = new ArrayList<>(); + for (String id : conferenceableConnectionIds) { + if (mConnectionById.containsKey(id)) { + conferenceable.add(mConnectionById.get(id)); + } + } + + if (hasConnection(callId)) { + findConnectionForAction(callId, "setConferenceableConnections") + .setConferenceableConnections(conferenceable); + } else { + findConferenceForAction(callId, "setConferenceableConnections") + .setConferenceableConnections(conferenceable); + } + } + + @Override + public void addExistingConnection(String callId, ParcelableConnection connection, + Session.Info sessionInfo) { + String callingPackage = mOurConnectionServiceImpl.getApplicationContext(). + getOpPackageName(); + int callingTargetSdkVersion = mOurConnectionServiceImpl.getApplicationInfo() + .targetSdkVersion; + RemoteConnection remoteConnection = new RemoteConnection(callId, + mOutgoingConnectionServiceRpc, connection, callingPackage, + callingTargetSdkVersion); + mConnectionById.put(callId, remoteConnection); + remoteConnection.registerCallback(new RemoteConnection.Callback() { + @Override + public void onDestroyed(RemoteConnection connection) { + mConnectionById.remove(callId); + maybeDisconnectAdapter(); + } + }); + mOurConnectionServiceImpl.addRemoteExistingConnection(remoteConnection); + } + + @Override + public void putExtras(String callId, Bundle extras, Session.Info sessionInfo) { + if (hasConnection(callId)) { + findConnectionForAction(callId, "putExtras").putExtras(extras); + } else { + findConferenceForAction(callId, "putExtras").putExtras(extras); + } + } + + @Override + public void removeExtras(String callId, List<String> keys, Session.Info sessionInfo) { + if (hasConnection(callId)) { + findConnectionForAction(callId, "removeExtra").removeExtras(keys); + } else { + findConferenceForAction(callId, "removeExtra").removeExtras(keys); + } + } + + @Override + public void setAudioRoute(String callId, int audioRoute, Session.Info sessionInfo) { + if (hasConnection(callId)) { + // TODO(3pcalls): handle this for remote connections. + // Likely we don't want to do anything since it doesn't make sense for self-managed + // connections to go through a connection mgr. + } + } + + @Override + public void onConnectionEvent(String callId, String event, Bundle extras, + Session.Info sessionInfo) { + if (mConnectionById.containsKey(callId)) { + findConnectionForAction(callId, "onConnectionEvent").onConnectionEvent(event, + extras); + } + } + + @Override + public void onRttInitiationSuccess(String callId, Session.Info sessionInfo) + throws RemoteException { + if (hasConnection(callId)) { + findConnectionForAction(callId, "onRttInitiationSuccess") + .onRttInitiationSuccess(); + } else { + Log.w(this, "onRttInitiationSuccess called on a remote conference"); + } + } + + @Override + public void onRttInitiationFailure(String callId, int reason, Session.Info sessionInfo) + throws RemoteException { + if (hasConnection(callId)) { + findConnectionForAction(callId, "onRttInitiationFailure") + .onRttInitiationFailure(reason); + } else { + Log.w(this, "onRttInitiationFailure called on a remote conference"); + } + } + + @Override + public void onRttSessionRemotelyTerminated(String callId, Session.Info sessionInfo) + throws RemoteException { + if (hasConnection(callId)) { + findConnectionForAction(callId, "onRttSessionRemotelyTerminated") + .onRttSessionRemotelyTerminated(); + } else { + Log.w(this, "onRttSessionRemotelyTerminated called on a remote conference"); + } + } + + @Override + public void onRemoteRttRequest(String callId, Session.Info sessionInfo) + throws RemoteException { + if (hasConnection(callId)) { + findConnectionForAction(callId, "onRemoteRttRequest") + .onRemoteRttRequest(); + } else { + Log.w(this, "onRemoteRttRequest called on a remote conference"); + } + } + }; + + private final ConnectionServiceAdapterServant mServant = + new ConnectionServiceAdapterServant(mServantDelegate); + + private final DeathRecipient mDeathRecipient = new DeathRecipient() { + @Override + public void binderDied() { + for (RemoteConnection c : mConnectionById.values()) { + c.setDestroyed(); + } + for (RemoteConference c : mConferenceById.values()) { + c.setDestroyed(); + } + mConnectionById.clear(); + mConferenceById.clear(); + mPendingConnections.clear(); + mOutgoingConnectionServiceRpc.asBinder().unlinkToDeath(mDeathRecipient, 0); + } + }; + + private final IConnectionService mOutgoingConnectionServiceRpc; + private final ConnectionService mOurConnectionServiceImpl; + private final Map<String, RemoteConnection> mConnectionById = new HashMap<>(); + private final Map<String, RemoteConference> mConferenceById = new HashMap<>(); + private final Set<RemoteConnection> mPendingConnections = new HashSet<>(); + + RemoteConnectionService( + IConnectionService outgoingConnectionServiceRpc, + ConnectionService ourConnectionServiceImpl) throws RemoteException { + mOutgoingConnectionServiceRpc = outgoingConnectionServiceRpc; + mOutgoingConnectionServiceRpc.asBinder().linkToDeath(mDeathRecipient, 0); + mOurConnectionServiceImpl = ourConnectionServiceImpl; + } + + @Override + public String toString() { + return "[RemoteCS - " + mOutgoingConnectionServiceRpc.asBinder().toString() + "]"; + } + + final RemoteConnection createRemoteConnection( + PhoneAccountHandle connectionManagerPhoneAccount, + ConnectionRequest request, + boolean isIncoming) { + final String id = UUID.randomUUID().toString(); + final ConnectionRequest newRequest = new ConnectionRequest.Builder() + .setAccountHandle(request.getAccountHandle()) + .setAddress(request.getAddress()) + .setExtras(request.getExtras()) + .setVideoState(request.getVideoState()) + .setRttPipeFromInCall(request.getRttPipeFromInCall()) + .setRttPipeToInCall(request.getRttPipeToInCall()) + .build(); + try { + if (mConnectionById.isEmpty()) { + mOutgoingConnectionServiceRpc.addConnectionServiceAdapter(mServant.getStub(), + null /*Session.Info*/); + } + RemoteConnection connection = + new RemoteConnection(id, mOutgoingConnectionServiceRpc, newRequest); + mPendingConnections.add(connection); + mConnectionById.put(id, connection); + mOutgoingConnectionServiceRpc.createConnection( + connectionManagerPhoneAccount, + id, + newRequest, + isIncoming, + false /* isUnknownCall */, + null /*Session.info*/); + connection.registerCallback(new RemoteConnection.Callback() { + @Override + public void onDestroyed(RemoteConnection connection) { + mConnectionById.remove(id); + maybeDisconnectAdapter(); + } + }); + return connection; + } catch (RemoteException e) { + return RemoteConnection.failure( + new DisconnectCause(DisconnectCause.ERROR, e.toString())); + } + } + + private boolean hasConnection(String callId) { + return mConnectionById.containsKey(callId); + } + + private RemoteConnection findConnectionForAction( + String callId, String action) { + if (mConnectionById.containsKey(callId)) { + return mConnectionById.get(callId); + } + Log.w(this, "%s - Cannot find Connection %s", action, callId); + return NULL_CONNECTION; + } + + private RemoteConference findConferenceForAction( + String callId, String action) { + if (mConferenceById.containsKey(callId)) { + return mConferenceById.get(callId); + } + Log.w(this, "%s - Cannot find Conference %s", action, callId); + return NULL_CONFERENCE; + } + + private void maybeDisconnectAdapter() { + if (mConnectionById.isEmpty() && mConferenceById.isEmpty()) { + try { + mOutgoingConnectionServiceRpc.removeConnectionServiceAdapter(mServant.getStub(), + null /*Session.info*/); + } catch (RemoteException e) { + } + } + } +} diff --git a/android/telecom/Response.java b/android/telecom/Response.java new file mode 100644 index 00000000..ce7a7612 --- /dev/null +++ b/android/telecom/Response.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2014 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 + */ +public interface Response<IN, OUT> { + + /** + * Provide a set of results. + * + * @param request The original request. + * @param result The results. + */ + void onResult(IN request, OUT... result); + + /** + * Indicates the inability to provide results. + * + * @param request The original request. + * @param code An integer code indicating the reason for failure. + * @param msg A message explaining the reason for failure. + */ + void onError(IN request, int code, String msg); +} diff --git a/android/telecom/StatusHints.java b/android/telecom/StatusHints.java new file mode 100644 index 00000000..453f408b --- /dev/null +++ b/android/telecom/StatusHints.java @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2014 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.SystemApi; +import android.content.ComponentName; +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.Icon; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.Objects; + +/** + * Contains status label and icon displayed in the in-call UI. + */ +public final class StatusHints implements Parcelable { + + private final CharSequence mLabel; + private final Icon mIcon; + private final Bundle mExtras; + + /** + * @hide + */ + @SystemApi @Deprecated + public StatusHints(ComponentName packageName, CharSequence label, int iconResId, + Bundle extras) { + this(label, iconResId == 0 ? null : Icon.createWithResource(packageName.getPackageName(), + iconResId), extras); + } + + public StatusHints(CharSequence label, Icon icon, Bundle extras) { + mLabel = label; + mIcon = icon; + mExtras = extras; + } + + /** + * @return A package used to load the icon. + * + * @hide + */ + @SystemApi @Deprecated + public ComponentName getPackageName() { + // Minimal compatibility shim for legacy apps' tests + return new ComponentName("", ""); + } + + /** + * @return The label displayed in the in-call UI. + */ + public CharSequence getLabel() { + return mLabel; + } + + /** + * The icon resource ID for the icon to show. + * + * @return A resource ID. + * + * @hide + */ + @SystemApi @Deprecated + public int getIconResId() { + // Minimal compatibility shim for legacy apps' tests + return 0; + } + + /** + * @return An icon displayed in the in-call UI. + * + * @hide + */ + @SystemApi @Deprecated + public Drawable getIcon(Context context) { + return mIcon.loadDrawable(context); + } + + /** + * @return An icon depicting the status. + */ + public Icon getIcon() { + return mIcon; + } + + /** + * @return Extra data used to display status. + */ + public Bundle getExtras() { + return mExtras; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeCharSequence(mLabel); + out.writeParcelable(mIcon, 0); + out.writeParcelable(mExtras, 0); + } + + public static final Creator<StatusHints> CREATOR + = new Creator<StatusHints>() { + public StatusHints createFromParcel(Parcel in) { + return new StatusHints(in); + } + + public StatusHints[] newArray(int size) { + return new StatusHints[size]; + } + }; + + private StatusHints(Parcel in) { + mLabel = in.readCharSequence(); + mIcon = in.readParcelable(getClass().getClassLoader()); + mExtras = in.readParcelable(getClass().getClassLoader()); + } + + @Override + public boolean equals(Object other) { + if (other != null && other instanceof StatusHints) { + StatusHints otherHints = (StatusHints) other; + return Objects.equals(otherHints.getLabel(), getLabel()) && + Objects.equals(otherHints.getIcon(), getIcon()) && + Objects.equals(otherHints.getExtras(), getExtras()); + } + return false; + } + + @Override + public int hashCode() { + return Objects.hashCode(mLabel) + Objects.hashCode(mIcon) + Objects.hashCode(mExtras); + } +} diff --git a/android/telecom/TelecomAnalytics.java b/android/telecom/TelecomAnalytics.java new file mode 100644 index 00000000..6e0d02c1 --- /dev/null +++ b/android/telecom/TelecomAnalytics.java @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2016 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.SystemApi; +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.ArrayList; +import java.util.List; + +/** + * @hide + */ +@SystemApi +public final class TelecomAnalytics implements Parcelable { + public static final Parcelable.Creator<TelecomAnalytics> CREATOR = + new Parcelable.Creator<TelecomAnalytics> () { + + @Override + public TelecomAnalytics createFromParcel(Parcel in) { + return new TelecomAnalytics(in); + } + + @Override + public TelecomAnalytics[] newArray(int size) { + return new TelecomAnalytics[size]; + } + }; + + public static final class SessionTiming extends TimedEvent<Integer> implements Parcelable { + public static final Parcelable.Creator<SessionTiming> CREATOR = + new Parcelable.Creator<SessionTiming> () { + + @Override + public SessionTiming createFromParcel(Parcel in) { + return new SessionTiming(in); + } + + @Override + public SessionTiming[] newArray(int size) { + return new SessionTiming[size]; + } + }; + + public static final int ICA_ANSWER_CALL = 1; + public static final int ICA_REJECT_CALL = 2; + public static final int ICA_DISCONNECT_CALL = 3; + public static final int ICA_HOLD_CALL = 4; + public static final int ICA_UNHOLD_CALL = 5; + public static final int ICA_MUTE = 6; + public static final int ICA_SET_AUDIO_ROUTE = 7; + public static final int ICA_CONFERENCE = 8; + + public static final int CSW_HANDLE_CREATE_CONNECTION_COMPLETE = 100; + public static final int CSW_SET_ACTIVE = 101; + public static final int CSW_SET_RINGING = 102; + public static final int CSW_SET_DIALING = 103; + public static final int CSW_SET_DISCONNECTED = 104; + public static final int CSW_SET_ON_HOLD = 105; + public static final int CSW_REMOVE_CALL = 106; + public static final int CSW_SET_IS_CONFERENCED = 107; + public static final int CSW_ADD_CONFERENCE_CALL = 108; + + private int mId; + private long mTime; + + public SessionTiming(int id, long time) { + this.mId = id; + this.mTime = time; + } + + private SessionTiming(Parcel in) { + mId = in.readInt(); + mTime = in.readLong(); + } + + @Override + public Integer getKey() { + return mId; + } + + @Override + public long getTime() { + return mTime; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeInt(mId); + out.writeLong(mTime); + } + } + + private List<SessionTiming> mSessionTimings; + private List<ParcelableCallAnalytics> mCallAnalytics; + + public TelecomAnalytics(List<SessionTiming> sessionTimings, + List<ParcelableCallAnalytics> callAnalytics) { + this.mSessionTimings = sessionTimings; + this.mCallAnalytics = callAnalytics; + } + + private TelecomAnalytics(Parcel in) { + mSessionTimings = new ArrayList<>(); + in.readTypedList(mSessionTimings, SessionTiming.CREATOR); + mCallAnalytics = new ArrayList<>(); + in.readTypedList(mCallAnalytics, ParcelableCallAnalytics.CREATOR); + } + + public List<SessionTiming> getSessionTimings() { + return mSessionTimings; + } + + public List<ParcelableCallAnalytics> getCallAnalytics() { + return mCallAnalytics; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeTypedList(mSessionTimings); + out.writeTypedList(mCallAnalytics); + } +} diff --git a/android/telecom/TelecomManager.java b/android/telecom/TelecomManager.java new file mode 100644 index 00000000..9e52c71b --- /dev/null +++ b/android/telecom/TelecomManager.java @@ -0,0 +1,1768 @@ +/* + * Copyright (C) 2014 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.Manifest; +import android.annotation.RequiresPermission; +import android.annotation.SuppressAutoDoc; +import android.annotation.SuppressLint; +import android.annotation.SystemApi; +import android.annotation.SystemService; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.telephony.TelephonyManager; +import android.text.TextUtils; +import android.util.Log; + +import com.android.internal.telecom.ITelecomService; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Provides access to information about active calls and registration/call-management functionality. + * Apps can use methods in this class to determine the current call state. + * <p> + * Apps do not instantiate this class directly; instead, they retrieve a reference to an instance + * through {@link Context#getSystemService Context.getSystemService(Context.TELECOM_SERVICE)}. + * <p> + * Note that access to some telecom information is permission-protected. Your app cannot access the + * protected information or gain access to protected functionality unless it has the appropriate + * permissions declared in its manifest file. Where permissions apply, they are noted in the method + * descriptions. + */ +@SuppressAutoDoc +@SystemService(Context.TELECOM_SERVICE) +public class TelecomManager { + + /** + * Activity action: Starts the UI for handing an incoming call. This intent starts the in-call + * UI by notifying the Telecom system that an incoming call exists for a specific call service + * (see {@link android.telecom.ConnectionService}). Telecom reads the Intent extras to find + * and bind to the appropriate {@link android.telecom.ConnectionService} which Telecom will + * ultimately use to control and get information about the call. + * <p> + * Input: get*Extra field {@link #EXTRA_PHONE_ACCOUNT_HANDLE} contains the component name of the + * {@link android.telecom.ConnectionService} that Telecom should bind to. Telecom will then + * ask the connection service for more information about the call prior to showing any UI. + * + * @deprecated Use {@link #addNewIncomingCall} instead. + */ + public static final String ACTION_INCOMING_CALL = "android.telecom.action.INCOMING_CALL"; + + /** + * Similar to {@link #ACTION_INCOMING_CALL}, but is used only by Telephony to add a new + * sim-initiated MO call for carrier testing. + * @deprecated Use {@link #addNewUnknownCall} instead. + * @hide + */ + public static final String ACTION_NEW_UNKNOWN_CALL = "android.telecom.action.NEW_UNKNOWN_CALL"; + + /** + * An {@link android.content.Intent} action sent by the telecom framework to start a + * configuration dialog for a registered {@link PhoneAccount}. There is no default dialog + * and each app that registers a {@link PhoneAccount} should provide one if desired. + * <p> + * A user can access the list of enabled {@link android.telecom.PhoneAccount}s through the Phone + * app's settings menu. For each entry, the settings app will add a click action. When + * triggered, the click-action will start this intent along with the extra + * {@link #EXTRA_PHONE_ACCOUNT_HANDLE} to indicate the {@link PhoneAccount} to configure. If the + * {@link PhoneAccount} package does not register an {@link android.app.Activity} for this + * intent, then it will not be sent. + */ + public static final String ACTION_CONFIGURE_PHONE_ACCOUNT = + "android.telecom.action.CONFIGURE_PHONE_ACCOUNT"; + + /** + * The {@link android.content.Intent} action used to show the call accessibility settings page. + */ + public static final String ACTION_SHOW_CALL_ACCESSIBILITY_SETTINGS = + "android.telecom.action.SHOW_CALL_ACCESSIBILITY_SETTINGS"; + + /** + * The {@link android.content.Intent} action used to show the call settings page. + */ + public static final String ACTION_SHOW_CALL_SETTINGS = + "android.telecom.action.SHOW_CALL_SETTINGS"; + + /** + * The {@link android.content.Intent} action used to show the respond via SMS settings page. + */ + public static final String ACTION_SHOW_RESPOND_VIA_SMS_SETTINGS = + "android.telecom.action.SHOW_RESPOND_VIA_SMS_SETTINGS"; + + /** + * The {@link android.content.Intent} action used to show the settings page used to configure + * {@link PhoneAccount} preferences. + */ + public static final String ACTION_CHANGE_PHONE_ACCOUNTS = + "android.telecom.action.CHANGE_PHONE_ACCOUNTS"; + + /** + * {@link android.content.Intent} action used indicate that a new phone account was just + * registered. + * <p> + * The Intent {@link Intent#getExtras() extras} will contain {@link #EXTRA_PHONE_ACCOUNT_HANDLE} + * to indicate which {@link PhoneAccount} was registered. + * <p> + * Will only be sent to the default dialer app (see {@link #getDefaultDialerPackage()}). + */ + public static final String ACTION_PHONE_ACCOUNT_REGISTERED = + "android.telecom.action.PHONE_ACCOUNT_REGISTERED"; + + /** + * {@link android.content.Intent} action used indicate that a phone account was just + * unregistered. + * <p> + * The Intent {@link Intent#getExtras() extras} will contain {@link #EXTRA_PHONE_ACCOUNT_HANDLE} + * to indicate which {@link PhoneAccount} was unregistered. + * <p> + * Will only be sent to the default dialer app (see {@link #getDefaultDialerPackage()}). + */ + public static final String ACTION_PHONE_ACCOUNT_UNREGISTERED = + "android.telecom.action.PHONE_ACCOUNT_UNREGISTERED"; + + /** + * Activity action: Shows a dialog asking the user whether or not they want to replace the + * current default Dialer with the one specified in + * {@link #EXTRA_CHANGE_DEFAULT_DIALER_PACKAGE_NAME}. + * + * Usage example: + * <pre> + * Intent intent = new Intent(TelecomManager.ACTION_CHANGE_DEFAULT_DIALER); + * intent.putExtra(TelecomManager.EXTRA_CHANGE_DEFAULT_DIALER_PACKAGE_NAME, + * getActivity().getPackageName()); + * startActivity(intent); + * </pre> + */ + public static final String ACTION_CHANGE_DEFAULT_DIALER = + "android.telecom.action.CHANGE_DEFAULT_DIALER"; + + /** + * Broadcast intent action indicating that the current default dialer has changed. + * The string extra {@link #EXTRA_CHANGE_DEFAULT_DIALER_PACKAGE_NAME} will contain the + * name of the package that the default dialer was changed to. + * + * @see #EXTRA_CHANGE_DEFAULT_DIALER_PACKAGE_NAME + */ + public static final String ACTION_DEFAULT_DIALER_CHANGED = + "android.telecom.action.DEFAULT_DIALER_CHANGED"; + + /** + * Extra value used to provide the package name for {@link #ACTION_CHANGE_DEFAULT_DIALER}. + */ + public static final String EXTRA_CHANGE_DEFAULT_DIALER_PACKAGE_NAME = + "android.telecom.extra.CHANGE_DEFAULT_DIALER_PACKAGE_NAME"; + + /** + * Optional extra for {@link android.content.Intent#ACTION_CALL} containing a boolean that + * determines whether the speakerphone should be automatically turned on for an outgoing call. + */ + public static final String EXTRA_START_CALL_WITH_SPEAKERPHONE = + "android.telecom.extra.START_CALL_WITH_SPEAKERPHONE"; + + /** + * Optional extra for {@link android.content.Intent#ACTION_CALL} containing an integer that + * determines the desired video state for an outgoing call. + * Valid options: + * {@link VideoProfile#STATE_AUDIO_ONLY}, + * {@link VideoProfile#STATE_BIDIRECTIONAL}, + * {@link VideoProfile#STATE_RX_ENABLED}, + * {@link VideoProfile#STATE_TX_ENABLED}. + */ + public static final String EXTRA_START_CALL_WITH_VIDEO_STATE = + "android.telecom.extra.START_CALL_WITH_VIDEO_STATE"; + + /** + * Optional extra for {@link #addNewIncomingCall(PhoneAccountHandle, Bundle)} containing an + * integer that determines the requested video state for an incoming call. + * Valid options: + * {@link VideoProfile#STATE_AUDIO_ONLY}, + * {@link VideoProfile#STATE_BIDIRECTIONAL}, + * {@link VideoProfile#STATE_RX_ENABLED}, + * {@link VideoProfile#STATE_TX_ENABLED}. + */ + public static final String EXTRA_INCOMING_VIDEO_STATE = + "android.telecom.extra.INCOMING_VIDEO_STATE"; + + /** + * The extra used with an {@link android.content.Intent#ACTION_CALL} and + * {@link android.content.Intent#ACTION_DIAL} {@code Intent} to specify a + * {@link PhoneAccountHandle} to use when making the call. + * <p class="note"> + * Retrieve with {@link android.content.Intent#getParcelableExtra(String)}. + */ + public static final String EXTRA_PHONE_ACCOUNT_HANDLE = + "android.telecom.extra.PHONE_ACCOUNT_HANDLE"; + + /** + * Optional extra for {@link android.content.Intent#ACTION_CALL} containing a string call + * subject which will be associated with an outgoing call. Should only be specified if the + * {@link PhoneAccount} supports the capability {@link PhoneAccount#CAPABILITY_CALL_SUBJECT}. + */ + public static final String EXTRA_CALL_SUBJECT = "android.telecom.extra.CALL_SUBJECT"; + + /** + * The extra used by a {@link ConnectionService} to provide the handle of the caller that + * has initiated a new incoming call. + */ + public static final String EXTRA_INCOMING_CALL_ADDRESS = + "android.telecom.extra.INCOMING_CALL_ADDRESS"; + + /** + * Optional extra for {@link #ACTION_INCOMING_CALL} containing a {@link Bundle} which contains + * metadata about the call. This {@link Bundle} will be returned to the + * {@link ConnectionService}. + */ + public static final String EXTRA_INCOMING_CALL_EXTRAS = + "android.telecom.extra.INCOMING_CALL_EXTRAS"; + + /** + * Optional extra for {@link android.content.Intent#ACTION_CALL} and + * {@link android.content.Intent#ACTION_DIAL} {@code Intent} containing a {@link Bundle} + * which contains metadata about the call. This {@link Bundle} will be saved into + * {@code Call.Details} and passed to the {@link ConnectionService} when placing the call. + */ + public static final String EXTRA_OUTGOING_CALL_EXTRAS = + "android.telecom.extra.OUTGOING_CALL_EXTRAS"; + + /** + * @hide + */ + public static final String EXTRA_UNKNOWN_CALL_HANDLE = + "android.telecom.extra.UNKNOWN_CALL_HANDLE"; + + /** + * Optional extra for incoming and outgoing calls containing a long which specifies the time the + * call was created. This value is in milliseconds since boot. + * @hide + */ + public static final String EXTRA_CALL_CREATED_TIME_MILLIS = + "android.telecom.extra.CALL_CREATED_TIME_MILLIS"; + + /** + * Optional extra for incoming and outgoing calls containing a long which specifies the time + * telecom began routing the call. This value is in milliseconds since boot. + * @hide + */ + public static final String EXTRA_CALL_TELECOM_ROUTING_START_TIME_MILLIS = + "android.telecom.extra.CALL_TELECOM_ROUTING_START_TIME_MILLIS"; + + /** + * Optional extra for incoming and outgoing calls containing a long which specifies the time + * telecom finished routing the call. This value is in milliseconds since boot. + * @hide + */ + public static final String EXTRA_CALL_TELECOM_ROUTING_END_TIME_MILLIS = + "android.telecom.extra.CALL_TELECOM_ROUTING_END_TIME_MILLIS"; + + /** + * Optional extra for {@link android.telephony.TelephonyManager#ACTION_PHONE_STATE_CHANGED} + * containing the disconnect code. + */ + public static final String EXTRA_CALL_DISCONNECT_CAUSE = + "android.telecom.extra.CALL_DISCONNECT_CAUSE"; + + /** + * Optional extra for {@link android.telephony.TelephonyManager#ACTION_PHONE_STATE_CHANGED} + * containing the disconnect message. + */ + public static final String EXTRA_CALL_DISCONNECT_MESSAGE = + "android.telecom.extra.CALL_DISCONNECT_MESSAGE"; + + /** + * Optional extra for {@link android.telephony.TelephonyManager#ACTION_PHONE_STATE_CHANGED} + * containing the component name of the associated connection service. + * @hide + */ + @SystemApi + public static final String EXTRA_CONNECTION_SERVICE = + "android.telecom.extra.CONNECTION_SERVICE"; + + /** + * Optional extra for communicating the call technology used by a + * {@link com.android.internal.telephony.Connection} to Telecom + * @hide + */ + public static final String EXTRA_CALL_TECHNOLOGY_TYPE = + "android.telecom.extra.CALL_TECHNOLOGY_TYPE"; + + /** + * An optional {@link android.content.Intent#ACTION_CALL} intent extra denoting the + * package name of the app specifying an alternative gateway for the call. + * The value is a string. + * + * (The following comment corresponds to the all GATEWAY_* extras) + * An app which sends the {@link android.content.Intent#ACTION_CALL} intent can specify an + * alternative address to dial which is different from the one specified and displayed to + * the user. This alternative address is referred to as the gateway address. + */ + public static final String GATEWAY_PROVIDER_PACKAGE = + "android.telecom.extra.GATEWAY_PROVIDER_PACKAGE"; + + /** + * An optional {@link android.content.Intent#ACTION_CALL} intent extra corresponding to the + * original address to dial for the call. This is used when an alternative gateway address is + * provided to recall the original address. + * The value is a {@link android.net.Uri}. + * + * (See {@link #GATEWAY_PROVIDER_PACKAGE} for details) + */ + public static final String GATEWAY_ORIGINAL_ADDRESS = + "android.telecom.extra.GATEWAY_ORIGINAL_ADDRESS"; + + /** + * The number which the party on the other side of the line will see (and use to return the + * call). + * <p> + * {@link ConnectionService}s which interact with {@link RemoteConnection}s should only populate + * this if the {@link android.telephony.TelephonyManager#getLine1Number()} value, as that is the + * user's expected caller ID. + */ + public static final String EXTRA_CALL_BACK_NUMBER = "android.telecom.extra.CALL_BACK_NUMBER"; + + /** + * The number of milliseconds that Telecom should wait after disconnecting a call via the + * ACTION_NEW_OUTGOING_CALL broadcast, in order to wait for the app which cancelled the call + * to make a new one. + * @hide + */ + public static final String EXTRA_NEW_OUTGOING_CALL_CANCEL_TIMEOUT = + "android.telecom.extra.NEW_OUTGOING_CALL_CANCEL_TIMEOUT"; + + /** + * Boolean extra specified to indicate that the intention of adding a call is to handover an + * existing call from the user's device to a different {@link PhoneAccount}. + * <p> + * Used when calling {@link #addNewIncomingCall(PhoneAccountHandle, Bundle)} + * to indicate to Telecom that the purpose of adding a new incoming call is to handover an + * existing call from the user's device to a different {@link PhoneAccount}. This occurs on + * the receiving side of a handover. + * <p> + * Used when Telecom calls + * {@link ConnectionService#onCreateOutgoingConnection(PhoneAccountHandle, ConnectionRequest)} + * to indicate that the purpose of Telecom requesting a new outgoing connection it to request + * a handover to this {@link ConnectionService} from an ongoing call on the user's device. This + * occurs on the initiating side of a handover. + * <p> + * The phone number of the call used by Telecom to determine which call should be handed over. + * @hide + */ + public static final String EXTRA_IS_HANDOVER = "android.telecom.extra.IS_HANDOVER"; + + /** + * Parcelable extra used with {@link #EXTRA_IS_HANDOVER} to indicate the source + * {@link PhoneAccountHandle} when initiating a handover which {@link ConnectionService} + * the handover is from. + * @hide + */ + public static final String EXTRA_HANDOVER_FROM_PHONE_ACCOUNT = + "android.telecom.extra.HANDOVER_FROM_PHONE_ACCOUNT"; + + /** + * Extra key specified in the {@link ConnectionRequest#getExtras()} when Telecom calls + * {@link ConnectionService#onCreateOutgoingConnection(PhoneAccountHandle, ConnectionRequest)} + * to inform the {@link ConnectionService} what the initial {@link CallAudioState} of the + * {@link Connection} will be. + * @hide + */ + public static final String EXTRA_CALL_AUDIO_STATE = "android.telecom.extra.CALL_AUDIO_STATE"; + + /** + * A boolean extra, which when set on the {@link Intent#ACTION_CALL} intent or on the bundle + * passed into {@link #placeCall(Uri, Bundle)}, indicates that the call should be initiated with + * an RTT session open. See {@link android.telecom.Call.RttCall} for more information on RTT. + */ + public static final String EXTRA_START_CALL_WITH_RTT = + "android.telecom.extra.START_CALL_WITH_RTT"; + + /** + * A boolean meta-data value indicating whether an {@link InCallService} implements an + * in-call user interface. Dialer implementations (see {@link #getDefaultDialerPackage()}) which + * would also like to replace the in-call interface should set this meta-data to {@code true} in + * the manifest registration of their {@link InCallService}. + */ + public static final String METADATA_IN_CALL_SERVICE_UI = "android.telecom.IN_CALL_SERVICE_UI"; + + /** + * A boolean meta-data value indicating whether an {@link InCallService} implements an + * in-call user interface to be used while the device is in car-mode (see + * {@link android.content.res.Configuration.UI_MODE_TYPE_CAR}). + * + * @hide + */ + public static final String METADATA_IN_CALL_SERVICE_CAR_MODE_UI = + "android.telecom.IN_CALL_SERVICE_CAR_MODE_UI"; + + /** + * A boolean meta-data value indicating whether an {@link InCallService} implements ringing. + * Dialer implementations (see {@link #getDefaultDialerPackage()}) which would also like to + * override the system provided ringing should set this meta-data to {@code true} in the + * manifest registration of their {@link InCallService}. + */ + public static final String METADATA_IN_CALL_SERVICE_RINGING = + "android.telecom.IN_CALL_SERVICE_RINGING"; + + /** + * A boolean meta-data value indicating whether an {@link InCallService} wants to be informed of + * calls which have the {@link Call.Details#PROPERTY_IS_EXTERNAL_CALL} property. An external + * call is one which a {@link ConnectionService} knows about, but is not connected to directly. + * Dialer implementations (see {@link #getDefaultDialerPackage()}) which would like to be + * informed of external calls should set this meta-data to {@code true} in the manifest + * registration of their {@link InCallService}. By default, the {@link InCallService} will NOT + * be informed of external calls. + */ + public static final String METADATA_INCLUDE_EXTERNAL_CALLS = + "android.telecom.INCLUDE_EXTERNAL_CALLS"; + + /** + * A boolean meta-data value indicating whether an {@link InCallService} wants to be informed of + * calls which have the {@link Call.Details#PROPERTY_SELF_MANAGED} property. A self-managed + * call is one which originates from a self-managed {@link ConnectionService} which has chosen + * to implement its own call user interface. An {@link InCallService} implementation which + * would like to be informed of external calls should set this meta-data to {@code true} in the + * manifest registration of their {@link InCallService}. By default, the {@link InCallService} + * will NOT be informed about self-managed calls. + * <p> + * An {@link InCallService} which receives self-managed calls is free to view and control the + * 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 = + "android.telecom.INCLUDE_SELF_MANAGED_CALLS"; + + /** + * The dual tone multi-frequency signaling character sent to indicate the dialing system should + * pause for a predefined period. + */ + public static final char DTMF_CHARACTER_PAUSE = ','; + + /** + * The dual-tone multi-frequency signaling character sent to indicate the dialing system should + * wait for user confirmation before proceeding. + */ + public static final char DTMF_CHARACTER_WAIT = ';'; + + /** + * TTY (teletypewriter) mode is off. + * + * @hide + */ + public static final int TTY_MODE_OFF = 0; + + /** + * TTY (teletypewriter) mode is on. The speaker is off and the microphone is muted. The user + * will communicate with the remote party by sending and receiving text messages. + * + * @hide + */ + public static final int TTY_MODE_FULL = 1; + + /** + * TTY (teletypewriter) mode is in hearing carryover mode (HCO). The microphone is muted but the + * speaker is on. The user will communicate with the remote party by sending text messages and + * hearing an audible reply. + * + * @hide + */ + public static final int TTY_MODE_HCO = 2; + + /** + * TTY (teletypewriter) mode is in voice carryover mode (VCO). The speaker is off but the + * microphone is still on. User will communicate with the remote party by speaking and receiving + * text message replies. + * + * @hide + */ + public static final int TTY_MODE_VCO = 3; + + /** + * Broadcast intent action indicating that the current TTY mode has changed. An intent extra + * provides this state as an int. + * + * @see #EXTRA_CURRENT_TTY_MODE + * @hide + */ + public static final String ACTION_CURRENT_TTY_MODE_CHANGED = + "android.telecom.action.CURRENT_TTY_MODE_CHANGED"; + + /** + * The lookup key for an int that indicates the current TTY mode. + * Valid modes are: + * - {@link #TTY_MODE_OFF} + * - {@link #TTY_MODE_FULL} + * - {@link #TTY_MODE_HCO} + * - {@link #TTY_MODE_VCO} + * + * @hide + */ + public static final String EXTRA_CURRENT_TTY_MODE = + "android.telecom.intent.extra.CURRENT_TTY_MODE"; + + /** + * Broadcast intent action indicating that the TTY preferred operating mode has changed. An + * intent extra provides the new mode as an int. + * + * @see #EXTRA_TTY_PREFERRED_MODE + * @hide + */ + public static final String ACTION_TTY_PREFERRED_MODE_CHANGED = + "android.telecom.action.TTY_PREFERRED_MODE_CHANGED"; + + /** + * The lookup key for an int that indicates preferred TTY mode. Valid modes are: - + * {@link #TTY_MODE_OFF} - {@link #TTY_MODE_FULL} - {@link #TTY_MODE_HCO} - + * {@link #TTY_MODE_VCO} + * + * @hide + */ + public static final String EXTRA_TTY_PREFERRED_MODE = + "android.telecom.intent.extra.TTY_PREFERRED"; + + /** + * Broadcast intent action for letting custom component know to show the missed call + * notification. If no custom component exists then this is sent to the default dialer which + * should post a missed-call notification. + */ + public static final String ACTION_SHOW_MISSED_CALLS_NOTIFICATION = + "android.telecom.action.SHOW_MISSED_CALLS_NOTIFICATION"; + + /** + * The number of calls associated with the notification. If the number is zero then the missed + * call notification should be dismissed. + */ + public static final String EXTRA_NOTIFICATION_COUNT = + "android.telecom.extra.NOTIFICATION_COUNT"; + + /** + * The number associated with the missed calls. This number is only relevant + * when EXTRA_NOTIFICATION_COUNT is 1. + */ + public static final String EXTRA_NOTIFICATION_PHONE_NUMBER = + "android.telecom.extra.NOTIFICATION_PHONE_NUMBER"; + + /** + * The intent to clear missed calls. + * @hide + */ + @SystemApi + public static final String EXTRA_CLEAR_MISSED_CALLS_INTENT = + "android.telecom.extra.CLEAR_MISSED_CALLS_INTENT"; + + /** + * The intent to call back a missed call. + * @hide + */ + @SystemApi + public static final String EXTRA_CALL_BACK_INTENT = + "android.telecom.extra.CALL_BACK_INTENT"; + + /** + * The following 4 constants define how properties such as phone numbers and names are + * displayed to the user. + */ + + /** + * Indicates that the address or number of a call is allowed to be displayed for caller ID. + */ + public static final int PRESENTATION_ALLOWED = 1; + + /** + * Indicates that the address or number of a call is blocked by the other party. + */ + public static final int PRESENTATION_RESTRICTED = 2; + + /** + * Indicates that the address or number of a call is not specified or known by the carrier. + */ + public static final int PRESENTATION_UNKNOWN = 3; + + /** + * Indicates that the address or number of a call belongs to a pay phone. + */ + public static final int PRESENTATION_PAYPHONE = 4; + + private static final String TAG = "TelecomManager"; + + private final Context mContext; + + private final ITelecomService mTelecomServiceOverride; + + /** + * @hide + */ + public static TelecomManager from(Context context) { + return (TelecomManager) context.getSystemService(Context.TELECOM_SERVICE); + } + + /** + * @hide + */ + public TelecomManager(Context context) { + this(context, null); + } + + /** + * @hide + */ + public TelecomManager(Context context, ITelecomService telecomServiceImpl) { + Context appContext = context.getApplicationContext(); + if (appContext != null) { + mContext = appContext; + } else { + mContext = context; + } + mTelecomServiceOverride = telecomServiceImpl; + android.telecom.Log.initMd5Sum(); + } + + /** + * Return the {@link PhoneAccount} which will be used to place outgoing calls to addresses with + * the specified {@code uriScheme}. This {@link PhoneAccount} will always be a member of the + * list which is returned from invoking {@link #getCallCapablePhoneAccounts()}. The specific + * account returned depends on the following priorities: + * <ul> + * <li> If the user-selected default {@link PhoneAccount} supports the specified scheme, it will + * be returned. + * </li> + * <li> If there exists only one {@link PhoneAccount} that supports the specified scheme, it + * will be returned. + * </li> + * </ul> + * <p> + * If no {@link PhoneAccount} fits the criteria above, this method will return {@code null}. + * + * Requires permission: {@link android.Manifest.permission#READ_PHONE_STATE} + * + * @param uriScheme The URI scheme. + * @return The {@link PhoneAccountHandle} corresponding to the account to be used. + */ + @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) + public PhoneAccountHandle getDefaultOutgoingPhoneAccount(String uriScheme) { + try { + if (isServiceConnected()) { + return getTelecomService().getDefaultOutgoingPhoneAccount(uriScheme, + mContext.getOpPackageName()); + } + } catch (RemoteException e) { + Log.e(TAG, "Error calling ITelecomService#getDefaultOutgoingPhoneAccount", e); + } + return null; + } + + /** + * Return the {@link PhoneAccount} which is the user-chosen default for making outgoing phone + * calls. This {@code PhoneAccount} will always be a member of the list which is returned from + * calling {@link #getCallCapablePhoneAccounts()} + * <p> + * Apps must be prepared for this method to return {@code null}, indicating that there currently + * exists no user-chosen default {@code PhoneAccount}. + * + * @return The user outgoing phone account selected by the user. + * @hide + */ + public PhoneAccountHandle getUserSelectedOutgoingPhoneAccount() { + try { + if (isServiceConnected()) { + return getTelecomService().getUserSelectedOutgoingPhoneAccount(); + } + } catch (RemoteException e) { + Log.e(TAG, "Error calling ITelecomService#getUserSelectedOutgoingPhoneAccount", e); + } + return null; + } + + /** + * Sets the user-chosen default for making outgoing phone calls. + * @hide + */ + public void setUserSelectedOutgoingPhoneAccount(PhoneAccountHandle accountHandle) { + try { + if (isServiceConnected()) { + getTelecomService().setUserSelectedOutgoingPhoneAccount(accountHandle); + } + } catch (RemoteException e) { + Log.e(TAG, "Error calling ITelecomService#setUserSelectedOutgoingPhoneAccount"); + } + } + + /** + * Returns the current SIM call manager. Apps must be prepared for this method to return + * {@code null}, indicating that there currently exists no user-chosen default + * {@code PhoneAccount}. + * + * @return The phone account handle of the current sim call manager. + */ + public PhoneAccountHandle getSimCallManager() { + try { + if (isServiceConnected()) { + return getTelecomService().getSimCallManager(); + } + } catch (RemoteException e) { + Log.e(TAG, "Error calling ITelecomService#getSimCallManager"); + } + return null; + } + + /** + * Returns the current SIM call manager for the specified user. Apps must be prepared for this + * method to return {@code null}, indicating that there currently exists no user-chosen default + * {@code PhoneAccount}. + * + * @return The phone account handle of the current sim call manager. + * + * @hide + */ + public PhoneAccountHandle getSimCallManager(int userId) { + try { + if (isServiceConnected()) { + return getTelecomService().getSimCallManagerForUser(userId); + } + } catch (RemoteException e) { + Log.e(TAG, "Error calling ITelecomService#getSimCallManagerForUser"); + } + return null; + } + + /** + * Returns the current connection manager. Apps must be prepared for this method to return + * {@code null}, indicating that there currently exists no user-chosen default + * {@code PhoneAccount}. + * + * @return The phone account handle of the current connection manager. + * @hide + */ + @SystemApi + public PhoneAccountHandle getConnectionManager() { + return getSimCallManager(); + } + + /** + * Returns a list of the {@link PhoneAccountHandle}s which can be used to make and receive phone + * calls which support the specified URI scheme. + * <P> + * For example, invoking with {@code "tel"} will find all {@link PhoneAccountHandle}s which + * support telephone calls (e.g. URIs such as {@code tel:555-555-1212}). Invoking with + * {@code "sip"} will find all {@link PhoneAccountHandle}s which support SIP calls (e.g. URIs + * such as {@code sip:example@sipexample.com}). + * + * @param uriScheme The URI scheme. + * @return A list of {@code PhoneAccountHandle} objects supporting the URI scheme. + * @hide + */ + @SystemApi + @RequiresPermission(anyOf = { + android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE, + android.Manifest.permission.READ_PHONE_STATE + }) + public List<PhoneAccountHandle> getPhoneAccountsSupportingScheme(String uriScheme) { + try { + if (isServiceConnected()) { + return getTelecomService().getPhoneAccountsSupportingScheme(uriScheme, + mContext.getOpPackageName()); + } + } catch (RemoteException e) { + Log.e(TAG, "Error calling ITelecomService#getPhoneAccountsSupportingScheme", e); + } + return new ArrayList<>(); + } + + + /** + * Returns a list of {@link PhoneAccountHandle}s which can be used to make and receive phone + * calls. The returned list includes only those accounts which have been explicitly enabled + * by the user. + * + * Requires permission: {@link android.Manifest.permission#READ_PHONE_STATE} + * + * @see #EXTRA_PHONE_ACCOUNT_HANDLE + * @return A list of {@code PhoneAccountHandle} objects. + */ + @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) + public List<PhoneAccountHandle> getCallCapablePhoneAccounts() { + return getCallCapablePhoneAccounts(false); + } + + /** + * Returns a list of {@link PhoneAccountHandle}s for self-managed {@link ConnectionService}s. + * <p> + * Self-Managed {@link ConnectionService}s have a {@link PhoneAccount} with + * {@link PhoneAccount#CAPABILITY_SELF_MANAGED}. + * <p> + * Requires permission {@link android.Manifest.permission#READ_PHONE_STATE}, or that the caller + * is the default dialer app. + * <p> + * A {@link SecurityException} will be thrown if a called is not the default dialer, or lacks + * the {@link android.Manifest.permission#READ_PHONE_STATE} permission. + * + * @return A list of {@code PhoneAccountHandle} objects. + */ + @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) + public List<PhoneAccountHandle> getSelfManagedPhoneAccounts() { + try { + if (isServiceConnected()) { + return getTelecomService().getSelfManagedPhoneAccounts(mContext.getOpPackageName()); + } + } catch (RemoteException e) { + Log.e(TAG, "Error calling ITelecomService#getSelfManagedPhoneAccounts()", e); + } + return new ArrayList<>(); + } + + /** + * Returns a list of {@link PhoneAccountHandle}s including those which have not been enabled + * by the user. + * + * @return A list of {@code PhoneAccountHandle} objects. + * @hide + */ + public List<PhoneAccountHandle> getCallCapablePhoneAccounts(boolean includeDisabledAccounts) { + try { + if (isServiceConnected()) { + return getTelecomService().getCallCapablePhoneAccounts( + includeDisabledAccounts, mContext.getOpPackageName()); + } + } catch (RemoteException e) { + Log.e(TAG, "Error calling ITelecomService#getCallCapablePhoneAccounts(" + + includeDisabledAccounts + ")", e); + } + return new ArrayList<>(); + } + + /** + * Returns a list of all {@link PhoneAccount}s registered for the calling package. + * + * @return A list of {@code PhoneAccountHandle} objects. + * @hide + */ + @SystemApi + @SuppressLint("Doclava125") + public List<PhoneAccountHandle> getPhoneAccountsForPackage() { + try { + if (isServiceConnected()) { + return getTelecomService().getPhoneAccountsForPackage(mContext.getPackageName()); + } + } catch (RemoteException e) { + Log.e(TAG, "Error calling ITelecomService#getPhoneAccountsForPackage", e); + } + return null; + } + + /** + * Return the {@link PhoneAccount} for a specified {@link PhoneAccountHandle}. Object includes + * resources which can be used in a user interface. + * + * @param account The {@link PhoneAccountHandle}. + * @return The {@link PhoneAccount} object. + */ + public PhoneAccount getPhoneAccount(PhoneAccountHandle account) { + try { + if (isServiceConnected()) { + return getTelecomService().getPhoneAccount(account); + } + } catch (RemoteException e) { + Log.e(TAG, "Error calling ITelecomService#getPhoneAccount", e); + } + return null; + } + + /** + * Returns a count of all {@link PhoneAccount}s. + * + * @return The count of {@link PhoneAccount}s. + * @hide + */ + @SystemApi + public int getAllPhoneAccountsCount() { + try { + if (isServiceConnected()) { + return getTelecomService().getAllPhoneAccountsCount(); + } + } catch (RemoteException e) { + Log.e(TAG, "Error calling ITelecomService#getAllPhoneAccountsCount", e); + } + return 0; + } + + /** + * Returns a list of all {@link PhoneAccount}s. + * + * @return All {@link PhoneAccount}s. + * @hide + */ + @SystemApi + public List<PhoneAccount> getAllPhoneAccounts() { + try { + if (isServiceConnected()) { + return getTelecomService().getAllPhoneAccounts(); + } + } catch (RemoteException e) { + Log.e(TAG, "Error calling ITelecomService#getAllPhoneAccounts", e); + } + return Collections.EMPTY_LIST; + } + + /** + * Returns a list of all {@link PhoneAccountHandle}s. + * + * @return All {@link PhoneAccountHandle}s. + * @hide + */ + @SystemApi + public List<PhoneAccountHandle> getAllPhoneAccountHandles() { + try { + if (isServiceConnected()) { + return getTelecomService().getAllPhoneAccountHandles(); + } + } catch (RemoteException e) { + Log.e(TAG, "Error calling ITelecomService#getAllPhoneAccountHandles", e); + } + return Collections.EMPTY_LIST; + } + + /** + * Register a {@link PhoneAccount} for use by the system that will be stored in Device Encrypted + * storage. When registering {@link PhoneAccount}s, existing registrations will be overwritten + * if the {@link PhoneAccountHandle} matches that of a {@link PhoneAccount} which is already + * registered. Once registered, the {@link PhoneAccount} is listed to the user as an option + * when placing calls. The user may still need to enable the {@link PhoneAccount} within + * the phone app settings before the account is usable. + * <p> + * A {@link SecurityException} will be thrown if an app tries to register a + * {@link PhoneAccountHandle} where the package name specified within + * {@link PhoneAccountHandle#getComponentName()} does not match the package name of the app. + * + * @param account The complete {@link PhoneAccount}. + */ + public void registerPhoneAccount(PhoneAccount account) { + try { + if (isServiceConnected()) { + getTelecomService().registerPhoneAccount(account); + } + } catch (RemoteException e) { + Log.e(TAG, "Error calling ITelecomService#registerPhoneAccount", e); + } + } + + /** + * Remove a {@link PhoneAccount} registration from the system. + * + * @param accountHandle A {@link PhoneAccountHandle} for the {@link PhoneAccount} to unregister. + */ + public void unregisterPhoneAccount(PhoneAccountHandle accountHandle) { + try { + if (isServiceConnected()) { + getTelecomService().unregisterPhoneAccount(accountHandle); + } + } catch (RemoteException e) { + Log.e(TAG, "Error calling ITelecomService#unregisterPhoneAccount", e); + } + } + + /** + * Remove all Accounts that belong to the calling package from the system. + * @hide + */ + @SystemApi + @SuppressLint("Doclava125") + public void clearPhoneAccounts() { + clearAccounts(); + } + /** + * Remove all Accounts that belong to the calling package from the system. + * @deprecated Use {@link #clearPhoneAccounts()} instead. + * @hide + */ + @SystemApi + @SuppressLint("Doclava125") + public void clearAccounts() { + try { + if (isServiceConnected()) { + getTelecomService().clearAccounts(mContext.getPackageName()); + } + } catch (RemoteException e) { + Log.e(TAG, "Error calling ITelecomService#clearAccounts", e); + } + } + + /** + * Remove all Accounts that belong to the specified package from the system. + * @hide + */ + public void clearAccountsForPackage(String packageName) { + try { + if (isServiceConnected() && !TextUtils.isEmpty(packageName)) { + getTelecomService().clearAccounts(packageName); + } + } catch (RemoteException e) { + Log.e(TAG, "Error calling ITelecomService#clearAccountsForPackage", e); + } + } + + + /** + * @deprecated - Use {@link TelecomManager#getDefaultDialerPackage} to directly access + * the default dialer's package name instead. + * @hide + */ + @SystemApi + @SuppressLint("Doclava125") + public ComponentName getDefaultPhoneApp() { + try { + if (isServiceConnected()) { + return getTelecomService().getDefaultPhoneApp(); + } + } catch (RemoteException e) { + Log.e(TAG, "RemoteException attempting to get the default phone app.", e); + } + return null; + } + + /** + * Used to determine the currently selected default dialer package. + * + * @return package name for the default dialer package or null if no package has been + * selected as the default dialer. + */ + public String getDefaultDialerPackage() { + try { + if (isServiceConnected()) { + return getTelecomService().getDefaultDialerPackage(); + } + } catch (RemoteException e) { + Log.e(TAG, "RemoteException attempting to get the default dialer package name.", e); + } + return null; + } + + /** + * Used to set the default dialer package. + * + * @param packageName to set the default dialer to.. + * + * @result {@code true} if the default dialer was successfully changed, {@code false} if + * the specified package does not correspond to an installed dialer, or is already + * the default dialer. + * + * Requires permission: {@link android.Manifest.permission#MODIFY_PHONE_STATE} + * Requires permission: {@link android.Manifest.permission#WRITE_SECURE_SETTINGS} + * + * @hide + */ + public boolean setDefaultDialer(String packageName) { + try { + if (isServiceConnected()) { + return getTelecomService().setDefaultDialer(packageName); + } + } catch (RemoteException e) { + Log.e(TAG, "RemoteException attempting to set the default dialer.", e); + } + return false; + } + + /** + * Used to determine the dialer package that is preloaded on the system partition. + * + * @return package name for the system dialer package or null if no system dialer is preloaded. + * @hide + */ + public String getSystemDialerPackage() { + try { + if (isServiceConnected()) { + return getTelecomService().getSystemDialerPackage(); + } + } catch (RemoteException e) { + Log.e(TAG, "RemoteException attempting to get the system dialer package name.", e); + } + return null; + } + + /** + * Return whether a given phone number is the configured voicemail number for a + * particular phone account. + * + * Requires permission: {@link android.Manifest.permission#READ_PHONE_STATE} + * + * @param accountHandle The handle for the account to check the voicemail number against + * @param number The number to look up. + */ + @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) + public boolean isVoiceMailNumber(PhoneAccountHandle accountHandle, String number) { + try { + if (isServiceConnected()) { + return getTelecomService().isVoiceMailNumber(accountHandle, number, + mContext.getOpPackageName()); + } + } catch (RemoteException e) { + Log.e(TAG, "RemoteException calling ITelecomService#isVoiceMailNumber.", e); + } + return false; + } + + /** + * Return the voicemail number for a given phone account. + * + * Requires permission: {@link android.Manifest.permission#READ_PHONE_STATE} + * + * @param accountHandle The handle for the phone account. + * @return The voicemail number for the phone account, and {@code null} if one has not been + * configured. + */ + @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) + public String getVoiceMailNumber(PhoneAccountHandle accountHandle) { + try { + if (isServiceConnected()) { + return getTelecomService().getVoiceMailNumber(accountHandle, + mContext.getOpPackageName()); + } + } catch (RemoteException e) { + Log.e(TAG, "RemoteException calling ITelecomService#hasVoiceMailNumber.", e); + } + return null; + } + + /** + * Return the line 1 phone number for given phone account. + * + * Requires permission: {@link android.Manifest.permission#READ_PHONE_STATE} + * + * @param accountHandle The handle for the account retrieve a number for. + * @return A string representation of the line 1 phone number. + */ + @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) + public String getLine1Number(PhoneAccountHandle accountHandle) { + try { + if (isServiceConnected()) { + return getTelecomService().getLine1Number(accountHandle, + mContext.getOpPackageName()); + } + } catch (RemoteException e) { + Log.e(TAG, "RemoteException calling ITelecomService#getLine1Number.", e); + } + return null; + } + + /** + * Returns whether there is an ongoing phone call (can be in dialing, ringing, active or holding + * states) originating from either a manager or self-managed {@link ConnectionService}. + * <p> + * Requires permission: {@link android.Manifest.permission#READ_PHONE_STATE} + * + * @return {@code true} if there is an ongoing call in either a managed or self-managed + * {@link ConnectionService}, {@code false} otherwise. + */ + @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) + public boolean isInCall() { + try { + if (isServiceConnected()) { + return getTelecomService().isInCall(mContext.getOpPackageName()); + } + } catch (RemoteException e) { + Log.e(TAG, "RemoteException calling isInCall().", e); + } + return false; + } + + /** + * Returns whether there is an ongoing call originating from a managed + * {@link ConnectionService}. An ongoing call can be in dialing, ringing, active or holding + * states. + * <p> + * If you also need to know if there are ongoing self-managed calls, use {@link #isInCall()} + * instead. + * <p> + * Requires permission: {@link android.Manifest.permission#READ_PHONE_STATE} + * + * @return {@code true} if there is an ongoing call in a managed {@link ConnectionService}, + * {@code false} otherwise. + */ + @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) + public boolean isInManagedCall() { + try { + if (isServiceConnected()) { + return getTelecomService().isInManagedCall(mContext.getOpPackageName()); + } + } catch (RemoteException e) { + Log.e(TAG, "RemoteException calling isInManagedCall().", e); + } + return false; + } + + /** + * Returns one of the following constants that represents the current state of Telecom: + * + * {@link TelephonyManager#CALL_STATE_RINGING} + * {@link TelephonyManager#CALL_STATE_OFFHOOK} + * {@link TelephonyManager#CALL_STATE_IDLE} + * + * Note that this API does not require the + * {@link android.Manifest.permission#READ_PHONE_STATE} permission. This is intentional, to + * preserve the behavior of {@link TelephonyManager#getCallState()}, which also did not require + * the permission. + * + * Takes into consideration both managed and self-managed calls. + * + * @hide + */ + @SystemApi + public int getCallState() { + try { + if (isServiceConnected()) { + return getTelecomService().getCallState(); + } + } catch (RemoteException e) { + Log.d(TAG, "RemoteException calling getCallState().", e); + } + return TelephonyManager.CALL_STATE_IDLE; + } + + /** + * Returns whether there currently exists is a ringing incoming-call. + * + * @return {@code true} if there is a managed or self-managed ringing call. + * @hide + */ + @SystemApi + @RequiresPermission(anyOf = { + android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE, + android.Manifest.permission.READ_PHONE_STATE + }) + public boolean isRinging() { + try { + if (isServiceConnected()) { + return getTelecomService().isRinging(mContext.getOpPackageName()); + } + } catch (RemoteException e) { + Log.e(TAG, "RemoteException attempting to get ringing state of phone app.", e); + } + return false; + } + + /** + * Ends an ongoing call. + * TODO: L-release - need to convert all invocations of ITelecomService#endCall to use this + * method (clockwork & gearhead). + * @hide + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) + public boolean endCall() { + try { + if (isServiceConnected()) { + return getTelecomService().endCall(); + } + } catch (RemoteException e) { + Log.e(TAG, "Error calling ITelecomService#endCall", e); + } + return false; + } + + /** + * If there is a ringing incoming call, this method accepts the call on behalf of the user. + * + * If the incoming call is a video call, the call will be answered with the same video state as + * the incoming call requests. This means, for example, that an incoming call requesting + * {@link VideoProfile#STATE_BIDIRECTIONAL} will be answered, accepting that state. + * + * Requires permission: {@link android.Manifest.permission#MODIFY_PHONE_STATE} or + * {@link android.Manifest.permission#ANSWER_PHONE_CALLS} + */ + //TODO: L-release - need to convert all invocation of ITelecmmService#answerRingingCall to use + // this method (clockwork & gearhead). + @RequiresPermission(anyOf = + {Manifest.permission.ANSWER_PHONE_CALLS, Manifest.permission.MODIFY_PHONE_STATE}) + public void acceptRingingCall() { + try { + if (isServiceConnected()) { + getTelecomService().acceptRingingCall(mContext.getPackageName()); + } + } catch (RemoteException e) { + Log.e(TAG, "Error calling ITelecomService#acceptRingingCall", e); + } + } + + /** + * If there is a ringing incoming call, this method accepts the call on behalf of the user, + * with the specified video state. + * + * Requires permission: {@link android.Manifest.permission#MODIFY_PHONE_STATE} or + * {@link android.Manifest.permission#ANSWER_PHONE_CALLS} + * + * @param videoState The desired video state to answer the call with. + */ + @RequiresPermission(anyOf = + {Manifest.permission.ANSWER_PHONE_CALLS, Manifest.permission.MODIFY_PHONE_STATE}) + public void acceptRingingCall(int videoState) { + try { + if (isServiceConnected()) { + getTelecomService().acceptRingingCallWithVideoState( + mContext.getPackageName(), videoState); + } + } catch (RemoteException e) { + Log.e(TAG, "Error calling ITelecomService#acceptRingingCallWithVideoState", e); + } + } + + /** + * Silences the ringer if a ringing call exists. + * + * Requires permission: {@link android.Manifest.permission#MODIFY_PHONE_STATE} + */ + @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) + public void silenceRinger() { + try { + if (isServiceConnected()) { + getTelecomService().silenceRinger(mContext.getOpPackageName()); + } + } catch (RemoteException e) { + Log.e(TAG, "Error calling ITelecomService#silenceRinger", e); + } + } + + /** + * Returns whether TTY is supported on this device. + */ + + @RequiresPermission(anyOf = { + android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE, + android.Manifest.permission.READ_PHONE_STATE + }) + public boolean isTtySupported() { + try { + if (isServiceConnected()) { + return getTelecomService().isTtySupported(mContext.getOpPackageName()); + } + } catch (RemoteException e) { + Log.e(TAG, "RemoteException attempting to get TTY supported state.", e); + } + return false; + } + + /** + * Returns the current TTY mode of the device. For TTY to be on the user must enable it in + * settings and have a wired headset plugged in. + * Valid modes are: + * - {@link TelecomManager#TTY_MODE_OFF} + * - {@link TelecomManager#TTY_MODE_FULL} + * - {@link TelecomManager#TTY_MODE_HCO} + * - {@link TelecomManager#TTY_MODE_VCO} + * @hide + */ + public int getCurrentTtyMode() { + try { + if (isServiceConnected()) { + return getTelecomService().getCurrentTtyMode(mContext.getOpPackageName()); + } + } catch (RemoteException e) { + Log.e(TAG, "RemoteException attempting to get the current TTY mode.", e); + } + return TTY_MODE_OFF; + } + + /** + * Registers a new incoming call. A {@link ConnectionService} should invoke this method when it + * has an incoming call. For managed {@link ConnectionService}s, the specified + * {@link PhoneAccountHandle} must have been registered with {@link #registerPhoneAccount} and + * the user must have enabled the corresponding {@link PhoneAccount}. This can be checked using + * {@link #getPhoneAccount}. Self-managed {@link ConnectionService}s must have + * {@link android.Manifest.permission#MANAGE_OWN_CALLS} to add a new incoming call. + * <p> + * The incoming call you are adding is assumed to have a video state of + * {@link VideoProfile#STATE_AUDIO_ONLY}, unless the extra value + * {@link #EXTRA_INCOMING_VIDEO_STATE} is specified. + * <p> + * Once invoked, this method will cause the system to bind to the {@link ConnectionService} + * associated with the {@link PhoneAccountHandle} and request additional information about the + * call (See {@link ConnectionService#onCreateIncomingConnection}) before starting the incoming + * call UI. + * <p> + * For a managed {@link ConnectionService}, a {@link SecurityException} will be thrown if either + * the {@link PhoneAccountHandle} does not correspond to a registered {@link PhoneAccount} or + * the associated {@link PhoneAccount} is not currently enabled by the user. + * <p> + * 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}. + * + * @param phoneAccount A {@link PhoneAccountHandle} registered with + * {@link #registerPhoneAccount}. + * @param extras A bundle that will be passed through to + * {@link ConnectionService#onCreateIncomingConnection}. + */ + public void addNewIncomingCall(PhoneAccountHandle phoneAccount, Bundle extras) { + try { + if (isServiceConnected()) { + getTelecomService().addNewIncomingCall( + phoneAccount, extras == null ? new Bundle() : extras); + } + } catch (RemoteException e) { + Log.e(TAG, "RemoteException adding a new incoming call: " + phoneAccount, e); + } + } + + /** + * Registers a new unknown call with Telecom. This can only be called by the system Telephony + * service. This is invoked when Telephony detects a new unknown connection that was neither + * a new incoming call, nor an user-initiated outgoing call. + * + * @param phoneAccount A {@link PhoneAccountHandle} registered with + * {@link #registerPhoneAccount}. + * @param extras A bundle that will be passed through to + * {@link ConnectionService#onCreateIncomingConnection}. + * @hide + */ + @SystemApi + public void addNewUnknownCall(PhoneAccountHandle phoneAccount, Bundle extras) { + try { + if (isServiceConnected()) { + getTelecomService().addNewUnknownCall( + phoneAccount, extras == null ? new Bundle() : extras); + } + } catch (RemoteException e) { + Log.e(TAG, "RemoteException adding a new unknown call: " + phoneAccount, e); + } + } + + /** + * Processes the specified dial string as an MMI code. + * MMI codes are any sequence of characters entered into the dialpad that contain a "*" or "#". + * Some of these sequences launch special behavior through handled by Telephony. + * This method uses the default subscription. + * <p> + * Requires that the method-caller be set as the system dialer app. + * </p> + * + * Requires permission: {@link android.Manifest.permission#MODIFY_PHONE_STATE} + * + * @param dialString The digits to dial. + * @return True if the digits were processed as an MMI code, false otherwise. + */ + @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) + public boolean handleMmi(String dialString) { + ITelecomService service = getTelecomService(); + if (service != null) { + try { + return service.handlePinMmi(dialString, mContext.getOpPackageName()); + } catch (RemoteException e) { + Log.e(TAG, "Error calling ITelecomService#handlePinMmi", e); + } + } + return false; + } + + /** + * Processes the specified dial string as an MMI code. + * MMI codes are any sequence of characters entered into the dialpad that contain a "*" or "#". + * Some of these sequences launch special behavior through handled by Telephony. + * <p> + * Requires that the method-caller be set as the system dialer app. + * </p> + * + * Requires permission: {@link android.Manifest.permission#MODIFY_PHONE_STATE} + * + * @param accountHandle The handle for the account the MMI code should apply to. + * @param dialString The digits to dial. + * @return True if the digits were processed as an MMI code, false otherwise. + */ + @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) + public boolean handleMmi(String dialString, PhoneAccountHandle accountHandle) { + ITelecomService service = getTelecomService(); + if (service != null) { + try { + return service.handlePinMmiForPhoneAccount(accountHandle, dialString, + mContext.getOpPackageName()); + } catch (RemoteException e) { + Log.e(TAG, "Error calling ITelecomService#handlePinMmi", e); + } + } + return false; + } + + /** + * Requires permission: {@link android.Manifest.permission#MODIFY_PHONE_STATE} + * + * @param accountHandle The handle for the account to derive an adn query URI for or + * {@code null} to return a URI which will use the default account. + * @return The URI (with the content:// scheme) specific to the specified {@link PhoneAccount} + * for the the content retrieve. + */ + @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) + public Uri getAdnUriForPhoneAccount(PhoneAccountHandle accountHandle) { + ITelecomService service = getTelecomService(); + if (service != null && accountHandle != null) { + try { + return service.getAdnUriForPhoneAccount(accountHandle, mContext.getOpPackageName()); + } catch (RemoteException e) { + Log.e(TAG, "Error calling ITelecomService#getAdnUriForPhoneAccount", e); + } + } + return Uri.parse("content://icc/adn"); + } + + /** + * Removes the missed-call notification if one is present. + * <p> + * Requires that the method-caller be set as the system dialer app. + * </p> + * + * Requires permission: {@link android.Manifest.permission#MODIFY_PHONE_STATE} + */ + @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) + public void cancelMissedCallsNotification() { + ITelecomService service = getTelecomService(); + if (service != null) { + try { + service.cancelMissedCallsNotification(mContext.getOpPackageName()); + } catch (RemoteException e) { + Log.e(TAG, "Error calling ITelecomService#cancelMissedCallsNotification", e); + } + } + } + + /** + * Brings the in-call screen to the foreground if there is an ongoing call. If there is + * currently no ongoing call, then this method does nothing. + * <p> + * Requires that the method-caller be set as the system dialer app or have the + * {@link android.Manifest.permission#READ_PHONE_STATE} permission. + * </p> + * + * @param showDialpad Brings up the in-call dialpad as part of showing the in-call screen. + */ + @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) + public void showInCallScreen(boolean showDialpad) { + ITelecomService service = getTelecomService(); + if (service != null) { + try { + service.showInCallScreen(showDialpad, mContext.getOpPackageName()); + } catch (RemoteException e) { + Log.e(TAG, "Error calling ITelecomService#showCallScreen", e); + } + } + } + + /** + * Places a new outgoing call to the provided address using the system telecom service with + * the specified extras. + * + * This method is equivalent to placing an outgoing call using {@link Intent#ACTION_CALL}, + * except that the outgoing call will always be sent via the system telecom service. If + * method-caller is either the user selected default dialer app or preloaded system dialer + * app, then emergency calls will also be allowed. + * + * Placing a call via a managed {@link ConnectionService} requires permission: + * {@link android.Manifest.permission#CALL_PHONE} + * + * Usage example: + * <pre> + * Uri uri = Uri.fromParts("tel", "12345", null); + * Bundle extras = new Bundle(); + * extras.putBoolean(TelecomManager.EXTRA_START_CALL_WITH_SPEAKERPHONE, true); + * telecomManager.placeCall(uri, extras); + * </pre> + * + * The following keys are supported in the supplied extras. + * <ul> + * <li>{@link #EXTRA_OUTGOING_CALL_EXTRAS}</li> + * <li>{@link #EXTRA_PHONE_ACCOUNT_HANDLE}</li> + * <li>{@link #EXTRA_START_CALL_WITH_SPEAKERPHONE}</li> + * <li>{@link #EXTRA_START_CALL_WITH_VIDEO_STATE}</li> + * </ul> + * <p> + * An app which implements the self-managed {@link ConnectionService} API uses + * {@link #placeCall(Uri, Bundle)} to inform Telecom of a new outgoing call. A self-managed + * {@link ConnectionService} must include {@link #EXTRA_PHONE_ACCOUNT_HANDLE} to specify its + * associated {@link android.telecom.PhoneAccountHandle}. + * + * Self-managed {@link ConnectionService}s require permission + * {@link android.Manifest.permission#MANAGE_OWN_CALLS}. + * + * @param address The address to make the call to. + * @param extras Bundle of extras to use with the call. + */ + @RequiresPermission(anyOf = {android.Manifest.permission.CALL_PHONE, + android.Manifest.permission.MANAGE_OWN_CALLS}) + public void placeCall(Uri address, Bundle extras) { + ITelecomService service = getTelecomService(); + if (service != null) { + if (address == null) { + Log.w(TAG, "Cannot place call to empty address."); + } + try { + service.placeCall(address, extras == null ? new Bundle() : extras, + mContext.getOpPackageName()); + } catch (RemoteException e) { + Log.e(TAG, "Error calling ITelecomService#placeCall", e); + } + } + } + + /** + * Enables and disables specified phone account. + * + * @param handle Handle to the phone account. + * @param isEnabled Enable state of the phone account. + * @hide + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) + public void enablePhoneAccount(PhoneAccountHandle handle, boolean isEnabled) { + ITelecomService service = getTelecomService(); + if (service != null) { + try { + service.enablePhoneAccount(handle, isEnabled); + } catch (RemoteException e) { + Log.e(TAG, "Error enablePhoneAbbount", e); + } + } + } + + /** + * Dumps telecom analytics for uploading. + * + * @return + * @hide + */ + @SystemApi + @RequiresPermission(Manifest.permission.DUMP) + public TelecomAnalytics dumpAnalytics() { + ITelecomService service = getTelecomService(); + TelecomAnalytics result = null; + if (service != null) { + try { + result = service.dumpCallAnalytics(); + } catch (RemoteException e) { + Log.e(TAG, "Error dumping call analytics", e); + } + } + return result; + } + + /** + * Creates the {@link Intent} which can be used with {@link Context#startActivity(Intent)} to + * launch the activity to manage blocked numbers. + * <p> The activity will display the UI to manage blocked numbers only if + * {@link android.provider.BlockedNumberContract#canCurrentUserBlockNumbers(Context)} returns + * {@code true} for the current user. + */ + public Intent createManageBlockedNumbersIntent() { + ITelecomService service = getTelecomService(); + Intent result = null; + if (service != null) { + try { + result = service.createManageBlockedNumbersIntent(); + } catch (RemoteException e) { + Log.e(TAG, "Error calling ITelecomService#createManageBlockedNumbersIntent", e); + } + } + return result; + } + + /** + * Determines whether Telecom would permit an incoming call to be added via the + * {@link #addNewIncomingCall(PhoneAccountHandle, Bundle)} API for the specified + * {@link PhoneAccountHandle}. + * <p> + * A {@link ConnectionService} may not add a call for the specified {@link PhoneAccountHandle} + * in the following situations: + * <ul> + * <li>{@link PhoneAccount} does not have property + * {@link PhoneAccount#CAPABILITY_SELF_MANAGED} set (i.e. it is a managed + * {@link ConnectionService}), and the active or held call limit has + * been reached.</li> + * <li>There is an ongoing emergency call.</li> + * </ul> + * + * @param phoneAccountHandle The {@link PhoneAccountHandle} the call will be added for. + * @return {@code true} if telecom will permit an incoming call to be added, {@code false} + * otherwise. + */ + public boolean isIncomingCallPermitted(PhoneAccountHandle phoneAccountHandle) { + if (phoneAccountHandle == null) { + return false; + } + + ITelecomService service = getTelecomService(); + if (service != null) { + try { + return service.isIncomingCallPermitted(phoneAccountHandle); + } catch (RemoteException e) { + Log.e(TAG, "Error isIncomingCallPermitted", e); + } + } + return false; + } + + /** + * Determines whether Telecom would permit an outgoing call to be placed via the + * {@link #placeCall(Uri, Bundle)} API for the specified {@link PhoneAccountHandle}. + * <p> + * A {@link ConnectionService} may not place a call for the specified {@link PhoneAccountHandle} + * in the following situations: + * <ul> + * <li>{@link PhoneAccount} does not have property + * {@link PhoneAccount#CAPABILITY_SELF_MANAGED} set (i.e. it is a managed + * {@link ConnectionService}), and the active, held or ringing call limit has + * been reached.</li> + * <li>{@link PhoneAccount} has property {@link PhoneAccount#CAPABILITY_SELF_MANAGED} set + * (i.e. it is a self-managed {@link ConnectionService} and there is an ongoing call in + * another {@link ConnectionService}.</li> + * <li>There is an ongoing emergency call.</li> + * </ul> + * + * @param phoneAccountHandle The {@link PhoneAccountHandle} the call will be added for. + * @return {@code true} if telecom will permit an outgoing call to be placed, {@code false} + * otherwise. + */ + public boolean isOutgoingCallPermitted(PhoneAccountHandle phoneAccountHandle) { + ITelecomService service = getTelecomService(); + if (service != null) { + try { + return service.isOutgoingCallPermitted(phoneAccountHandle); + } catch (RemoteException e) { + Log.e(TAG, "Error isOutgoingCallPermitted", e); + } + } + return false; + } + + + private ITelecomService getTelecomService() { + if (mTelecomServiceOverride != null) { + return mTelecomServiceOverride; + } + return ITelecomService.Stub.asInterface(ServiceManager.getService(Context.TELECOM_SERVICE)); + } + + private boolean isServiceConnected() { + boolean isConnected = getTelecomService() != null; + if (!isConnected) { + Log.w(TAG, "Telecom Service not found."); + } + return isConnected; + } +} diff --git a/android/telecom/TimedEvent.java b/android/telecom/TimedEvent.java new file mode 100644 index 00000000..e484e791 --- /dev/null +++ b/android/telecom/TimedEvent.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2016 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 java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +/** + * @hide + */ +public abstract class TimedEvent<T> { + public abstract long getTime(); + public abstract T getKey(); + + public static <T> Map<T, Double> averageTimings(Collection<? extends TimedEvent<T>> events) { + HashMap<T, Integer> counts = new HashMap<>(); + HashMap<T, Double> result = new HashMap<>(); + + for (TimedEvent<T> entry : events) { + if (counts.containsKey(entry.getKey())) { + counts.put(entry.getKey(), counts.get(entry.getKey()) + 1); + result.put(entry.getKey(), result.get(entry.getKey()) + entry.getTime()); + } else { + counts.put(entry.getKey(), 1); + result.put(entry.getKey(), (double) entry.getTime()); + } + } + + for (Map.Entry<T, Double> entry : result.entrySet()) { + result.put(entry.getKey(), entry.getValue() / counts.get(entry.getKey())); + } + + return result; + } +} + diff --git a/android/telecom/VideoCallImpl.java b/android/telecom/VideoCallImpl.java new file mode 100644 index 00000000..bae58ff8 --- /dev/null +++ b/android/telecom/VideoCallImpl.java @@ -0,0 +1,353 @@ +/* + * Copyright (C) 2014 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.net.Uri; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.Message; +import android.os.RemoteException; +import android.telecom.InCallService.VideoCall; +import android.view.Surface; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.os.SomeArgs; +import com.android.internal.telecom.IVideoCallback; +import com.android.internal.telecom.IVideoProvider; + +/** + * Implementation of a Video Call, which allows InCallUi to communicate commands to the underlying + * {@link Connection.VideoProvider}, and direct callbacks from the + * {@link Connection.VideoProvider} to the appropriate {@link VideoCall.Listener}. + * + * {@hide} + */ +public class VideoCallImpl extends VideoCall { + + private final IVideoProvider mVideoProvider; + private final VideoCallListenerBinder mBinder; + private VideoCall.Callback mCallback; + private int mVideoQuality = VideoProfile.QUALITY_UNKNOWN; + private int mVideoState = VideoProfile.STATE_AUDIO_ONLY; + private final String mCallingPackageName; + + private int mTargetSdkVersion; + + private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient() { + @Override + public void binderDied() { + mVideoProvider.asBinder().unlinkToDeath(this, 0); + } + }; + + /** + * IVideoCallback stub implementation. + */ + private final class VideoCallListenerBinder extends IVideoCallback.Stub { + @Override + public void receiveSessionModifyRequest(VideoProfile videoProfile) { + if (mHandler == null) { + return; + } + mHandler.obtainMessage(MessageHandler.MSG_RECEIVE_SESSION_MODIFY_REQUEST, + videoProfile).sendToTarget(); + + } + + @Override + public void receiveSessionModifyResponse(int status, VideoProfile requestProfile, + VideoProfile responseProfile) { + if (mHandler == null) { + return; + } + SomeArgs args = SomeArgs.obtain(); + args.arg1 = status; + args.arg2 = requestProfile; + args.arg3 = responseProfile; + mHandler.obtainMessage(MessageHandler.MSG_RECEIVE_SESSION_MODIFY_RESPONSE, args) + .sendToTarget(); + } + + @Override + public void handleCallSessionEvent(int event) { + if (mHandler == null) { + return; + } + mHandler.obtainMessage(MessageHandler.MSG_HANDLE_CALL_SESSION_EVENT, event) + .sendToTarget(); + } + + @Override + public void changePeerDimensions(int width, int height) { + if (mHandler == null) { + return; + } + SomeArgs args = SomeArgs.obtain(); + args.arg1 = width; + args.arg2 = height; + mHandler.obtainMessage(MessageHandler.MSG_CHANGE_PEER_DIMENSIONS, args).sendToTarget(); + } + + @Override + public void changeVideoQuality(int videoQuality) { + if (mHandler == null) { + return; + } + mHandler.obtainMessage(MessageHandler.MSG_CHANGE_VIDEO_QUALITY, videoQuality, 0) + .sendToTarget(); + } + + @Override + public void changeCallDataUsage(long dataUsage) { + if (mHandler == null) { + return; + } + mHandler.obtainMessage(MessageHandler.MSG_CHANGE_CALL_DATA_USAGE, dataUsage) + .sendToTarget(); + } + + @Override + public void changeCameraCapabilities(VideoProfile.CameraCapabilities cameraCapabilities) { + if (mHandler == null) { + return; + } + mHandler.obtainMessage(MessageHandler.MSG_CHANGE_CAMERA_CAPABILITIES, + cameraCapabilities).sendToTarget(); + } + } + + /** Default handler used to consolidate binder method calls onto a single thread. */ + private final class MessageHandler extends Handler { + private static final int MSG_RECEIVE_SESSION_MODIFY_REQUEST = 1; + private static final int MSG_RECEIVE_SESSION_MODIFY_RESPONSE = 2; + private static final int MSG_HANDLE_CALL_SESSION_EVENT = 3; + private static final int MSG_CHANGE_PEER_DIMENSIONS = 4; + private static final int MSG_CHANGE_CALL_DATA_USAGE = 5; + private static final int MSG_CHANGE_CAMERA_CAPABILITIES = 6; + private static final int MSG_CHANGE_VIDEO_QUALITY = 7; + + public MessageHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + if (mCallback == null) { + return; + } + + SomeArgs args; + switch (msg.what) { + case MSG_RECEIVE_SESSION_MODIFY_REQUEST: + mCallback.onSessionModifyRequestReceived((VideoProfile) msg.obj); + break; + case MSG_RECEIVE_SESSION_MODIFY_RESPONSE: + args = (SomeArgs) msg.obj; + try { + int status = (int) args.arg1; + VideoProfile requestProfile = (VideoProfile) args.arg2; + VideoProfile responseProfile = (VideoProfile) args.arg3; + + mCallback.onSessionModifyResponseReceived( + status, requestProfile, responseProfile); + } finally { + args.recycle(); + } + break; + case MSG_HANDLE_CALL_SESSION_EVENT: + mCallback.onCallSessionEvent((int) msg.obj); + break; + case MSG_CHANGE_PEER_DIMENSIONS: + args = (SomeArgs) msg.obj; + try { + int width = (int) args.arg1; + int height = (int) args.arg2; + mCallback.onPeerDimensionsChanged(width, height); + } finally { + args.recycle(); + } + break; + case MSG_CHANGE_CALL_DATA_USAGE: + mCallback.onCallDataUsageChanged((long) msg.obj); + break; + case MSG_CHANGE_CAMERA_CAPABILITIES: + mCallback.onCameraCapabilitiesChanged( + (VideoProfile.CameraCapabilities) msg.obj); + break; + case MSG_CHANGE_VIDEO_QUALITY: + mVideoQuality = msg.arg1; + mCallback.onVideoQualityChanged(msg.arg1); + break; + default: + break; + } + } + }; + + private Handler mHandler; + + VideoCallImpl(IVideoProvider videoProvider, String callingPackageName, int targetSdkVersion) + throws RemoteException { + mVideoProvider = videoProvider; + mVideoProvider.asBinder().linkToDeath(mDeathRecipient, 0); + + mBinder = new VideoCallListenerBinder(); + mVideoProvider.addVideoCallback(mBinder); + mCallingPackageName = callingPackageName; + setTargetSdkVersion(targetSdkVersion); + } + + @VisibleForTesting + public void setTargetSdkVersion(int sdkVersion) { + mTargetSdkVersion = sdkVersion; + } + + public void destroy() { + unregisterCallback(mCallback); + } + + /** {@inheritDoc} */ + public void registerCallback(VideoCall.Callback callback) { + registerCallback(callback, null); + } + + /** {@inheritDoc} */ + public void registerCallback(VideoCall.Callback callback, Handler handler) { + mCallback = callback; + if (handler == null) { + mHandler = new MessageHandler(Looper.getMainLooper()); + } else { + mHandler = new MessageHandler(handler.getLooper()); + } + } + + /** {@inheritDoc} */ + public void unregisterCallback(VideoCall.Callback callback) { + if (callback != mCallback) { + return; + } + + mCallback = null; + try { + mVideoProvider.removeVideoCallback(mBinder); + } catch (RemoteException e) { + } + } + + /** {@inheritDoc} */ + public void setCamera(String cameraId) { + try { + Log.w(this, "setCamera: cameraId=%s, calling=%s", cameraId, mCallingPackageName); + mVideoProvider.setCamera(cameraId, mCallingPackageName, mTargetSdkVersion); + } catch (RemoteException e) { + } + } + + /** {@inheritDoc} */ + public void setPreviewSurface(Surface surface) { + try { + mVideoProvider.setPreviewSurface(surface); + } catch (RemoteException e) { + } + } + + /** {@inheritDoc} */ + public void setDisplaySurface(Surface surface) { + try { + mVideoProvider.setDisplaySurface(surface); + } catch (RemoteException e) { + } + } + + /** {@inheritDoc} */ + public void setDeviceOrientation(int rotation) { + try { + mVideoProvider.setDeviceOrientation(rotation); + } catch (RemoteException e) { + } + } + + /** {@inheritDoc} */ + public void setZoom(float value) { + try { + mVideoProvider.setZoom(value); + } catch (RemoteException e) { + } + } + + /** + * Sends a session modification request to the video provider. + * <p> + * The {@link InCallService} will create the {@code requestProfile} based on the current + * video state (i.e. {@link Call.Details#getVideoState()}). It is, however, possible that the + * video state maintained by the {@link InCallService} could get out of sync with what is known + * by the {@link android.telecom.Connection.VideoProvider}. To remove ambiguity, the + * {@link VideoCallImpl} passes along the pre-modify video profile to the {@code VideoProvider} + * to ensure it has full context of the requested change. + * + * @param requestProfile The requested video profile. + */ + public void sendSessionModifyRequest(VideoProfile requestProfile) { + try { + VideoProfile originalProfile = new VideoProfile(mVideoState, mVideoQuality); + + mVideoProvider.sendSessionModifyRequest(originalProfile, requestProfile); + } catch (RemoteException e) { + } + } + + /** {@inheritDoc} */ + public void sendSessionModifyResponse(VideoProfile responseProfile) { + try { + mVideoProvider.sendSessionModifyResponse(responseProfile); + } catch (RemoteException e) { + } + } + + /** {@inheritDoc} */ + public void requestCameraCapabilities() { + try { + mVideoProvider.requestCameraCapabilities(); + } catch (RemoteException e) { + } + } + + /** {@inheritDoc} */ + public void requestCallDataUsage() { + try { + mVideoProvider.requestCallDataUsage(); + } catch (RemoteException e) { + } + } + + /** {@inheritDoc} */ + public void setPauseImage(Uri uri) { + try { + mVideoProvider.setPauseImage(uri); + } catch (RemoteException e) { + } + } + + /** + * Sets the video state for the current video call. + * @param videoState the new video state. + */ + public void setVideoState(int videoState) { + mVideoState = videoState; + } +} diff --git a/android/telecom/VideoCallbackServant.java b/android/telecom/VideoCallbackServant.java new file mode 100644 index 00000000..1fbad224 --- /dev/null +++ b/android/telecom/VideoCallbackServant.java @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2014 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 + R* limitations under the License. + */ + +package android.telecom; + +import com.android.internal.os.SomeArgs; +import com.android.internal.telecom.IVideoCallback; + +import android.os.Handler; +import android.os.Message; +import android.os.RemoteException; + +/** + * A component that provides an RPC servant implementation of {@link IVideoCallback}, + * posting incoming messages on the main thread on a client-supplied delegate object. + * + * TODO: Generate this and similar classes using a compiler starting from AIDL interfaces. + * + * @hide + */ +final class VideoCallbackServant { + private static final int MSG_RECEIVE_SESSION_MODIFY_REQUEST = 0; + private static final int MSG_RECEIVE_SESSION_MODIFY_RESPONSE = 1; + private static final int MSG_HANDLE_CALL_SESSION_EVENT = 2; + private static final int MSG_CHANGE_PEER_DIMENSIONS = 3; + private static final int MSG_CHANGE_CALL_DATA_USAGE = 4; + private static final int MSG_CHANGE_CAMERA_CAPABILITIES = 5; + private static final int MSG_CHANGE_VIDEO_QUALITY = 6; + + private final IVideoCallback mDelegate; + + private final Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + try { + internalHandleMessage(msg); + } catch (RemoteException e) { + } + } + + // Internal method defined to centralize handling of RemoteException + private void internalHandleMessage(Message msg) throws RemoteException { + switch (msg.what) { + case MSG_RECEIVE_SESSION_MODIFY_REQUEST: { + mDelegate.receiveSessionModifyRequest((VideoProfile) msg.obj); + break; + } + case MSG_RECEIVE_SESSION_MODIFY_RESPONSE: { + SomeArgs args = (SomeArgs) msg.obj; + try { + mDelegate.receiveSessionModifyResponse( + args.argi1, + (VideoProfile) args.arg1, + (VideoProfile) args.arg2); + } finally { + args.recycle(); + } + break; + } + case MSG_HANDLE_CALL_SESSION_EVENT: { + SomeArgs args = (SomeArgs) msg.obj; + try { + mDelegate.handleCallSessionEvent(args.argi1); + } finally { + args.recycle(); + } + break; + } + case MSG_CHANGE_PEER_DIMENSIONS: { + SomeArgs args = (SomeArgs) msg.obj; + try { + mDelegate.changePeerDimensions(args.argi1, args.argi2); + } finally { + args.recycle(); + } + break; + } + case MSG_CHANGE_CALL_DATA_USAGE: { + SomeArgs args = (SomeArgs) msg.obj; + try { + mDelegate.changeCallDataUsage((long) args.arg1); + } finally { + args.recycle(); + } + break; + } + case MSG_CHANGE_CAMERA_CAPABILITIES: { + mDelegate.changeCameraCapabilities((VideoProfile.CameraCapabilities) msg.obj); + break; + } + case MSG_CHANGE_VIDEO_QUALITY: { + mDelegate.changeVideoQuality(msg.arg1); + break; + } + } + } + }; + + private final IVideoCallback mStub = new IVideoCallback.Stub() { + @Override + public void receiveSessionModifyRequest(VideoProfile videoProfile) throws RemoteException { + mHandler.obtainMessage(MSG_RECEIVE_SESSION_MODIFY_REQUEST, videoProfile).sendToTarget(); + } + + @Override + public void receiveSessionModifyResponse(int status, VideoProfile requestedProfile, + VideoProfile responseProfile) throws RemoteException { + SomeArgs args = SomeArgs.obtain(); + args.argi1 = status; + args.arg1 = requestedProfile; + args.arg2 = responseProfile; + mHandler.obtainMessage(MSG_RECEIVE_SESSION_MODIFY_RESPONSE, args).sendToTarget(); + } + + @Override + public void handleCallSessionEvent(int event) throws RemoteException { + SomeArgs args = SomeArgs.obtain(); + args.argi1 = event; + mHandler.obtainMessage(MSG_HANDLE_CALL_SESSION_EVENT, args).sendToTarget(); + } + + @Override + public void changePeerDimensions(int width, int height) throws RemoteException { + SomeArgs args = SomeArgs.obtain(); + args.argi1 = width; + args.argi2 = height; + mHandler.obtainMessage(MSG_CHANGE_PEER_DIMENSIONS, args).sendToTarget(); + } + + @Override + public void changeCallDataUsage(long dataUsage) throws RemoteException { + SomeArgs args = SomeArgs.obtain(); + args.arg1 = dataUsage; + mHandler.obtainMessage(MSG_CHANGE_CALL_DATA_USAGE, args).sendToTarget(); + } + + @Override + public void changeCameraCapabilities( + VideoProfile.CameraCapabilities cameraCapabilities) + throws RemoteException { + mHandler.obtainMessage(MSG_CHANGE_CAMERA_CAPABILITIES, cameraCapabilities) + .sendToTarget(); + } + + @Override + public void changeVideoQuality(int videoQuality) throws RemoteException { + mHandler.obtainMessage(MSG_CHANGE_VIDEO_QUALITY, videoQuality, 0).sendToTarget(); + } + }; + + public VideoCallbackServant(IVideoCallback delegate) { + mDelegate = delegate; + } + + public IVideoCallback getStub() { + return mStub; + } +} diff --git a/android/telecom/VideoProfile.java b/android/telecom/VideoProfile.java new file mode 100644 index 00000000..e0e3a085 --- /dev/null +++ b/android/telecom/VideoProfile.java @@ -0,0 +1,471 @@ +/* + * Copyright (C) 2014 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.os.Parcel; +import android.os.Parcelable; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Represents attributes of video calls. + */ +public class VideoProfile implements Parcelable { + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({QUALITY_UNKNOWN, QUALITY_HIGH, QUALITY_MEDIUM, QUALITY_LOW, QUALITY_DEFAULT}) + public @interface VideoQuality {} + + /** + * "Unknown" video quality. + * @hide + */ + public static final int QUALITY_UNKNOWN = 0; + /** + * "High" video quality. + */ + public static final int QUALITY_HIGH = 1; + + /** + * "Medium" video quality. + */ + public static final int QUALITY_MEDIUM = 2; + + /** + * "Low" video quality. + */ + public static final int QUALITY_LOW = 3; + + /** + * Use default video quality. + */ + public static final int QUALITY_DEFAULT = 4; + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef( + flag = true, + value = {STATE_AUDIO_ONLY, STATE_TX_ENABLED, STATE_RX_ENABLED, STATE_BIDIRECTIONAL, + STATE_PAUSED}) + public @interface VideoState {} + + /** + * Used when answering or dialing a call to indicate that the call does not have a video + * component. + * <p> + * Should <b>not</b> be used in comparison checks to determine if a video state represents an + * audio-only call. + * <p> + * The following, for example, is not the correct way to check if a call is audio-only: + * <pre> + * {@code + * // This is the incorrect way to check for an audio-only call. + * if (videoState == VideoProfile.STATE_AUDIO_ONLY) { + * // Handle audio-only call. + * } + * } + * </pre> + * <p> + * Instead, use the {@link VideoProfile#isAudioOnly(int)} helper function to check if a + * video state represents an audio-only call: + * <pre> + * {@code + * // This is the correct way to check for an audio-only call. + * if (VideoProfile.isAudioOnly(videoState)) { + * // Handle audio-only call. + * } + * } + * </pre> + */ + public static final int STATE_AUDIO_ONLY = 0x0; + + /** + * Video transmission is enabled. + */ + public static final int STATE_TX_ENABLED = 0x1; + + /** + * Video reception is enabled. + */ + public static final int STATE_RX_ENABLED = 0x2; + + /** + * Video signal is bi-directional. + */ + public static final int STATE_BIDIRECTIONAL = STATE_TX_ENABLED | STATE_RX_ENABLED; + + /** + * Video is paused. + */ + public static final int STATE_PAUSED = 0x4; + + private final int mVideoState; + + private final int mQuality; + + /** + * Creates an instance of the VideoProfile + * + * @param videoState The video state. + */ + public VideoProfile(@VideoState int videoState) { + this(videoState, QUALITY_DEFAULT); + } + + /** + * Creates an instance of the VideoProfile + * + * @param videoState The video state. + * @param quality The video quality. + */ + public VideoProfile(@VideoState int videoState, @VideoQuality int quality) { + mVideoState = videoState; + mQuality = quality; + } + + /** + * The video state of the call. + * Valid values: {@link VideoProfile#STATE_AUDIO_ONLY}, + * {@link VideoProfile#STATE_BIDIRECTIONAL}, + * {@link VideoProfile#STATE_TX_ENABLED}, + * {@link VideoProfile#STATE_RX_ENABLED}, + * {@link VideoProfile#STATE_PAUSED}. + */ + @VideoState + public int getVideoState() { + return mVideoState; + } + + /** + * The desired video quality for the call. + * Valid values: {@link VideoProfile#QUALITY_HIGH}, {@link VideoProfile#QUALITY_MEDIUM}, + * {@link VideoProfile#QUALITY_LOW}, {@link VideoProfile#QUALITY_DEFAULT}. + */ + @VideoQuality + public int getQuality() { + return mQuality; + } + + /** + * Responsible for creating VideoProfile objects from deserialized Parcels. + **/ + public static final Parcelable.Creator<VideoProfile> CREATOR = + new Parcelable.Creator<VideoProfile> () { + /** + * Creates a MediaProfile instances from a parcel. + * + * @param source The parcel. + * @return The MediaProfile. + */ + @Override + public VideoProfile createFromParcel(Parcel source) { + int state = source.readInt(); + int quality = source.readInt(); + + ClassLoader classLoader = VideoProfile.class.getClassLoader(); + return new VideoProfile(state, quality); + } + + @Override + public VideoProfile[] newArray(int size) { + return new VideoProfile[size]; + } + }; + + /** + * Describe the kinds of special objects contained in this Parcelable's + * marshalled representation. + * + * @return a bitmask indicating the set of special object types marshalled + * by the Parcelable. + */ + @Override + public int describeContents() { + return 0; + } + + /** + * Flatten this object in to a Parcel. + * + * @param dest The Parcel in which the object should be written. + * @param flags Additional flags about how the object should be written. + * May be 0 or {@link #PARCELABLE_WRITE_RETURN_VALUE}. + */ + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mVideoState); + dest.writeInt(mQuality); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("[VideoProfile videoState = "); + sb.append(videoStateToString(mVideoState)); + sb.append(" videoQuality = "); + sb.append(mQuality); + sb.append("]"); + return sb.toString(); + } + + /** + * Generates a string representation of a video state. + * + * @param videoState The video state. + * @return String representation of the video state. + */ + public static String videoStateToString(@VideoState int videoState) { + StringBuilder sb = new StringBuilder(); + sb.append("Audio"); + + if (videoState == STATE_AUDIO_ONLY) { + sb.append(" Only"); + } else { + if (isTransmissionEnabled(videoState)) { + sb.append(" Tx"); + } + + if (isReceptionEnabled(videoState)) { + sb.append(" Rx"); + } + + if (isPaused(videoState)) { + sb.append(" Pause"); + } + } + + return sb.toString(); + } + + /** + * Indicates whether the video state is audio only. + * <p> + * Note: Considers only whether either both the {@link #STATE_RX_ENABLED} or + * {@link #STATE_TX_ENABLED} bits are off, but not {@link #STATE_PAUSED}. + * + * @param videoState The video state. + * @return {@code True} if the video state is audio only, {@code false} otherwise. + */ + public static boolean isAudioOnly(@VideoState int videoState) { + return !hasState(videoState, VideoProfile.STATE_TX_ENABLED) + && !hasState(videoState, VideoProfile.STATE_RX_ENABLED); + } + + /** + * Indicates whether video transmission or reception is enabled for a video state. + * + * @param videoState The video state. + * @return {@code True} if video transmission or reception is enabled, {@code false} otherwise. + */ + public static boolean isVideo(@VideoState int videoState) { + return hasState(videoState, VideoProfile.STATE_TX_ENABLED) + || hasState(videoState, VideoProfile.STATE_RX_ENABLED) + || hasState(videoState, VideoProfile.STATE_BIDIRECTIONAL); + } + + /** + * Indicates whether the video state has video transmission enabled. + * + * @param videoState The video state. + * @return {@code True} if video transmission is enabled, {@code false} otherwise. + */ + public static boolean isTransmissionEnabled(@VideoState int videoState) { + return hasState(videoState, VideoProfile.STATE_TX_ENABLED); + } + + /** + * Indicates whether the video state has video reception enabled. + * + * @param videoState The video state. + * @return {@code True} if video reception is enabled, {@code false} otherwise. + */ + public static boolean isReceptionEnabled(@VideoState int videoState) { + return hasState(videoState, VideoProfile.STATE_RX_ENABLED); + } + + /** + * Indicates whether the video state is bi-directional. + * + * @param videoState The video state. + * @return {@code True} if the video is bi-directional, {@code false} otherwise. + */ + public static boolean isBidirectional(@VideoState int videoState) { + return hasState(videoState, VideoProfile.STATE_BIDIRECTIONAL); + } + + /** + * Indicates whether the video state is paused. + * + * @param videoState The video state. + * @return {@code True} if the video is paused, {@code false} otherwise. + */ + public static boolean isPaused(@VideoState int videoState) { + return hasState(videoState, VideoProfile.STATE_PAUSED); + } + + /** + * Indicates if a specified state is set in a videoState bit-mask. + * + * @param videoState The video state bit-mask. + * @param state The state to check. + * @return {@code True} if the state is set. + */ + private static boolean hasState(@VideoState int videoState, @VideoState int state) { + return (videoState & state) == state; + } + + /** + * Represents the camera capabilities important to a Video Telephony provider. + */ + public static final class CameraCapabilities implements Parcelable { + + /** + * The width of the camera video in pixels. + */ + private final int mWidth; + + /** + * The height of the camera video in pixels. + */ + private final int mHeight; + + /** + * Whether the camera supports zoom. + */ + private final boolean mZoomSupported; + + /** + * The maximum zoom supported by the camera. + */ + private final float mMaxZoom; + + /** + * Create a call camera capabilities instance. + * + * @param width The width of the camera video (in pixels). + * @param height The height of the camera video (in pixels). + */ + public CameraCapabilities(int width, int height) { + this(width, height, false, 1.0f); + } + + /** + * Create a call camera capabilities instance that optionally + * supports zoom. + * + * @param width The width of the camera video (in pixels). + * @param height The height of the camera video (in pixels). + * @param zoomSupported True when camera supports zoom. + * @param maxZoom Maximum zoom supported by camera. + * @hide + */ + public CameraCapabilities(int width, int height, boolean zoomSupported, float maxZoom) { + mWidth = width; + mHeight = height; + mZoomSupported = zoomSupported; + mMaxZoom = maxZoom; + } + + /** + * Responsible for creating CallCameraCapabilities objects from deserialized Parcels. + **/ + public static final Parcelable.Creator<CameraCapabilities> CREATOR = + new Parcelable.Creator<CameraCapabilities> () { + /** + * Creates a CallCameraCapabilities instances from a parcel. + * + * @param source The parcel. + * @return The CallCameraCapabilities. + */ + @Override + public CameraCapabilities createFromParcel(Parcel source) { + int width = source.readInt(); + int height = source.readInt(); + boolean supportsZoom = source.readByte() != 0; + float maxZoom = source.readFloat(); + + return new CameraCapabilities(width, height, supportsZoom, maxZoom); + } + + @Override + public CameraCapabilities[] newArray(int size) { + return new CameraCapabilities[size]; + } + }; + + /** + * Describe the kinds of special objects contained in this Parcelable's + * marshalled representation. + * + * @return a bitmask indicating the set of special object types marshalled + * by the Parcelable. + */ + @Override + public int describeContents() { + return 0; + } + + /** + * Flatten this object in to a Parcel. + * + * @param dest The Parcel in which the object should be written. + * @param flags Additional flags about how the object should be written. + * May be 0 or {@link #PARCELABLE_WRITE_RETURN_VALUE}. + */ + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(getWidth()); + dest.writeInt(getHeight()); + dest.writeByte((byte) (isZoomSupported() ? 1 : 0)); + dest.writeFloat(getMaxZoom()); + } + + /** + * The width of the camera video in pixels. + */ + public int getWidth() { + return mWidth; + } + + /** + * The height of the camera video in pixels. + */ + public int getHeight() { + return mHeight; + } + + /** + * Whether the camera supports zoom. + * @hide + */ + public boolean isZoomSupported() { + return mZoomSupported; + } + + /** + * The maximum zoom supported by the camera. + * @hide + */ + public float getMaxZoom() { + return mMaxZoom; + } + } + +} diff --git a/android/telecom/Voicemail.java b/android/telecom/Voicemail.java new file mode 100644 index 00000000..ca235bf3 --- /dev/null +++ b/android/telecom/Voicemail.java @@ -0,0 +1,320 @@ +/* + * Copyright (C) 2015 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.net.Uri; +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Represents a single voicemail stored in the voicemail content provider. + * + * @hide + */ +public class Voicemail implements Parcelable { + private final Long mTimestamp; + private final String mNumber; + private final PhoneAccountHandle mPhoneAccount; + private final Long mId; + private final Long mDuration; + private final String mSource; + private final String mProviderData; + private final Uri mUri; + private final Boolean mIsRead; + private final Boolean mHasContent; + private final String mTranscription; + + private Voicemail(Long timestamp, String number, PhoneAccountHandle phoneAccountHandle, Long id, + Long duration, String source, String providerData, Uri uri, Boolean isRead, + Boolean hasContent, String transcription) { + mTimestamp = timestamp; + mNumber = number; + mPhoneAccount = phoneAccountHandle; + mId = id; + mDuration = duration; + mSource = source; + mProviderData = providerData; + mUri = uri; + mIsRead = isRead; + mHasContent = hasContent; + mTranscription = transcription; + } + + /** + * Create a {@link Builder} for a new {@link Voicemail} to be inserted. + * <p> + * The number and the timestamp are mandatory for insertion. + */ + public static Builder createForInsertion(long timestamp, String number) { + return new Builder().setNumber(number).setTimestamp(timestamp); + } + + /** + * Create a {@link Builder} for a {@link Voicemail} to be updated (or deleted). + * <p> + * The id and source data fields are mandatory for update - id is necessary for updating the + * database and source data is necessary for updating the server. + */ + public static Builder createForUpdate(long id, String sourceData) { + return new Builder().setId(id).setSourceData(sourceData); + } + + /** + * Builder pattern for creating a {@link Voicemail}. The builder must be created with the + * {@link #createForInsertion(long, String)} method. + * <p> + * This class is <b>not thread safe</b> + */ + public static class Builder { + private Long mBuilderTimestamp; + private String mBuilderNumber; + private PhoneAccountHandle mBuilderPhoneAccount; + private Long mBuilderId; + private Long mBuilderDuration; + private String mBuilderSourcePackage; + private String mBuilderSourceData; + private Uri mBuilderUri; + private Boolean mBuilderIsRead; + private boolean mBuilderHasContent; + private String mBuilderTranscription; + + /** You should use the correct factory method to construct a builder. */ + private Builder() { + } + + public Builder setNumber(String number) { + mBuilderNumber = number; + return this; + } + + public Builder setTimestamp(long timestamp) { + mBuilderTimestamp = timestamp; + return this; + } + + public Builder setPhoneAccount(PhoneAccountHandle phoneAccount) { + mBuilderPhoneAccount = phoneAccount; + return this; + } + + public Builder setId(long id) { + mBuilderId = id; + return this; + } + + public Builder setDuration(long duration) { + mBuilderDuration = duration; + return this; + } + + public Builder setSourcePackage(String sourcePackage) { + mBuilderSourcePackage = sourcePackage; + return this; + } + + public Builder setSourceData(String sourceData) { + mBuilderSourceData = sourceData; + return this; + } + + public Builder setUri(Uri uri) { + mBuilderUri = uri; + return this; + } + + public Builder setIsRead(boolean isRead) { + mBuilderIsRead = isRead; + return this; + } + + public Builder setHasContent(boolean hasContent) { + mBuilderHasContent = hasContent; + return this; + } + + public Builder setTranscription(String transcription) { + mBuilderTranscription = transcription; + return this; + } + + public Voicemail build() { + mBuilderId = mBuilderId == null ? -1 : mBuilderId; + mBuilderTimestamp = mBuilderTimestamp == null ? 0 : mBuilderTimestamp; + mBuilderDuration = mBuilderDuration == null ? 0: mBuilderDuration; + mBuilderIsRead = mBuilderIsRead == null ? false : mBuilderIsRead; + return new Voicemail(mBuilderTimestamp, mBuilderNumber, mBuilderPhoneAccount, + mBuilderId, mBuilderDuration, mBuilderSourcePackage, mBuilderSourceData, + mBuilderUri, mBuilderIsRead, mBuilderHasContent, mBuilderTranscription); + } + } + + /** + * The identifier of the voicemail in the content provider. + * <p> + * This may be missing in the case of a new {@link Voicemail} that we plan to insert into the + * content provider, since until it has been inserted we don't know what id it should have. If + * none is specified, we return -1. + */ + public long getId() { + return mId; + } + + /** The number of the person leaving the voicemail, empty string if unknown, null if not set. */ + public String getNumber() { + return mNumber; + } + + /** The phone account associated with the voicemail, null if not set. */ + public PhoneAccountHandle getPhoneAccount() { + return mPhoneAccount; + } + + /** The timestamp the voicemail was received, in millis since the epoch, zero if not set. */ + public long getTimestampMillis() { + return mTimestamp; + } + + /** Gets the duration of the voicemail in millis, or zero if the field is not set. */ + public long getDuration() { + return mDuration; + } + + /** + * Returns the package name of the source that added this voicemail, or null if this field is + * not set. + */ + public String getSourcePackage() { + return mSource; + } + + /** + * Returns the application-specific data type stored with the voicemail, or null if this field + * is not set. + * <p> + * Source data is typically used as an identifier to uniquely identify the voicemail against + * the voicemail server. This is likely to be something like the IMAP UID, or some other + * server-generated identifying string. + */ + public String getSourceData() { + return mProviderData; + } + + /** + * Gets the Uri that can be used to refer to this voicemail, and to make it play. + * <p> + * Returns null if we don't know the Uri. + */ + public Uri getUri() { + return mUri; + } + + /** + * Tells us if the voicemail message has been marked as read. + * <p> + * Always returns false if this field has not been set, i.e. if hasRead() returns false. + */ + public boolean isRead() { + return mIsRead; + } + + /** + * Tells us if there is content stored at the Uri. + */ + public boolean hasContent() { + return mHasContent; + } + + /** + * Returns the text transcription of this voicemail, or null if this field is not set. + */ + public String getTranscription() { + return mTranscription; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeLong(mTimestamp); + dest.writeCharSequence(mNumber); + if (mPhoneAccount == null) { + dest.writeInt(0); + } else { + dest.writeInt(1); + mPhoneAccount.writeToParcel(dest, flags); + } + dest.writeLong(mId); + dest.writeLong(mDuration); + dest.writeCharSequence(mSource); + dest.writeCharSequence(mProviderData); + if (mUri == null) { + dest.writeInt(0); + } else { + dest.writeInt(1); + mUri.writeToParcel(dest, flags); + } + if (mIsRead) { + dest.writeInt(1); + } else { + dest.writeInt(0); + } + if (mHasContent) { + dest.writeInt(1); + } else { + dest.writeInt(0); + } + dest.writeCharSequence(mTranscription); + } + + public static final Creator<Voicemail> CREATOR + = new Creator<Voicemail>() { + @Override + public Voicemail createFromParcel(Parcel in) { + return new Voicemail(in); + } + + @Override + public Voicemail[] newArray(int size) { + return new Voicemail[size]; + } + }; + + private Voicemail(Parcel in) { + mTimestamp = in.readLong(); + mNumber = (String) in.readCharSequence(); + if (in.readInt() > 0) { + mPhoneAccount = PhoneAccountHandle.CREATOR.createFromParcel(in); + } else { + mPhoneAccount = null; + } + mId = in.readLong(); + mDuration = in.readLong(); + mSource = (String) in.readCharSequence(); + mProviderData = (String) in.readCharSequence(); + if (in.readInt() > 0) { + mUri = Uri.CREATOR.createFromParcel(in); + } else { + mUri = null; + } + mIsRead = in.readInt() > 0 ? true : false; + mHasContent = in.readInt() > 0 ? true : false; + mTranscription = (String) in.readCharSequence(); + } +} diff --git a/android/telecom/package-info.java b/android/telecom/package-info.java new file mode 100644 index 00000000..a4140e5a --- /dev/null +++ b/android/telecom/package-info.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2016 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 + */ + +/** + * The Android Telecom framework is responsible for managing calls on an Android device. This can + * include SIM-based calls using the {@code Telephony} framework, VOIP calls using SIP (e.g. the + * {@code SipConnectionService}), or via a third-party VOIP + * {@link android.telecom.ConnectionService}. Telecom acts as a switchboard, routing calls and + * audio focus between {@link android.telecom.Connection}s provided by + * {@link android.telecom.ConnectionService} implementations, and + * {@link android.telecom.InCallService} implementations which provide a user interface for calls. + * <p> + * Android supports the following calling use cases (with increasing level of complexity): + * <ul> + * <li>Implement the self-managed {@link android.telecom.ConnectionService} API - this is ideal + * for developers of standalone calling apps which do not wish to show their calls within the + * default phone app, and do not wish to have other calls shown in their user interface. Using + * a self-managed {@link android.telecom.ConnectionService} implementation within your + * standalone calling app helps you ensure that your app will interoperate not only with native + * telephony calling on the device, but also other standalone calling apps implementing this + * API. It also manages audio routing and focus for you.</li> + * <li>Implement the managed {@link android.telecom.ConnectionService} API - facilitates + * development of a calling solution that relies on the existing device phone application (see + * {@link android.telecom.TelecomManager#getDefaultDialerPackage()}) to provide the user + * interface for calls. An example might be a third party implementation of SIP calling, or a + * VOIP calling service. A {@link android.telecom.ConnectionService} alone provides only the + * means of connecting calls, but has no associated user interface.</li> + * <li>Implement the {@link android.telecom.InCallService} API - facilitates development of a + * replacement for the device's default Phone/Dialer app. The + * {@link android.telecom.InCallService} alone does not have any calling capability and consists + * of the user-interface side of calling only. An {@link android.telecom.InCallService} must + * handle all Calls the Telecom framework is aware of. It must not make assumptions about the + * nature of the calls (e.g. assuming calls are SIM-based telephony calls), and should not + * implement calling restrictions based on any one {@link android.telecom.ConnectionService} + * (e.g. it should not enforce Telephony restrictions for video calls).</li> + * <li>Implement both the {@link android.telecom.InCallService} and + * {@link android.telecom.ConnectionService} API - ideal if you wish to create your own + * {@link android.telecom.ConnectionService} based calling solution, complete with its own + * full user interface, while showing all other Android calls in the same user interface. Using + * this approach, you must still ensure that your {@link android.telecom.InCallService} makes + * no assumption about the source of the calls it displays. You must also ensure that your + * {@link android.telecom.ConnectionService} implementation can still function without the + * default phone app being set to your custom {@link android.telecom.InCallService}.</li> + * </ul> + */ +package android.telecom;
\ No newline at end of file |