Self Managed CS implementation.
Implementation for self-managed connection service APIs.
Add ability for self-managed CS to:
- register a phone account and have it be auto-enabled.
- add ability for self-managed calls, ensuring only self-manage calls
can be added.
Add implementations for new isXCallPermitted APIs.
Filter self-managed calls from InCallServices.
Ensure self managed connection creation doesn't use emergency call CS.
Add ability to set audio route from self-managed connection
Overhaul some of the CallsManager getCallsWithState methods to support
other use cases for self-managed calls (also use steams/filters.. Ooo aah)
Test: Manual
Bug: 34159263
Change-Id: I3131fd48ee5c5aa36c0e88992fa51879af07d495
diff --git a/src/com/android/server/telecom/CallsManager.java b/src/com/android/server/telecom/CallsManager.java
index de4931f..fb358aa 100644
--- a/src/com/android/server/telecom/CallsManager.java
+++ b/src/com/android/server/telecom/CallsManager.java
@@ -51,6 +51,7 @@
import android.telephony.PhoneNumberUtils;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
+import android.util.ArraySet;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.telephony.AsyncEmergencyContactNotifier;
@@ -68,6 +69,7 @@
import com.android.server.telecom.components.ErrorDialogActivity;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
@@ -79,6 +81,9 @@
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
/**
* Singleton.
@@ -122,6 +127,7 @@
private static final int MAXIMUM_DIALING_CALLS = 1;
private static final int MAXIMUM_OUTGOING_CALLS = 1;
private static final int MAXIMUM_TOP_LEVEL_CALLS = 2;
+ private static final int MAXIMUM_SELF_MANAGED_CALLS = 10;
private static final int[] OUTGOING_CALL_STATES =
{CallState.CONNECTING, CallState.SELECT_PHONE_ACCOUNT, CallState.DIALING,
@@ -131,6 +137,11 @@
{CallState.CONNECTING, CallState.SELECT_PHONE_ACCOUNT, CallState.DIALING,
CallState.PULLING, CallState.ACTIVE};
+ private static final int[] ANY_CALL_STATE =
+ {CallState.NEW, CallState.CONNECTING, CallState.SELECT_PHONE_ACCOUNT, CallState.DIALING,
+ CallState.RINGING, CallState.ACTIVE, CallState.ON_HOLD, CallState.DISCONNECTED,
+ CallState.ABORTED, CallState.DISCONNECTING, CallState.PULLING};
+
public static final String TELECOM_CALL_ID_PREFIX = "TC@";
// Maps call technologies in PhoneConstants to those in Analytics.
@@ -391,7 +402,7 @@
}
if (result.shouldAllowCall) {
- if (hasMaximumRingingCalls()) {
+ if (hasMaximumManagedRingingCalls(incomingCall)) {
if (shouldSilenceInsteadOfReject(incomingCall)) {
incomingCall.silence();
} else {
@@ -399,7 +410,7 @@
"Exceeds maximum number of ringing calls.");
rejectCallAndLog(incomingCall);
}
- } else if (hasMaximumDialingCalls()) {
+ } else if (hasMaximumManagedDialingCalls(incomingCall)) {
Log.i(this, "onCallFilteringCompleted: Call rejected! Exceeds maximum number of " +
"dialing calls.");
rejectCallAndLog(incomingCall);
@@ -758,7 +769,12 @@
setIntentExtrasAndStartTime(call, extras);
// TODO: Move this to be a part of addCall()
call.addListener(this);
- call.startCreateConnection(mPhoneAccountRegistrar);
+
+ if (call.isSelfManaged() && !isIncomingCallPermitted(call, call.getTargetPhoneAccount())) {
+ notifyCreateConnectionFailed(phoneAccountHandle, call);
+ } else {
+ call.startCreateConnection(mPhoneAccountRegistrar);
+ }
}
void addNewUnknownCall(PhoneAccountHandle phoneAccountHandle, Bundle extras) {
@@ -824,7 +840,11 @@
}
/**
- * Kicks off the first steps to creating an outgoing call so that InCallUI can launch.
+ * Kicks off the first steps to creating an outgoing call.
+ *
+ * For managed connections, this is the first step to launching the Incall UI.
+ * For self-managed connections, we don't expect the Incall UI to launch, but this is still a
+ * first step in getting the self-managed ConnectionService to create the connection.
*
* @param handle Handle to connect the call with.
* @param phoneAccountHandle The phone account which contains the component name of the
@@ -904,45 +924,53 @@
call.setVideoState(videoState);
}
- List<PhoneAccountHandle> accounts = constructPossiblePhoneAccounts(handle, initiatingUser);
- Log.v(this, "startOutgoingCall found accounts = " + accounts);
+ PhoneAccount targetPhoneAccount = mPhoneAccountRegistrar.getPhoneAccount(
+ phoneAccountHandle, initiatingUser);
+ boolean isSelfManaged = targetPhoneAccount != null && targetPhoneAccount.isSelfManaged();
- // Only dial with the requested phoneAccount if it is still valid. Otherwise treat this call
- // as if a phoneAccount was not specified (does the default behavior instead).
- // Note: We will not attempt to dial with a requested phoneAccount if it is disabled.
- if (phoneAccountHandle != null) {
- if (!accounts.contains(phoneAccountHandle)) {
- phoneAccountHandle = null;
- }
- }
+ List<PhoneAccountHandle> accounts;
+ if (!isSelfManaged) {
+ accounts = constructPossiblePhoneAccounts(handle, initiatingUser);
+ Log.v(this, "startOutgoingCall found accounts = " + accounts);
- if (phoneAccountHandle == null && accounts.size() > 0) {
- // No preset account, check if default exists that supports the URI scheme for the
- // handle and verify it can be used.
- if(accounts.size() > 1) {
- PhoneAccountHandle defaultPhoneAccountHandle =
- mPhoneAccountRegistrar.getOutgoingPhoneAccountForScheme(handle.getScheme(),
- initiatingUser);
- if (defaultPhoneAccountHandle != null &&
- accounts.contains(defaultPhoneAccountHandle)) {
- phoneAccountHandle = defaultPhoneAccountHandle;
+ // Only dial with the requested phoneAccount if it is still valid. Otherwise treat this
+ // call as if a phoneAccount was not specified (does the default behavior instead).
+ // Note: We will not attempt to dial with a requested phoneAccount if it is disabled.
+ if (phoneAccountHandle != null) {
+ if (!accounts.contains(phoneAccountHandle)) {
+ phoneAccountHandle = null;
}
- } else {
- // Use the only PhoneAccount that is available
- phoneAccountHandle = accounts.get(0);
}
+ if (phoneAccountHandle == null && accounts.size() > 0) {
+ // No preset account, check if default exists that supports the URI scheme for the
+ // handle and verify it can be used.
+ if (accounts.size() > 1) {
+ PhoneAccountHandle defaultPhoneAccountHandle =
+ mPhoneAccountRegistrar.getOutgoingPhoneAccountForScheme(
+ handle.getScheme(), initiatingUser);
+ if (defaultPhoneAccountHandle != null &&
+ accounts.contains(defaultPhoneAccountHandle)) {
+ phoneAccountHandle = defaultPhoneAccountHandle;
+ }
+ } else {
+ // Use the only PhoneAccount that is available
+ phoneAccountHandle = accounts.get(0);
+ }
+ }
+ } else {
+ accounts = Collections.EMPTY_LIST;
}
call.setTargetPhoneAccount(phoneAccountHandle);
- boolean isPotentialInCallMMICode = isPotentialInCallMMICode(handle);
+ boolean isPotentialInCallMMICode = isPotentialInCallMMICode(handle) && !isSelfManaged;
// Do not support any more live calls. Our options are to move a call to hold, disconnect
// a call, or cancel this call altogether. If a call is being reused, then it has already
// passed the makeRoomForOutgoingCall check once and will fail the second time due to the
// call transitioning into the CONNECTING state.
- if (!isPotentialInCallMMICode && (!isReusedCall &&
+ if (!isSelfManaged && !isPotentialInCallMMICode && (!isReusedCall &&
!makeRoomForOutgoingCall(call, call.isEmergencyCall()))) {
// just cancel at this point.
Log.i(this, "No remaining room for outgoing call: %s", call);
@@ -955,7 +983,7 @@
}
boolean needsAccountSelection = phoneAccountHandle == null && accounts.size() > 1 &&
- !call.isEmergencyCall();
+ !call.isEmergencyCall() && !isSelfManaged;
if (needsAccountSelection) {
// This is the state where the user is expected to select an account
@@ -1042,7 +1070,13 @@
if (call.getTargetPhoneAccount() != null || call.isEmergencyCall()) {
// If the account has been set, proceed to place the outgoing call.
// Otherwise the connection will be initiated when the account is set by the user.
- call.startCreateConnection(mPhoneAccountRegistrar);
+ if (call.isSelfManaged() && !isOutgoingCallPermitted(call,
+ call.getTargetPhoneAccount())) {
+
+ notifyCreateConnectionFailed(call.getTargetPhoneAccount(), call);
+ } else {
+ call.startCreateConnection(mPhoneAccountRegistrar);
+ }
} else if (mPhoneAccountRegistrar.getCallCapablePhoneAccounts(
requireCallCapableAccountByHandle ? call.getHandle().getScheme() : null, false,
call.getInitiatingUser()).isEmpty()) {
@@ -1948,42 +1982,92 @@
return false;
}
- private int getNumCallsWithState(int... states) {
- int count = 0;
- for (int state : states) {
- for (Call call : mCalls) {
- if (call.getParentCall() == null && call.getState() == state &&
- !call.isExternalCall()) {
+ @VisibleForTesting
+ public int getNumCallsWithState(final boolean isSelfManaged, Call excludeCall,
+ PhoneAccountHandle phoneAccountHandle, int... states) {
- count++;
- }
- }
+ Set<Integer> desiredStates = IntStream.of(states).boxed().collect(Collectors.toSet());
+
+ Stream<Call> callsStream = mCalls.stream()
+ .filter(call -> desiredStates.contains(call.getState()) &&
+ call.getParentCall() == null && !call.isExternalCall() &&
+ call.isSelfManaged() == isSelfManaged);
+
+ // If a call to exclude was specifeid, filter it out.
+ if (excludeCall != null) {
+ callsStream = callsStream.filter(call -> call != excludeCall);
}
- return count;
+
+ // If a phone account handle was specified, only consider calls for that phone account.
+ if (phoneAccountHandle != null) {
+ callsStream = callsStream.filter(
+ call -> phoneAccountHandle.equals(call.getTargetPhoneAccount()));
+ }
+
+ return (int) callsStream.count();
}
- private boolean hasMaximumLiveCalls() {
- return MAXIMUM_LIVE_CALLS <= getNumCallsWithState(LIVE_CALL_STATES);
+ private boolean hasMaximumManagedLiveCalls(Call exceptCall) {
+ return MAXIMUM_LIVE_CALLS <= getNumCallsWithState(false /* isSelfManaged */,
+ exceptCall, null /* phoneAccountHandle */, LIVE_CALL_STATES);
}
- private boolean hasMaximumHoldingCalls() {
- return MAXIMUM_HOLD_CALLS <= getNumCallsWithState(CallState.ON_HOLD);
+ private boolean hasMaximumSelfManagedCalls(Call exceptCall,
+ PhoneAccountHandle phoneAccountHandle) {
+ return MAXIMUM_SELF_MANAGED_CALLS <= getNumCallsWithState(true /* isSelfManaged */,
+ exceptCall, phoneAccountHandle, ANY_CALL_STATE);
}
- private boolean hasMaximumRingingCalls() {
- return MAXIMUM_RINGING_CALLS <= getNumCallsWithState(CallState.RINGING);
+ private boolean hasMaximumManagedHoldingCalls(Call exceptCall) {
+ return MAXIMUM_HOLD_CALLS <= getNumCallsWithState(false /* isSelfManaged */, exceptCall,
+ null /* phoneAccountHandle */, CallState.ON_HOLD);
}
- private boolean hasMaximumOutgoingCalls() {
- return MAXIMUM_OUTGOING_CALLS <= getNumCallsWithState(OUTGOING_CALL_STATES);
+ private boolean hasMaximumManagedRingingCalls(Call exceptCall) {
+ return MAXIMUM_RINGING_CALLS <= getNumCallsWithState(false /* isSelfManaged */, exceptCall,
+ null /* phoneAccountHandle */, CallState.RINGING);
}
- private boolean hasMaximumDialingCalls() {
- return MAXIMUM_DIALING_CALLS <= getNumCallsWithState(CallState.DIALING, CallState.PULLING);
+ private boolean hasMaximumSelfManagedRingingCalls(Call exceptCall,
+ PhoneAccountHandle phoneAccountHandle) {
+ return MAXIMUM_RINGING_CALLS <= getNumCallsWithState(true /* isSelfManaged */, exceptCall,
+ phoneAccountHandle, CallState.RINGING);
+ }
+
+ private boolean hasMaximumManagedOutgoingCalls(Call exceptCall) {
+ return MAXIMUM_OUTGOING_CALLS <= getNumCallsWithState(false /* isSelfManaged */, exceptCall,
+ null /* phoneAccountHandle */, OUTGOING_CALL_STATES);
+ }
+
+ private boolean hasMaximumManagedDialingCalls(Call exceptCall) {
+ return MAXIMUM_DIALING_CALLS <= getNumCallsWithState(false /* isSelfManaged */, exceptCall,
+ null /* phoneAccountHandle */, CallState.DIALING, CallState.PULLING);
+ }
+
+ /**
+ * Given a {@link PhoneAccountHandle} determines if there are calls owned by any other
+ * {@link PhoneAccountHandle}.
+ * @param phoneAccountHandle The {@link PhoneAccountHandle} to check.
+ * @return {@code true} if there are other calls, {@code false} otherwise.
+ */
+ public boolean hasCallsForOtherPhoneAccount(PhoneAccountHandle phoneAccountHandle) {
+ return mCalls.stream().filter(call ->
+ !phoneAccountHandle.equals(call.getTargetPhoneAccount()) &&
+ call.getParentCall() == null &&
+ !call.isExternalCall()).count() > 0;
+ }
+
+ /**
+ * Given a {@link PhoneAccountHandle} determines if there are and managed calls.
+ * @return {@code true} if there are managed calls, {@code false} otherwise.
+ */
+ public boolean hasManagedCalls() {
+ return mCalls.stream().filter(call -> !call.isSelfManaged() &&
+ !call.isExternalCall()).count() > 0;
}
private boolean makeRoomForOutgoingCall(Call call, boolean isEmergency) {
- if (hasMaximumLiveCalls()) {
+ if (hasMaximumManagedLiveCalls(call)) {
// NOTE: If the amount of live calls changes beyond 1, this logic will probably
// have to change.
Call liveCall = getFirstCallWithState(LIVE_CALL_STATES);
@@ -1997,7 +2081,7 @@
return true;
}
- if (hasMaximumOutgoingCalls()) {
+ if (hasMaximumManagedOutgoingCalls(call)) {
Call outgoingCall = getFirstCallWithState(OUTGOING_CALL_STATES);
if (isEmergency && !outgoingCall.isEmergencyCall()) {
// Disconnect the current outgoing call if it's not an emergency call. If the
@@ -2019,7 +2103,7 @@
return false;
}
- if (hasMaximumHoldingCalls()) {
+ if (hasMaximumManagedHoldingCalls(call)) {
// There is no more room for any more calls, unless it's an emergency.
if (isEmergency) {
// Kill the current active call, this is easier then trying to disconnect a
@@ -2231,6 +2315,61 @@
new MissedCallNotifier.CallInfoFactory());
}
+ public boolean isIncomingCallPermitted(PhoneAccountHandle phoneAccountHandle) {
+ return isIncomingCallPermitted(null /* excludeCall */, phoneAccountHandle);
+ }
+
+ public boolean isIncomingCallPermitted(Call excludeCall,
+ PhoneAccountHandle phoneAccountHandle) {
+ if (phoneAccountHandle == null) {
+ return false;
+ }
+ PhoneAccount phoneAccount =
+ mPhoneAccountRegistrar.getPhoneAccountUnchecked(phoneAccountHandle);
+ if (phoneAccount == null) {
+ return false;
+ }
+
+ if (!phoneAccount.isSelfManaged()) {
+ return !hasMaximumManagedRingingCalls(excludeCall) &&
+ !hasMaximumManagedHoldingCalls(excludeCall);
+ } else {
+ return !hasEmergencyCall() &&
+ !hasMaximumSelfManagedRingingCalls(excludeCall, phoneAccountHandle) &&
+ !hasMaximumSelfManagedCalls(excludeCall, phoneAccountHandle) &&
+ !hasManagedCalls();
+ }
+ }
+
+ public boolean isOutgoingCallPermitted(PhoneAccountHandle phoneAccountHandle) {
+ return isOutgoingCallPermitted(null /* excludeCall */, phoneAccountHandle);
+ }
+
+ public boolean isOutgoingCallPermitted(Call excludeCall,
+ PhoneAccountHandle phoneAccountHandle) {
+ if (phoneAccountHandle == null) {
+ return false;
+ }
+ PhoneAccount phoneAccount =
+ mPhoneAccountRegistrar.getPhoneAccountUnchecked(phoneAccountHandle);
+ if (phoneAccount == null) {
+ return false;
+ }
+
+ if (!phoneAccount.isSelfManaged()) {
+ return !hasMaximumManagedOutgoingCalls(excludeCall) &&
+ !hasMaximumManagedDialingCalls(excludeCall) &&
+ !hasMaximumManagedLiveCalls(excludeCall) &&
+ !hasMaximumManagedHoldingCalls(excludeCall);
+ } else {
+ // Only permit outgoing calls if there is no ongoing emergency calls and all other calls
+ // are associated with the current PhoneAccountHandle.
+ return !hasEmergencyCall() &&
+ !hasMaximumSelfManagedCalls(excludeCall, phoneAccountHandle) &&
+ !hasCallsForOtherPhoneAccount(phoneAccountHandle);
+ }
+ }
+
/**
* Dumps the state of the {@link CallsManager}.
*
@@ -2314,4 +2453,26 @@
call.setIntentExtras(extras);
}
+
+ /**
+ * Notifies the {@link android.telecom.ConnectionService} associated with a
+ * {@link PhoneAccountHandle} that the attempt to create a new connection has failed.
+ *
+ * @param phoneAccountHandle The {@link PhoneAccountHandle}.
+ * @param call The {@link Call} which could not be added.
+ */
+ private void notifyCreateConnectionFailed(PhoneAccountHandle phoneAccountHandle, Call call) {
+ if (phoneAccountHandle == null) {
+ return;
+ }
+ ConnectionServiceWrapper service = mConnectionServiceRepository.getService(
+ phoneAccountHandle.getComponentName(), phoneAccountHandle.getUserHandle());
+ if (service == null) {
+ Log.i(this, "Found no connection service.");
+ return;
+ } else {
+ call.setConnectionService(service);
+ service.createConnectionFailed(call);
+ }
+ }
}
diff --git a/src/com/android/server/telecom/ConnectionServiceWrapper.java b/src/com/android/server/telecom/ConnectionServiceWrapper.java
index 27af7f4..edb3ee9 100644
--- a/src/com/android/server/telecom/ConnectionServiceWrapper.java
+++ b/src/com/android/server/telecom/ConnectionServiceWrapper.java
@@ -871,6 +871,13 @@
Log.addEvent(call, LogUtils.Events.START_CONNECTION, Log.piiHandle(call.getHandle()));
try {
+ // For self-managed incoming calls, if there is another ongoing call Telecom is
+ // responsible for showing a UI to ask the user if they'd like to answer this
+ // new incoming call.
+ boolean shouldShowIncomingCallUI = call.isSelfManaged() &&
+ !mCallsManager.hasCallsForOtherPhoneAccount(
+ call.getTargetPhoneAccount());
+
mServiceInterface.createConnection(
call.getConnectionManagerPhoneAccount(),
callId,
@@ -880,11 +887,11 @@
extras,
call.getVideoState(),
callId,
- false), // TODO(3pcalls): pass in true/false based on whether ux
- // should show.
+ shouldShowIncomingCallUI),
call.shouldAttachToExistingConnection(),
call.isUnknown(),
Log.getExternalSession());
+
} catch (RemoteException e) {
Log.e(this, e, "Failure to createConnection -- %s", getComponentName());
mPendingResponses.remove(callId).handleCreateConnectionFailure(
@@ -902,6 +909,50 @@
mBinder.bind(callback, call);
}
+ /**
+ * Notifies the {@link ConnectionService} associated with a {@link Call} that the request to
+ * create a connection has been denied or failed.
+ * @param call The call.
+ */
+ void createConnectionFailed(final Call call) {
+ Log.d(this, "createConnectionFailed(%s) via %s.", call, getComponentName());
+ BindCallback callback = new BindCallback() {
+ @Override
+ public void onSuccess() {
+ final String callId = mCallIdMapper.getCallId(call);
+ // If still bound, tell the connection service create connection has failed.
+ if (callId != null && isServiceValid("createConnectionFailed")) {
+ Log.addEvent(call, LogUtils.Events.CREATE_CONNECTION_FAILED,
+ Log.piiHandle(call.getHandle()));
+ try {
+ logOutgoing("createConnectionFailed %s", callId);
+ mServiceInterface.createConnectionFailed(callId,
+ new ConnectionRequest(
+ call.getTargetPhoneAccount(),
+ call.getHandle(),
+ call.getIntentExtras(),
+ call.getVideoState(),
+ callId,
+ false),
+ call.isIncoming(),
+ Log.getExternalSession());
+ call.setDisconnectCause(new DisconnectCause(DisconnectCause.CANCELED));
+ call.disconnect();
+ } catch (RemoteException e) {
+ }
+ }
+ }
+
+ @Override
+ public void onFailure() {
+ // Binding failed. Oh no.
+ Log.w(this, "onFailure - could not bind to CS for call %s", call.getId());
+ }
+ };
+
+ mBinder.bind(callback, call);
+ }
+
/** @see IConnectionService#abort(String, Session.Info) */
void abort(Call call) {
// Clear out any pending outgoing call data
diff --git a/src/com/android/server/telecom/CreateConnectionProcessor.java b/src/com/android/server/telecom/CreateConnectionProcessor.java
index 2b88848..629e949 100644
--- a/src/com/android/server/telecom/CreateConnectionProcessor.java
+++ b/src/com/android/server/telecom/CreateConnectionProcessor.java
@@ -132,8 +132,10 @@
mAttemptRecords.add(new CallAttemptRecord(
mCall.getTargetPhoneAccount(), mCall.getTargetPhoneAccount()));
}
- adjustAttemptsForConnectionManager();
- adjustAttemptsForEmergency(mCall.getTargetPhoneAccount());
+ if (!mCall.isSelfManaged()) {
+ adjustAttemptsForConnectionManager();
+ adjustAttemptsForEmergency(mCall.getTargetPhoneAccount());
+ }
mAttemptRecordIterator = mAttemptRecords.iterator();
attemptNextPhoneAccount();
}
diff --git a/src/com/android/server/telecom/InCallController.java b/src/com/android/server/telecom/InCallController.java
index 8e01d04..891734b 100644
--- a/src/com/android/server/telecom/InCallController.java
+++ b/src/com/android/server/telecom/InCallController.java
@@ -654,6 +654,11 @@
@Override
public void onCallAdded(Call call) {
+ if (call.isSelfManaged()) {
+ Log.i(this, "onCallAdded: skipped self-managed %s", call);
+ return;
+ }
+
if (!isBoundToServices()) {
bindToServices(call);
} else {
@@ -688,6 +693,11 @@
@Override
public void onCallRemoved(Call call) {
+ if (call.isSelfManaged()) {
+ Log.i(this, "onCallRemoved: skipped self-managed %s", call);
+ return;
+ }
+
Log.i(this, "onCallRemoved: %s", call);
if (mCallsManager.getCalls().isEmpty()) {
/** Let's add a 2 second delay before we send unbind to the services to hopefully
@@ -711,6 +721,11 @@
@Override
public void onExternalCallChanged(Call call, boolean isExternalCall) {
+ if (call.isSelfManaged()) {
+ Log.i(this, "onExternalCallChanged: skipped self-managed %s", call);
+ return;
+ }
+
Log.i(this, "onExternalCallChanged: %s -> %b", call, isExternalCall);
List<ComponentName> componentsUpdated = new ArrayList<>();
@@ -772,6 +787,11 @@
@Override
public void onCallStateChanged(Call call, int oldState, int newState) {
+ if (call.isSelfManaged()) {
+ Log.i(this, "onExternalCallChanged: skipped self-managed %s", call);
+ return;
+ }
+
updateCall(call);
}
@@ -780,6 +800,11 @@
Call call,
ConnectionServiceWrapper oldService,
ConnectionServiceWrapper newService) {
+ if (call.isSelfManaged()) {
+ Log.i(this, "onConnectionServiceChanged: skipped self-managed %s", call);
+ return;
+ }
+
updateCall(call);
}
@@ -825,6 +850,11 @@
@Override
public void onIsConferencedChanged(Call call) {
+ if (call.isSelfManaged()) {
+ Log.i(this, "onIsConferencedChanged: skipped self-managed %s", call);
+ return;
+ }
+
Log.d(this, "onIsConferencedChanged %s", call);
updateCall(call);
}
@@ -1119,7 +1149,8 @@
int numCallsSent = 0;
for (Call call : calls) {
try {
- if (call.isExternalCall() && !info.isExternalCallsSupported()) {
+ if (call.isSelfManaged() ||
+ (call.isExternalCall() && !info.isExternalCallsSupported())) {
continue;
}
diff --git a/src/com/android/server/telecom/LogUtils.java b/src/com/android/server/telecom/LogUtils.java
index 69ba0ff..121abcf 100644
--- a/src/com/android/server/telecom/LogUtils.java
+++ b/src/com/android/server/telecom/LogUtils.java
@@ -80,6 +80,7 @@
public static final String START_CALL_WAITING_TONE = "START_CALL_WAITING_TONE";
public static final String STOP_CALL_WAITING_TONE = "STOP_CALL_WAITING_TONE";
public static final String START_CONNECTION = "START_CONNECTION";
+ public static final String CREATE_CONNECTION_FAILED = "CREATE_CONNECTION_FAILED";
public static final String BIND_CS = "BIND_CS";
public static final String CS_BOUND = "CS_BOUND";
public static final String CONFERENCE_WITH = "CONF_WITH";
diff --git a/src/com/android/server/telecom/TelecomServiceImpl.java b/src/com/android/server/telecom/TelecomServiceImpl.java
index fc1eca6..6d5af5b 100644
--- a/src/com/android/server/telecom/TelecomServiceImpl.java
+++ b/src/com/android/server/telecom/TelecomServiceImpl.java
@@ -24,6 +24,7 @@
import static android.Manifest.permission.REGISTER_SIM_SUBSCRIPTION;
import static android.Manifest.permission.WRITE_SECURE_SETTINGS;
+import android.Manifest;
import android.app.ActivityManager;
import android.app.AppOpsManager;
import android.content.ComponentName;
@@ -952,6 +953,13 @@
enforceUserHandleMatchesCaller(phoneAccountHandle);
enforcePhoneAccountIsRegisteredEnabled(phoneAccountHandle,
Binder.getCallingUserHandle());
+ if (isSelfManagedConnectionService(phoneAccountHandle)) {
+ // Self-managed phone account, ensure it has MANAGE_OWN_CALLS.
+ mContext.enforceCallingOrSelfPermission(
+ android.Manifest.permission.MANAGE_OWN_CALLS,
+ "Self-managed phone accounts must have MANAGE_OWN_CALLS " +
+ "permission.");
+ }
}
long token = Binder.clearCallingIdentity();
try {
@@ -1030,7 +1038,18 @@
try {
Log.startSession("TSI.pC");
enforceCallingPackage(callingPackage);
- if (!canCallPhone(callingPackage, "placeCall")) {
+
+ PhoneAccountHandle phoneAccountHandle = null;
+ if (extras != null) {
+ phoneAccountHandle = extras.getParcelable(
+ TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE);
+ }
+ boolean isSelfManaged = phoneAccountHandle != null &&
+ isSelfManagedConnectionService(phoneAccountHandle);
+ if (isSelfManaged) {
+ mContext.enforceCallingOrSelfPermission(Manifest.permission.MANAGE_OWN_CALLS,
+ "Self-managed ConnectionServices require MANAGE_OWN_CALLS permission.");
+ } else if (!canCallPhone(callingPackage, "placeCall")) {
throw new SecurityException("Package " + callingPackage
+ " is not allowed to place phone calls");
}
@@ -1059,7 +1078,8 @@
}
mUserCallIntentProcessorFactory.create(mContext, userHandle)
.processIntent(
- intent, callingPackage, hasCallAppOp && hasCallPermission);
+ intent, callingPackage, isSelfManaged ||
+ (hasCallAppOp && hasCallPermission));
} finally {
Binder.restoreCallingIdentity(token);
}
@@ -1194,7 +1214,7 @@
synchronized (mLock) {
long token = Binder.clearCallingIdentity();
try {
- // TODO(3pcalls) - Implement body.
+ return mCallsManager.isIncomingCallPermitted(phoneAccountHandle);
} finally {
Binder.restoreCallingIdentity(token);
}
@@ -1202,7 +1222,6 @@
} finally {
Log.endSession();
}
- return true;
}
/**
@@ -1216,7 +1235,7 @@
synchronized (mLock) {
long token = Binder.clearCallingIdentity();
try {
- // TODO(3pcalls) - Implement body.
+ return mCallsManager.isOutgoingCallPermitted(phoneAccountHandle);
} finally {
Binder.restoreCallingIdentity(token);
}
@@ -1224,8 +1243,6 @@
} finally {
Log.endSession();
}
-
- return true;
}
};
@@ -1454,6 +1471,15 @@
}
}
+ private boolean isSelfManagedConnectionService(PhoneAccountHandle phoneAccountHandle) {
+ if (phoneAccountHandle != null) {
+ PhoneAccount phoneAccount = mPhoneAccountRegistrar.getPhoneAccountUnchecked(
+ phoneAccountHandle);
+ return phoneAccount.isSelfManaged();
+ }
+ return false;
+ }
+
private boolean canCallPhone(String callingPackage, String message) {
// The system/default dialer can always read phone state - so that emergency calls will
// still work.
diff --git a/testapps/AndroidManifest.xml b/testapps/AndroidManifest.xml
index c1eb258..6c7202b 100644
--- a/testapps/AndroidManifest.xml
+++ b/testapps/AndroidManifest.xml
@@ -25,6 +25,7 @@
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.CALL_PHONE" />
<uses-permission android:name="android.permission.CONTROL_INCALL_EXPERIENCE" />
+ <uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />
<uses-permission android:name="android.permission.READ_CALL_LOG" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.REGISTER_CALL_PROVIDER" />
@@ -155,6 +156,24 @@
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
+ </activity>
+
+ <activity android:name="com.android.server.telecom.testapps.SelfManagedCallingActivity"
+ android:label="@string/selfManagedCallingActivityLabel"
+ android:process="com.android.server.telecom.testapps.SelfMangingCallingApp">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
</activity>
+
+ <service android:name="com.android.server.telecom.testapps.SelfManagedConnectionService"
+ android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE"
+ android:process="com.android.server.telecom.testapps.SelfMangingCallingApp">
+ <intent-filter>
+ <action android:name="android.telecom.ConnectionService" />
+ </intent-filter>
+ </service>
</application>
</manifest>
diff --git a/testapps/res/layout/self_managed_call_list_item.xml b/testapps/res/layout/self_managed_call_list_item.xml
new file mode 100644
index 0000000..f3be4ad
--- /dev/null
+++ b/testapps/res/layout/self_managed_call_list_item.xml
@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+ <TextView
+ android:id="@+id/phoneNumber"
+ android:layout_gravity="left"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textSize="25dp"
+ android:text="TextView" />
+ <TextView
+ android:id="@+id/callState"
+ android:layout_gravity="left"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textSize="25dp"
+ android:text="TextView" />
+ <LinearLayout
+ android:orientation="horizontal"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content">
+ <Button
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Active"
+ android:id="@+id/setActiveButton" />
+ <Button
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Hold"
+ android:id="@+id/setHeldButton" />
+ <Button
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Disconnect"
+ android:id="@+id/disconnectButton" />
+ </LinearLayout>
+ <LinearLayout
+ android:orientation="horizontal"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content">
+ <Button
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Speaker"
+ android:id="@+id/speakerButton" />
+ <Button
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Earpiece"
+ android:id="@+id/earpieceButton" />
+ </LinearLayout>
+</LinearLayout>
\ No newline at end of file
diff --git a/testapps/res/layout/self_managed_sample_main.xml b/testapps/res/layout/self_managed_sample_main.xml
new file mode 100644
index 0000000..f239997
--- /dev/null
+++ b/testapps/res/layout/self_managed_sample_main.xml
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent">
+ <CheckBox
+ android:id="@+id/checkIfPermittedBeforeCalling"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/checkIfPermittedBeforeCallingButton" />
+ <TableLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content">
+ <TableRow android:layout_span="2">
+ <RadioGroup>
+ <RadioButton
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Acct 1"
+ android:id="@+id/useAcct1Button"/>
+ <RadioButton
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Acct 2"
+ android:id="@+id/useAcct2Button"/>
+ </RadioGroup>
+ </TableRow>
+ <TableRow android:layout_span="2">
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Place call:" />
+ </TableRow>
+ <TableRow>
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Number:" />
+ <EditText
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:id="@+id/phoneNumber" />
+ </TableRow>
+ <TableRow>
+ <Button
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Outgoing Call"
+ android:id="@+id/placeOutgoingCallButton" />
+ <Button
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Incoming Call"
+ android:id="@+id/placeIncomingCallButton" />
+ </TableRow>
+ <TableRow android:layout_span="2">
+ <ListView
+ android:id="@+id/callList"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:listSelector="@null"
+ android:divider="@null" />
+ </TableRow>
+ </TableLayout>
+</LinearLayout>
\ No newline at end of file
diff --git a/testapps/res/raw/sample_audio.ogg b/testapps/res/raw/sample_audio.ogg
new file mode 100644
index 0000000..0129b46
--- /dev/null
+++ b/testapps/res/raw/sample_audio.ogg
Binary files differ
diff --git a/testapps/res/raw/sample_audio2.ogg b/testapps/res/raw/sample_audio2.ogg
new file mode 100644
index 0000000..a0b39b4
--- /dev/null
+++ b/testapps/res/raw/sample_audio2.ogg
Binary files differ
diff --git a/testapps/res/values/donottranslate_strings.xml b/testapps/res/values/donottranslate_strings.xml
index 83d1ef3..74a3fd4 100644
--- a/testapps/res/values/donottranslate_strings.xml
+++ b/testapps/res/values/donottranslate_strings.xml
@@ -49,4 +49,17 @@
<string name="UssdUiAppLabel">Test Ussd UI</string>
<string name="placeUssdButton">Send USSD</string>
+
+ <!-- String for button in SelfManagedCallingActivity. -->
+ <string name="checkIfPermittedBeforeCallingButton">Check if calls permitted before calling</string>
+
+ <string name="selfManagedCallingActivityLabel">Self-Managed Sample</string>
+
+ <string name="outgoingCallNotPermitted">Outgoing call not permitted.</string>
+
+ <string name="outgoingCallNotPermittedCS">Outgoing call not permitted (CS Reported).</string>
+
+ <string name="incomingCallNotPermitted">Incoming call not permitted.</string>
+
+ <string name="incomingCallNotPermittedCS">Incoming call not permitted (CS Reported).</string>
</resources>
diff --git a/testapps/src/com/android/server/telecom/testapps/SelfManagedCallList.java b/testapps/src/com/android/server/telecom/testapps/SelfManagedCallList.java
new file mode 100644
index 0000000..e0d7442
--- /dev/null
+++ b/testapps/src/com/android/server/telecom/testapps/SelfManagedCallList.java
@@ -0,0 +1,133 @@
+/*
+ * 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 com.android.server.telecom.testapps;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.net.Uri;
+import android.telecom.ConnectionRequest;
+import android.telecom.Log;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import android.util.ArrayMap;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Manages the list of {@link SelfManagedConnection} active in the sample third-party calling app.
+ */
+public class SelfManagedCallList {
+ public abstract static class Listener {
+ public void onCreateIncomingConnectionFailed(ConnectionRequest request) {};
+ public void onCreateOutgoingConnectionFailed(ConnectionRequest request) {};
+ public void onConnectionListChanged() {};
+ }
+
+ public static String SELF_MANAGED_ACCOUNT_1 = "1";
+ public static String SELF_MANAGED_ACCOUNT_2 = "2";
+
+ private static SelfManagedCallList sInstance;
+ private static ComponentName COMPONENT_NAME = new ComponentName(
+ SelfManagedCallList.class.getPackage().getName(),
+ SelfManagedConnectionService.class.getName());
+ private static Uri SELF_MANAGED_ADDRESS_1 = Uri.fromParts(PhoneAccount.SCHEME_TEL, "555-1212",
+ "");
+ private static Uri SELF_MANAGED_ADDRESS_2 = Uri.fromParts(PhoneAccount.SCHEME_SIP,
+ "me@test.org", "");
+ private static Map<String, PhoneAccountHandle> mPhoneAccounts = new ArrayMap();
+
+ public static SelfManagedCallList getInstance() {
+ if (sInstance == null) {
+ sInstance = new SelfManagedCallList();
+ }
+ return sInstance;
+ }
+
+ private Listener mListener;
+
+ private List<SelfManagedConnection> mConnections = new ArrayList<>();
+
+ public void setListener(Listener listener) {
+ mListener = listener;
+ }
+
+ public void registerPhoneAccounts(Context context) {
+ registerPhoneAccount(context, SELF_MANAGED_ACCOUNT_1, SELF_MANAGED_ADDRESS_1);
+ registerPhoneAccount(context, SELF_MANAGED_ACCOUNT_2, SELF_MANAGED_ADDRESS_2);
+ }
+
+ public void registerPhoneAccount(Context context, String id, Uri address) {
+ PhoneAccountHandle handle = new PhoneAccountHandle(COMPONENT_NAME, id);
+ mPhoneAccounts.put(id, handle);
+ PhoneAccount.Builder builder = PhoneAccount.builder(handle, id)
+ .addSupportedUriScheme(PhoneAccount.SCHEME_TEL)
+ .addSupportedUriScheme(PhoneAccount.SCHEME_SIP)
+ .setAddress(address)
+ .setCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED |
+ PhoneAccount.CAPABILITY_VIDEO_CALLING |
+ PhoneAccount.CAPABILITY_SUPPORTS_VIDEO_CALLING);
+
+ TelecomManager.from(context).registerPhoneAccount(builder.build());
+ }
+
+ public PhoneAccountHandle getPhoneAccountHandle(String id) {
+ return mPhoneAccounts.get(id);
+ }
+
+ public void notifyCreateIncomingConnectionFailed(ConnectionRequest request) {
+ if (mListener != null) {
+ mListener.onCreateIncomingConnectionFailed(request);
+ }
+ }
+
+ public void notifyCreateOutgoingConnectionFailed(ConnectionRequest request) {
+ if (mListener != null) {
+ mListener.onCreateOutgoingConnectionFailed(request);
+ }
+ }
+
+ public void addConnection(SelfManagedConnection connection) {
+ Log.i(this, "addConnection %s", connection);
+ mConnections.add(connection);
+ if (mListener != null) {
+ Log.i(this, "addConnection calling onConnectionListChanged %s", connection);
+ mListener.onConnectionListChanged();
+ }
+ }
+
+ public void removeConnection(SelfManagedConnection connection) {
+ Log.i(this, "removeConnection %s", connection);
+ mConnections.remove(connection);
+ if (mListener != null) {
+ Log.i(this, "removeConnection calling onConnectionListChanged %s", connection);
+ mListener.onConnectionListChanged();
+ }
+ }
+
+ public List<SelfManagedConnection> getConnections() {
+ return mConnections;
+ }
+
+ public void notifyCallModified() {
+ if (mListener != null) {
+ mListener.onConnectionListChanged();
+ }
+ }
+}
diff --git a/testapps/src/com/android/server/telecom/testapps/SelfManagedCallListAdapter.java b/testapps/src/com/android/server/telecom/testapps/SelfManagedCallListAdapter.java
new file mode 100644
index 0000000..29d3c5f
--- /dev/null
+++ b/testapps/src/com/android/server/telecom/testapps/SelfManagedCallListAdapter.java
@@ -0,0 +1,189 @@
+/*
+ * 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 com.android.server.telecom.testapps;
+
+import android.telecom.CallAudioState;
+import android.telecom.DisconnectCause;
+import android.telecom.PhoneAccountHandle;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.TextView;
+
+import java.util.List;
+
+public class SelfManagedCallListAdapter extends BaseAdapter {
+
+ private static final String TAG = "SelfMgCallListAd";
+ /**
+ * Listener used to handle tap of the "disconnect" button for a connection.
+ */
+ private View.OnClickListener mDisconnectListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ View parent = (View) v.getParent().getParent();
+ SelfManagedConnection connection = (SelfManagedConnection) parent.getTag();
+ connection.setConnectionDisconnected(DisconnectCause.LOCAL);
+ SelfManagedCallList.getInstance().removeConnection(connection);
+ }
+ };
+
+ /**
+ * Listener used to handle tap of the "active" button for a connection.
+ */
+ private View.OnClickListener mActiveListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ View parent = (View) v.getParent().getParent();
+ SelfManagedConnection connection = (SelfManagedConnection) parent.getTag();
+ connection.setConnectionActive();
+ notifyDataSetChanged();
+ }
+ };
+
+ /**
+ * Listener used to handle tap of the "held" button for a connection.
+ */
+ private View.OnClickListener mHeldListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ View parent = (View) v.getParent().getParent();
+ SelfManagedConnection connection = (SelfManagedConnection) parent.getTag();
+ connection.setConnectionHeld();
+ notifyDataSetChanged();
+ }
+ };
+
+ private View.OnClickListener mSpeakerListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ View parent = (View) v.getParent().getParent();
+ SelfManagedConnection connection = (SelfManagedConnection) parent.getTag();
+ connection.setAudioRoute(CallAudioState.ROUTE_SPEAKER);
+ notifyDataSetChanged();
+ }
+ };
+
+ private View.OnClickListener mEarpieceListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ View parent = (View) v.getParent().getParent();
+ SelfManagedConnection connection = (SelfManagedConnection) parent.getTag();
+ connection.setAudioRoute(CallAudioState.ROUTE_EARPIECE);
+ notifyDataSetChanged();
+ }
+ };
+
+ private final LayoutInflater mLayoutInflater;
+
+ private List<SelfManagedConnection> mConnections;
+
+ public SelfManagedCallListAdapter(LayoutInflater layoutInflater,
+ List<SelfManagedConnection> connections) {
+
+ mLayoutInflater = layoutInflater;
+ mConnections = connections;
+ }
+
+ @Override
+ public int getCount() {
+ return mConnections.size();
+ }
+
+ @Override
+ public Object getItem(int position) {
+ return mConnections.get(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ View result = convertView == null
+ ? mLayoutInflater.inflate(R.layout.self_managed_call_list_item, parent, false)
+ : convertView;
+ SelfManagedConnection connection = mConnections.get(position);
+ PhoneAccountHandle phoneAccountHandle = connection.getExtras().getParcelable(
+ SelfManagedConnection.EXTRA_PHONE_ACCOUNT_HANDLE);
+ CallAudioState audioState = connection.getCallAudioState();
+ String audioRoute = "?";
+ if (audioState != null) {
+ switch (audioState.getRoute()) {
+ case CallAudioState.ROUTE_BLUETOOTH:
+ audioRoute = "BT";
+ break;
+ case CallAudioState.ROUTE_SPEAKER:
+ audioRoute = "\uD83D\uDD0A";
+ break;
+ case CallAudioState.ROUTE_EARPIECE:
+ audioRoute = "\uD83D\uDC42";
+ break;
+ case CallAudioState.ROUTE_WIRED_HEADSET:
+ audioRoute = "\uD83D\uDD0C";
+ break;
+ default:
+ audioRoute = "?";
+ break;
+ }
+ }
+ String callType;
+ if (connection.isIncomingCall()) {
+ if (connection.isIncomingCallUiShowing()) {
+ callType = "Incoming - Show Incoming UX";
+ } else {
+ callType = "Incoming - DO NOT SHOW Incoming UX";
+ }
+ } else {
+ callType = "Outgoing";
+ }
+ setInfoForRow(result, phoneAccountHandle.getId(), connection.getAddress().toString(),
+ android.telecom.Connection.stateToString(connection.getState()), audioRoute,
+ callType);
+ result.setTag(connection);
+ return result;
+ }
+
+ public void updateConnections() {
+ Log.i(TAG, "updateConnections "+ mConnections.size());
+
+ notifyDataSetChanged();
+ }
+
+ private void setInfoForRow(View view, String accountName, String number,
+ String status, String audioRoute, String callType) {
+
+ TextView numberTextView = (TextView) view.findViewById(R.id.phoneNumber);
+ TextView statusTextView = (TextView) view.findViewById(R.id.callState);
+ View activeButton = view.findViewById(R.id.setActiveButton);
+ activeButton.setOnClickListener(mActiveListener);
+ View disconnectButton = view.findViewById(R.id.disconnectButton);
+ disconnectButton.setOnClickListener(mDisconnectListener);
+ View setHeldButton = view.findViewById(R.id.setHeldButton);
+ setHeldButton.setOnClickListener(mHeldListener);
+ View speakerButton = view.findViewById(R.id.speakerButton);
+ speakerButton.setOnClickListener(mSpeakerListener);
+ View earpieceButton = view.findViewById(R.id.earpieceButton);
+ earpieceButton.setOnClickListener(mEarpieceListener);
+ numberTextView.setText(accountName + " - " + number + " (" + audioRoute + ")");
+ statusTextView.setText(callType + " - Status: " + status);
+ }
+}
diff --git a/testapps/src/com/android/server/telecom/testapps/SelfManagedCallingActivity.java b/testapps/src/com/android/server/telecom/testapps/SelfManagedCallingActivity.java
new file mode 100644
index 0000000..8b7eae0
--- /dev/null
+++ b/testapps/src/com/android/server/telecom/testapps/SelfManagedCallingActivity.java
@@ -0,0 +1,150 @@
+/*
+ * 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 com.android.server.telecom.testapps;
+
+import android.app.Activity;
+import android.net.Uri;
+import android.os.Bundle;
+import android.telecom.ConnectionRequest;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import android.util.Log;
+import android.view.View;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.EditText;
+import android.widget.ListView;
+import android.widget.RadioButton;
+import android.widget.Toast;
+
+import com.android.server.telecom.testapps.R;
+
+import java.util.Objects;
+
+/**
+ * Provides a sample third-party calling app UX which implements the self managed connection service
+ * APIs.
+ */
+public class SelfManagedCallingActivity extends Activity {
+ private static final String TAG = "SelfMgCallActivity";
+ private SelfManagedCallList mCallList = SelfManagedCallList.getInstance();
+ private CheckBox mCheckIfPermittedBeforeCalling;
+ private Button mPlaceOutgoingCallButton;
+ private Button mPlaceIncomingCallButton;
+ private RadioButton mUseAcct1Button;
+ private RadioButton mUseAcct2Button;
+ private EditText mNumber;
+ private ListView mListView;
+ private SelfManagedCallListAdapter mListAdapter;
+
+ private SelfManagedCallList.Listener mCallListListener = new SelfManagedCallList.Listener() {
+ @Override
+ public void onCreateIncomingConnectionFailed(ConnectionRequest request) {
+ Log.i(TAG, "onCreateIncomingConnectionFailed " + request);
+ Toast.makeText(SelfManagedCallingActivity.this,
+ R.string.incomingCallNotPermittedCS , Toast.LENGTH_SHORT).show();
+ };
+
+ @Override
+ public void onCreateOutgoingConnectionFailed(ConnectionRequest request) {
+ Log.i(TAG, "onCreateOutgoingConnectionFailed " + request);
+ Toast.makeText(SelfManagedCallingActivity.this,
+ R.string.outgoingCallNotPermittedCS , Toast.LENGTH_SHORT).show();
+ };
+
+ @Override
+ public void onConnectionListChanged() {
+ Log.i(TAG, "onConnectionListChanged");
+ mListAdapter.updateConnections();
+ };
+ };
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.self_managed_sample_main);
+ mCheckIfPermittedBeforeCalling = (CheckBox) findViewById(
+ R.id.checkIfPermittedBeforeCalling);
+ mPlaceOutgoingCallButton = (Button) findViewById(R.id.placeOutgoingCallButton);
+ mPlaceOutgoingCallButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ placeOutgoingCall();
+ }
+ });
+ mPlaceIncomingCallButton = (Button) findViewById(R.id.placeIncomingCallButton);
+ mPlaceIncomingCallButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ placeIncomingCall();
+ }
+ });
+ mUseAcct1Button = (RadioButton) findViewById(R.id.useAcct1Button);
+ mUseAcct2Button = (RadioButton) findViewById(R.id.useAcct2Button);
+ mNumber = (EditText) findViewById(R.id.phoneNumber);
+ mListView = (ListView) findViewById(R.id.callList);
+ mCallList.setListener(mCallListListener);
+ mCallList.registerPhoneAccounts(this);
+ mListAdapter = new SelfManagedCallListAdapter(getLayoutInflater(),
+ mCallList.getConnections());
+ mListView.setAdapter(mListAdapter);
+ Log.i(TAG, "onCreate - mCallList id " + Objects.hashCode(mCallList));
+ }
+
+ private PhoneAccountHandle getSelectedPhoneAccountHandle() {
+ if (mUseAcct1Button.isChecked()) {
+ return mCallList.getPhoneAccountHandle(SelfManagedCallList.SELF_MANAGED_ACCOUNT_1);
+ } else if (mUseAcct2Button.isChecked()) {
+ return mCallList.getPhoneAccountHandle(SelfManagedCallList.SELF_MANAGED_ACCOUNT_2);
+ }
+ return null;
+ }
+
+ private void placeOutgoingCall() {
+ TelecomManager tm = TelecomManager.from(this);
+ PhoneAccountHandle phoneAccountHandle = getSelectedPhoneAccountHandle();
+
+ if (mCheckIfPermittedBeforeCalling.isChecked()) {
+ if (!tm.isOutgoingCallPermitted(phoneAccountHandle)) {
+ Toast.makeText(this, R.string.outgoingCallNotPermitted , Toast.LENGTH_SHORT).show();
+ return;
+ }
+ }
+
+ Bundle extras = new Bundle();
+ extras.putParcelable(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE,
+ getSelectedPhoneAccountHandle());
+ tm.placeCall(Uri.parse(mNumber.getText().toString()), extras);
+ }
+
+ private void placeIncomingCall() {
+ TelecomManager tm = TelecomManager.from(this);
+ PhoneAccountHandle phoneAccountHandle = getSelectedPhoneAccountHandle();
+
+ if (mCheckIfPermittedBeforeCalling.isChecked()) {
+ if (!tm.isIncomingCallPermitted(phoneAccountHandle)) {
+ Toast.makeText(this, R.string.incomingCallNotPermitted , Toast.LENGTH_SHORT).show();
+ return;
+ }
+ }
+
+ Bundle extras = new Bundle();
+ extras.putParcelable(TelecomManager.EXTRA_INCOMING_CALL_ADDRESS,
+ Uri.parse(mNumber.getText().toString()));
+ tm.addNewIncomingCall(getSelectedPhoneAccountHandle(), extras);
+ }
+}
\ No newline at end of file
diff --git a/testapps/src/com/android/server/telecom/testapps/SelfManagedConnection.java b/testapps/src/com/android/server/telecom/testapps/SelfManagedConnection.java
new file mode 100644
index 0000000..5fd8eba
--- /dev/null
+++ b/testapps/src/com/android/server/telecom/testapps/SelfManagedConnection.java
@@ -0,0 +1,89 @@
+/*
+ * 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 com.android.server.telecom.testapps;
+
+import android.content.Context;
+import android.media.MediaPlayer;
+import android.telecom.CallAudioState;
+import android.telecom.Connection;
+import android.telecom.ConnectionService;
+import android.telecom.DisconnectCause;
+
+/**
+ * Sample self-managed {@link Connection} for a self-managed {@link ConnectionService}.
+ * <p>
+ * See {@link android.telecom} for more information on self-managed {@link ConnectionService}s.
+ */
+public class SelfManagedConnection extends Connection {
+ public static final String EXTRA_PHONE_ACCOUNT_HANDLE =
+ "com.android.server.telecom.testapps.extra.PHONE_ACCOUNT_HANDLE";
+
+ private final SelfManagedCallList mCallList;
+ private final MediaPlayer mMediaPlayer;
+ private final boolean mIsIncomingCall;
+ private boolean mIsIncomingCallUiShowing;
+
+ SelfManagedConnection(SelfManagedCallList callList, Context context, boolean isIncoming) {
+ mCallList = callList;
+ mMediaPlayer = createMediaPlayer(context);
+ mIsIncomingCall = isIncoming;
+ }
+
+ /**
+ * Handles updates to the audio state of the connection.
+ * @param state The new connection audio state.
+ */
+ @Override
+ public void onCallAudioStateChanged(CallAudioState state) {
+ mCallList.notifyCallModified();
+ }
+
+ public void setConnectionActive() {
+ mMediaPlayer.start();
+ setActive();
+ }
+
+ public void setConnectionHeld() {
+ mMediaPlayer.pause();
+ setOnHold();
+ }
+
+ public void setConnectionDisconnected(int cause) {
+ mMediaPlayer.stop();
+ setDisconnected(new DisconnectCause(cause));
+ destroy();
+ }
+
+ public void setIsIncomingCallUiShowing(boolean showing) {
+ mIsIncomingCallUiShowing = showing;
+ }
+
+ public boolean isIncomingCallUiShowing() {
+ return mIsIncomingCallUiShowing;
+ }
+
+ public boolean isIncomingCall() {
+ return mIsIncomingCall;
+ }
+
+ private MediaPlayer createMediaPlayer(Context context) {
+ int audioToPlay = (Math.random() > 0.5f) ? R.raw.sample_audio : R.raw.sample_audio2;
+ MediaPlayer mediaPlayer = MediaPlayer.create(context, audioToPlay);
+ mediaPlayer.setLooping(true);
+ return mediaPlayer;
+ }
+}
diff --git a/testapps/src/com/android/server/telecom/testapps/SelfManagedConnectionService.java b/testapps/src/com/android/server/telecom/testapps/SelfManagedConnectionService.java
new file mode 100644
index 0000000..31bbcd6
--- /dev/null
+++ b/testapps/src/com/android/server/telecom/testapps/SelfManagedConnectionService.java
@@ -0,0 +1,83 @@
+/*
+ * 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 com.android.server.telecom.testapps;
+
+import android.os.Bundle;
+import android.telecom.Connection;
+import android.telecom.ConnectionRequest;
+import android.telecom.ConnectionService;
+import android.telecom.Log;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+
+import java.util.Objects;
+
+/**
+ * Sample implementation of the self-managed {@link ConnectionService} API.
+ * <p>
+ * See {@link android.telecom} for more information on self-managed {@link ConnectionService}s.
+ */
+public class SelfManagedConnectionService extends ConnectionService {
+ private final SelfManagedCallList mCallList = SelfManagedCallList.getInstance();
+
+ @Override
+ public Connection onCreateOutgoingConnection(
+ PhoneAccountHandle connectionManagerAccount,
+ final ConnectionRequest request) {
+
+ return createSelfManagedConnection(request, false);
+ }
+
+ @Override
+ public Connection onCreateIncomingConnection(PhoneAccountHandle connectionManagerPhoneAccount,
+ ConnectionRequest request) {
+ return createSelfManagedConnection(request, true);
+ }
+
+ @Override
+ public void onCreateIncomingConnectionFailed(ConnectionRequest request) {
+ mCallList.notifyCreateIncomingConnectionFailed(request);
+ }
+
+ @Override
+ public void onCreateOutgoingConnectionFailed(ConnectionRequest request) {
+ mCallList.notifyCreateOutgoingConnectionFailed(request);
+ }
+
+
+ private Connection createSelfManagedConnection(ConnectionRequest request, boolean isIncoming) {
+ SelfManagedConnection connection = new SelfManagedConnection(mCallList,
+ getApplicationContext(), isIncoming);
+ connection.setConnectionProperties(Connection.PROPERTY_SELF_MANAGED);
+ connection.setAddress(request.getAddress(), TelecomManager.PRESENTATION_ALLOWED);
+ connection.setExtras(request.getExtras());
+ if (isIncoming) {
+ connection.setIsIncomingCallUiShowing(request.shouldShowIncomingCallUi());
+ }
+
+ // Track the phone account handle which created this connection so we can distinguish them
+ // in the sample call list later.
+ Bundle moreExtras = new Bundle();
+ moreExtras.putParcelable(SelfManagedConnection.EXTRA_PHONE_ACCOUNT_HANDLE,
+ request.getAccountHandle());
+ connection.putExtras(moreExtras);
+ connection.setVideoState(request.getVideoState());
+ Log.i(this, "createSelfManagedConnection %s", connection);
+ mCallList.addConnection(connection);
+ return connection;
+ }
+}
diff --git a/tests/src/com/android/server/telecom/tests/ConnectionServiceFixture.java b/tests/src/com/android/server/telecom/tests/ConnectionServiceFixture.java
index 59c9468..9907ca7 100644
--- a/tests/src/com/android/server/telecom/tests/ConnectionServiceFixture.java
+++ b/tests/src/com/android/server/telecom/tests/ConnectionServiceFixture.java
@@ -216,7 +216,7 @@
public void createConnection(PhoneAccountHandle connectionManagerPhoneAccount,
String id, ConnectionRequest request, boolean isIncoming, boolean isUnknown,
Session.Info info) throws RemoteException {
- Log.i(ConnectionServiceFixture.this, "xoxox createConnection --> " + id);
+ Log.i(ConnectionServiceFixture.this, "createConnection --> " + id);
if (mConnectionById.containsKey(id)) {
throw new RuntimeException("Connection already exists: " + id);
@@ -239,6 +239,18 @@
}
@Override
+ public void createConnectionFailed(String callId, ConnectionRequest request,
+ boolean isIncoming, Session.Info sessionInfo) throws RemoteException {
+ Log.i(ConnectionServiceFixture.this, "createConnectionFailed --> " + callId);
+
+ if (mConnectionById.containsKey(callId)) {
+ throw new RuntimeException("Connection already exists: " + callId);
+ }
+
+ // TODO(3p-calls): Implement this.
+ }
+
+ @Override
public void abort(String callId, Session.Info info) throws RemoteException { }
@Override