summaryrefslogtreecommitdiff
path: root/android/telecom
diff options
context:
space:
mode:
authorJustin Klaassen <justinklaassen@google.com>2017-09-15 17:58:39 -0400
committerJustin Klaassen <justinklaassen@google.com>2017-09-15 17:58:39 -0400
commit10d07c88d69cc64f73a069163e7ea5ba2519a099 (patch)
tree8dbd149eb350320a29c3d10e7ad3201de1c5cbee /android/telecom
parent677516fb6b6f207d373984757d3d9450474b6b00 (diff)
downloadandroid-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')
-rw-r--r--android/telecom/AudioState.java187
-rw-r--r--android/telecom/AuthenticatorService.java101
-rw-r--r--android/telecom/Call.java2092
-rw-r--r--android/telecom/CallAudioState.java213
-rw-r--r--android/telecom/CallScreeningService.java239
-rw-r--r--android/telecom/CallbackRecord.java44
-rw-r--r--android/telecom/Conference.java912
-rw-r--r--android/telecom/ConferenceParticipant.java158
-rw-r--r--android/telecom/Conferenceable.java26
-rw-r--r--android/telecom/Connection.java3120
-rw-r--r--android/telecom/ConnectionRequest.java374
-rw-r--r--android/telecom/ConnectionService.java2276
-rw-r--r--android/telecom/ConnectionServiceAdapter.java628
-rw-r--r--android/telecom/ConnectionServiceAdapterServant.java613
-rw-r--r--android/telecom/DefaultDialerManager.java235
-rw-r--r--android/telecom/DisconnectCause.java313
-rw-r--r--android/telecom/GatewayInfo.java117
-rw-r--r--android/telecom/InCallAdapter.java422
-rw-r--r--android/telecom/InCallService.java754
-rw-r--r--android/telecom/Log.java488
-rw-r--r--android/telecom/Logging/EventManager.java415
-rw-r--r--android/telecom/Logging/Runnable.java99
-rw-r--r--android/telecom/Logging/Session.java392
-rw-r--r--android/telecom/Logging/SessionManager.java428
-rw-r--r--android/telecom/Logging/TimedEvent.java50
-rw-r--r--android/telecom/ParcelableCall.java415
-rw-r--r--android/telecom/ParcelableCallAnalytics.java458
-rw-r--r--android/telecom/ParcelableConference.java189
-rw-r--r--android/telecom/ParcelableConnection.java329
-rw-r--r--android/telecom/ParcelableRttCall.java88
-rw-r--r--android/telecom/Phone.java391
-rw-r--r--android/telecom/PhoneAccount.java992
-rw-r--r--android/telecom/PhoneAccountHandle.java172
-rw-r--r--android/telecom/RemoteConference.java586
-rw-r--r--android/telecom/RemoteConnection.java1587
-rw-r--r--android/telecom/RemoteConnectionManager.java88
-rw-r--r--android/telecom/RemoteConnectionService.java575
-rw-r--r--android/telecom/Response.java40
-rw-r--r--android/telecom/StatusHints.java154
-rw-r--r--android/telecom/TelecomAnalytics.java148
-rw-r--r--android/telecom/TelecomManager.java1768
-rw-r--r--android/telecom/TimedEvent.java51
-rw-r--r--android/telecom/VideoCallImpl.java353
-rw-r--r--android/telecom/VideoCallbackServant.java171
-rw-r--r--android/telecom/VideoProfile.java471
-rw-r--r--android/telecom/Voicemail.java320
-rw-r--r--android/telecom/package-info.java59
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>
+ * &lt;service android:name="com.example.package.MyConnectionService"
+ * android:label="@string/some_label_for_my_connection_service"
+ * android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE"&gt;
+ * &lt;intent-filter&gt;
+ * &lt;action android:name="android.telecom.ConnectionService" /&gt;
+ * &lt;/intent-filter&gt;
+ * &lt;/service&gt;
+ * </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