Telecom Transactional APIs Implementation
Transactional APIs are defined as APIs that use
android.os.OutcomeReceivers. The receivers are to be completed by
Telecom for CallControl opertaions and the Client for CallEventCallback
operations. Doing so ensures the client and telecom are in sync with
call states and that operations can be completed on both ends.
bug: 249779561
Test: unit + CTS
Change-Id: Ib4cc7c7b05491e4f61c011a5d7af5bc24125d34d
diff --git a/src/com/android/server/telecom/Call.java b/src/com/android/server/telecom/Call.java
index b88711e..2a65afe 100644
--- a/src/com/android/server/telecom/Call.java
+++ b/src/com/android/server/telecom/Call.java
@@ -29,9 +29,11 @@
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
+import android.os.OutcomeReceiver;
import android.os.ParcelFileDescriptor;
import android.os.Parcelable;
import android.os.RemoteException;
+import android.os.ResultReceiver;
import android.os.SystemClock;
import android.os.Trace;
import android.os.UserHandle;
@@ -92,7 +94,6 @@
* from the time the call intent was received by Telecom (vs. the time the call was
* connected etc).
*/
-@VisibleForTesting
public class Call implements CreateConnectionResponse, EventManager.Loggable,
ConnectionServiceFocusManager.CallFocus {
public final static String CALL_ID_UNKNOWN = "-1";
@@ -381,6 +382,8 @@
*/
private ConnectionServiceWrapper mConnectionService;
+ private TransactionalServiceWrapper mTransactionalService;
+
private boolean mIsEmergencyCall;
// The Call is considered an emergency call for testing, but will not actually connect to
@@ -529,6 +532,8 @@
*/
private boolean mIsSelfManaged = false;
+ private boolean mIsTransactionalCall = false;
+
/**
* Indicates whether the {@link PhoneAccount} associated with an self-managed call want to
* expose the call to an {@link android.telecom.InCallService} which declares the metadata
@@ -1038,7 +1043,7 @@
@Override
public ConnectionServiceFocusManager.ConnectionServiceFocus getConnectionServiceWrapper() {
- return mConnectionService;
+ return (!mIsTransactionalCall ? mConnectionService : mTransactionalService);
}
@VisibleForTesting
@@ -1767,6 +1772,25 @@
setConnectionProperties(getConnectionProperties());
}
+ public boolean isTransactionalCall() {
+ return mIsTransactionalCall;
+ }
+
+ public void setIsTransactionalCall(boolean isTransactionalCall) {
+ mIsTransactionalCall = isTransactionalCall;
+
+ // Connection properties will add/remove the PROPERTY_SELF_MANAGED.
+ setConnectionProperties(getConnectionProperties());
+ }
+
+ public void setTransactionServiceWrapper(TransactionalServiceWrapper service) {
+ mTransactionalService = service;
+ }
+
+ public TransactionalServiceWrapper getTransactionServiceWrapper() {
+ return mTransactionalService;
+ }
+
public boolean visibleToInCallService() {
return mVisibleToInCallService;
}
@@ -2044,8 +2068,10 @@
createRttStreams();
// Call startRtt to pass the RTT pipes down to the connection service.
// They already turned on the RTT property so no request should be sent.
- mConnectionService.startRtt(this,
- getInCallToCsRttPipeForCs(), getCsToInCallRttPipeForCs());
+ if (mConnectionService != null) {
+ mConnectionService.startRtt(this,
+ getInCallToCsRttPipeForCs(), getCsToInCallRttPipeForCs());
+ }
mWasEverRtt = true;
if (isEmergencyCall()) {
mCallsManager.mute(false);
@@ -2146,7 +2172,6 @@
return mConferenceLevelActiveCall;
}
- @VisibleForTesting
public ConnectionServiceWrapper getConnectionService() {
return mConnectionService;
}
@@ -2462,7 +2487,10 @@
// Override the disconnect cause to MISSED
setOverrideDisconnectCauseCode(new DisconnectCause(DisconnectCause.MISSED));
}
- if (mConnectionService == null) {
+ if (mTransactionalService != null) {
+ mTransactionalService.onDisconnect(this, getDisconnectCause());
+ Log.i(this, "Send Disconnect to transactional service for call");
+ } else if (mConnectionService == null) {
Log.e(this, new Exception(), "disconnect() request on a call without a"
+ " connection service.");
} else {
@@ -2531,6 +2559,8 @@
// {@link ConnectionServiceAdapter#setActive} and other set* methods.
if (mConnectionService != null) {
mConnectionService.answer(this, videoState);
+ } else if (mTransactionalService != null) {
+ mTransactionalService.onAnswer(this, videoState);
} else {
Log.e(this, new NullPointerException(),
"answer call failed due to null CS callId=%s", getId());
@@ -2622,7 +2652,9 @@
// ringing. Since the call is already active on the connectionservice side, we want to
// hangup, not reject.
setOverrideDisconnectCauseCode(new DisconnectCause(DisconnectCause.REJECTED));
- if (mConnectionService != null) {
+ if (mTransactionalService != null) {
+ mTransactionalService.onReject(this, DisconnectCause.REJECTED);
+ } else if (mConnectionService != null) {
mConnectionService.disconnect(this);
} else {
Log.e(this, new NullPointerException(),
@@ -2633,7 +2665,9 @@
// Ensure video state history tracks video state at time of rejection.
mVideoStateHistory |= mVideoState;
- if (mConnectionService != null) {
+ if (mTransactionalService != null) {
+ mTransactionalService.onReject(this, DisconnectCause.REJECTED);
+ } else if (mConnectionService != null) {
mConnectionService.reject(this, rejectWithMessage, textMessage);
} else {
Log.e(this, new NullPointerException(),
@@ -2654,7 +2688,9 @@
// hangup, not reject.
// Since its simulated reason we can't pass along the reject reason.
setOverrideDisconnectCauseCode(new DisconnectCause(DisconnectCause.REJECTED));
- if (mConnectionService != null) {
+ if (mTransactionalService != null) {
+ mTransactionalService.onReject(this, DisconnectCause.REJECTED);
+ } else if (mConnectionService != null) {
mConnectionService.disconnect(this);
} else {
Log.e(this, new NullPointerException(),
@@ -2664,8 +2700,9 @@
} else if (isRinging("reject") || isAnswered("reject")) {
// Ensure video state history tracks video state at time of rejection.
mVideoStateHistory |= mVideoState;
-
- if (mConnectionService != null) {
+ if (mTransactionalService != null) {
+ mTransactionalService.onReject(this, rejectReason);
+ } else if (mConnectionService != null) {
mConnectionService.rejectWithReason(this, rejectReason);
} else {
Log.e(this, new NullPointerException(),
@@ -2684,7 +2721,9 @@
@VisibleForTesting
public void transfer(Uri number, boolean isConfirmationRequired) {
if (mState == CallState.ACTIVE || mState == CallState.ON_HOLD) {
- if (mConnectionService != null) {
+ if (mTransactionalService != null) {
+ Log.i(this, "transfer: called on TransactionalService. doing nothing");
+ } else if (mConnectionService != null) {
mConnectionService.transfer(this, number, isConfirmationRequired);
} else {
Log.e(this, new NullPointerException(),
@@ -2704,7 +2743,9 @@
public void transfer(Call otherCall) {
if (mState == CallState.ACTIVE &&
(otherCall != null && otherCall.getState() == CallState.ON_HOLD)) {
- if (mConnectionService != null) {
+ if (mTransactionalService != null) {
+ Log.i(this, "transfer: called on TransactionalService. doing nothing");
+ } else if (mConnectionService != null) {
mConnectionService.transfer(this, otherCall);
} else {
Log.e(this, new NullPointerException(),
@@ -2724,7 +2765,9 @@
public void hold(String reason) {
if (mState == CallState.ACTIVE) {
- if (mConnectionService != null) {
+ if (mTransactionalService != null) {
+ mTransactionalService.onSetInactive(this);
+ } else if (mConnectionService != null) {
mConnectionService.hold(this);
} else {
Log.e(this, new NullPointerException(),
@@ -2744,7 +2787,9 @@
public void unhold(String reason) {
if (mState == CallState.ON_HOLD) {
- if (mConnectionService != null) {
+ if (mTransactionalService != null){
+ mTransactionalService.onSetActive(this);
+ } else if (mConnectionService != null){
mConnectionService.unhold(this);
} else {
Log.e(this, new NullPointerException(),
@@ -2769,7 +2814,6 @@
}
}
- @VisibleForTesting
public boolean isActive() {
return mState == CallState.ACTIVE;
}
@@ -2835,7 +2879,9 @@
// If the change originated from an InCallService, notify the connection service.
if (source == SOURCE_INCALL_SERVICE) {
- if (mConnectionService != null) {
+ if (mTransactionalService != null) {
+ Log.i(this, "putExtras: called on TransactionalService. doing nothing");
+ } else if (mConnectionService != null) {
mConnectionService.onExtrasChanged(this, mExtras);
} else {
Log.e(this, new NullPointerException(),
@@ -2870,7 +2916,9 @@
// If the change originated from an InCallService, notify the connection service.
if (source == SOURCE_INCALL_SERVICE) {
- if (mConnectionService != null) {
+ if (mTransactionalService != null) {
+ Log.i(this, "removeExtras: called on TransactionalService. doing nothing");
+ } else if (mConnectionService != null) {
mConnectionService.onExtrasChanged(this, mExtras);
} else {
Log.e(this, new NullPointerException(),
@@ -2924,7 +2972,9 @@
}
void postDialContinue(boolean proceed) {
- if (mConnectionService != null) {
+ if (mTransactionalService != null) {
+ Log.i(this, "postDialContinue: called on TransactionalService. doing nothing");
+ } else if (mConnectionService != null) {
mConnectionService.onPostDialContinue(this, proceed);
} else {
Log.e(this, new NullPointerException(),
@@ -2933,7 +2983,9 @@
}
void conferenceWith(Call otherCall) {
- if (mConnectionService == null) {
+ if (mTransactionalService != null) {
+ Log.i(this, "conferenceWith: called on TransactionalService. doing nothing");
+ } else if (mConnectionService == null) {
Log.w(this, "conference requested on a call without a connection service.");
} else {
Log.addEvent(this, LogUtils.Events.CONFERENCE_WITH, otherCall);
@@ -2942,7 +2994,9 @@
}
void splitFromConference() {
- if (mConnectionService == null) {
+ if (mTransactionalService != null) {
+ Log.i(this, "splitFromConference: called on TransactionalService. doing nothing");
+ } else if (mConnectionService == null) {
Log.w(this, "splitting from conference call without a connection service");
} else {
Log.addEvent(this, LogUtils.Events.SPLIT_FROM_CONFERENCE);
@@ -2952,7 +3006,9 @@
@VisibleForTesting
public void mergeConference() {
- if (mConnectionService == null) {
+ if (mTransactionalService != null) {
+ Log.i(this, "mergeConference: called on TransactionalService. doing nothing");
+ } else if (mConnectionService == null) {
Log.w(this, "merging conference calls without a connection service.");
} else if (can(Connection.CAPABILITY_MERGE_CONFERENCE)) {
Log.addEvent(this, LogUtils.Events.CONFERENCE_WITH);
@@ -2963,7 +3019,9 @@
@VisibleForTesting
public void swapConference() {
- if (mConnectionService == null) {
+ if (mTransactionalService != null) {
+ Log.i(this, "swapConference: called on TransactionalService. doing nothing");
+ } else if (mConnectionService == null) {
Log.w(this, "swapping conference calls without a connection service.");
} else if (can(Connection.CAPABILITY_SWAP_CONFERENCE)) {
Log.addEvent(this, LogUtils.Events.SWAP);
@@ -2989,7 +3047,9 @@
}
public void addConferenceParticipants(List<Uri> participants) {
- if (mConnectionService == null) {
+ if (mTransactionalService != null) {
+ Log.i(this, "addConferenceParticipants: called on TransactionalService. doing nothing");
+ } else if (mConnectionService == null) {
Log.w(this, "adding conference participants without a connection service.");
} else if (can(Connection.CAPABILITY_ADD_PARTICIPANT)) {
Log.addEvent(this, LogUtils.Events.ADD_PARTICIPANT);
@@ -3017,6 +3077,11 @@
* If there is an ongoing emergency call, pull requests are also ignored.
*/
public void pullExternalCall() {
+ if (mTransactionalService != null) {
+ Log.i(this, "transfer: called on TransactionalService. doing nothing");
+ return;
+ }
+
if (mConnectionService == null) {
Log.w(this, "pulling a call without a connection service.");
}
@@ -3064,7 +3129,9 @@
* @param extras Associated extras.
*/
public void sendCallEvent(String event, int targetSdkVer, Bundle extras) {
- if (mConnectionService != null) {
+ if (mTransactionalService != null) {
+ Log.i(this, "sendCallEvent: called on TransactionalService. doing nothing");
+ } else if (mConnectionService != null) {
if (android.telecom.Call.EVENT_REQUEST_HANDOVER.equals(event)) {
if (targetSdkVer > Build.VERSION_CODES.P) {
Log.e(this, new Exception(), "sendCallEvent failed. Use public api handoverTo" +
@@ -3497,7 +3564,9 @@
}
public void stopRtt() {
- if (mConnectionService != null) {
+ if (mTransactionalService != null) {
+ Log.i(this, "stopRtt: called on TransactionalService. doing nothing");
+ } else if (mConnectionService != null) {
mConnectionService.stopRtt(this);
} else {
// If this gets called by the in-call app before the connection service is set, we'll
@@ -3507,6 +3576,10 @@
}
public void sendRttRequest() {
+ if (mTransactionalService != null) {
+ Log.i(this, "sendRttRequest: called on TransactionalService. doing nothing");
+ return;
+ }
createRttStreams();
mConnectionService.startRtt(this, getInCallToCsRttPipeForCs(), getCsToInCallRttPipeForCs());
}
@@ -3556,6 +3629,10 @@
Log.w(this, "Response ID %d does not match expected %d", id, mPendingRttRequestId);
return;
}
+ if (mTransactionalService != null) {
+ Log.i(this, "handleRttRequestResponse: called on TransactionalService. doing nothing");
+ return;
+ }
if (accept) {
createRttStreams();
Log.i(this, "RTT request %d accepted.", id);
@@ -4223,6 +4300,11 @@
}
public void maybeOnInCallServiceTrackingChanged(boolean isTracking, boolean hasUi) {
+ if (mTransactionalService != null) {
+ Log.i(this,
+ "maybeOnInCallServiceTrackingChanged: called on TransactionalService");
+ return;
+ }
if (mConnectionService == null) {
Log.w(this, "maybeOnInCallServiceTrackingChanged() request on a call"
+ " without a connection service.");
diff --git a/src/com/android/server/telecom/CallAudioRouteStateMachine.java b/src/com/android/server/telecom/CallAudioRouteStateMachine.java
index bf81cc9..0daa254 100644
--- a/src/com/android/server/telecom/CallAudioRouteStateMachine.java
+++ b/src/com/android/server/telecom/CallAudioRouteStateMachine.java
@@ -1775,7 +1775,12 @@
Set<Call> calls = mCallsManager.getTrackedCalls();
for (Call call : calls) {
if (call != null && call.getConnectionService() != null) {
- call.getConnectionService().onCallAudioStateChanged(call, newCallAudioState);
+ if (call.isTransactionalCall() && call.getTransactionServiceWrapper() != null) {
+ call.getTransactionServiceWrapper().onCallAudioStateChanged(call,
+ newCallAudioState);
+ } else {
+ call.getConnectionService().onCallAudioStateChanged(call, newCallAudioState);
+ }
}
}
}
diff --git a/src/com/android/server/telecom/CallsManager.java b/src/com/android/server/telecom/CallsManager.java
index 75b3864..e5ed4fc 100755
--- a/src/com/android/server/telecom/CallsManager.java
+++ b/src/com/android/server/telecom/CallsManager.java
@@ -64,6 +64,7 @@
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
+import android.os.OutcomeReceiver;
import android.os.PersistableBundle;
import android.os.Process;
import android.os.ResultReceiver;
@@ -77,8 +78,10 @@
import android.provider.CallLog.Calls;
import android.provider.Settings;
import android.sysprop.TelephonyProperties;
+import android.telecom.CallAttributes;
import android.telecom.CallAudioState;
import android.telecom.CallEndpoint;
+import android.telecom.CallException;
import android.telecom.CallScreeningService;
import android.telecom.CallerInfo;
import android.telecom.Conference;
@@ -159,7 +162,6 @@
* access from other packages specifically refraining from passing the CallsManager instance
* beyond the com.android.server.telecom package boundary.
*/
-@VisibleForTesting
public class CallsManager extends Call.ListenerBase
implements VideoProviderProxy.Listener, CallFilterResultCallback, CurrentUserProxy {
@@ -1149,7 +1151,6 @@
}
}
- @VisibleForTesting
public Call getForegroundCall() {
if (mCallAudioManager == null) {
// Happens when getForegroundCall is called before full initialization.
@@ -1289,7 +1290,7 @@
processIncomingCallIntent(phoneAccountHandle, extras, false);
}
- void processIncomingCallIntent(PhoneAccountHandle phoneAccountHandle, Bundle extras,
+ public Call processIncomingCallIntent(PhoneAccountHandle phoneAccountHandle, Bundle extras,
boolean isConference) {
Log.d(this, "processIncomingCallIntent");
boolean isHandover = extras.getBoolean(TelecomManager.EXTRA_IS_HANDOVER);
@@ -1299,7 +1300,7 @@
handle = extras.getParcelable(TelephonyManager.EXTRA_INCOMING_NUMBER);
}
Call call = new Call(
- getNextCallId(),
+ generateNextCallId(extras),
mContext,
this,
mLock,
@@ -1315,6 +1316,15 @@
mClockProxy,
mToastFactory);
+ // set properties for transactional call
+ if (extras.containsKey(TelecomManager.TRANSACTION_CALL_ID_KEY)) {
+ call.setIsTransactionalCall(true);
+ call.setConnectionCapabilities(
+ extras.getInt(CallAttributes.CALL_CAPABILITIES_KEY,
+ CallAttributes.SUPPORTS_SET_INACTIVE), true);
+ call.setTargetPhoneAccount(phoneAccountHandle);
+ }
+
// Ensure new calls related to self-managed calls/connections are set as such. This will
// be overridden when the actual connection is returned in startCreateConnection, however
// doing this now ensures the logs and any other logic will treat this call as self-managed
@@ -1459,9 +1469,15 @@
} else {
notifyCreateConnectionFailed(phoneAccountHandle, call);
}
+ } else if (call.isTransactionalCall()) {
+ // transactional calls should skip Call#startCreateConnection below
+ // as that is meant for Call objects with a ConnectionServiceWrapper
+ call.setState(CallState.RINGING, "explicitly set new incoming to ringing");
+ addCall(call);
} else {
call.startCreateConnection(mPhoneAccountRegistrar);
}
+ return call;
}
void addNewUnknownCall(PhoneAccountHandle phoneAccountHandle, Bundle extras) {
@@ -1552,6 +1568,14 @@
originalIntent, callingPackage, false);
}
+ private String generateNextCallId(Bundle extras) {
+ if (extras != null && extras.containsKey(TelecomManager.TRANSACTION_CALL_ID_KEY)) {
+ return extras.getString(TelecomManager.TRANSACTION_CALL_ID_KEY);
+ } else {
+ return getNextCallId();
+ }
+ }
+
private CompletableFuture<Call> startOutgoingCall(List<Uri> participants,
PhoneAccountHandle requestedAccountHandle,
Bundle extras, UserHandle initiatingUser, Intent originalIntent,
@@ -1578,7 +1602,7 @@
// Create a call with original handle. The handle may be changed when the call is attached
// to a connection service, but in most cases will remain the same.
if (call == null) {
- call = new Call(getNextCallId(), mContext,
+ call = new Call(generateNextCallId(extras), mContext,
this,
mLock,
mConnectionServiceRepository,
@@ -1593,6 +1617,15 @@
isConference, /* isConference */
mClockProxy,
mToastFactory);
+
+ if (extras.containsKey(TelecomManager.TRANSACTION_CALL_ID_KEY)) {
+ call.setIsTransactionalCall(true);
+ call.setConnectionCapabilities(
+ extras.getInt(CallAttributes.CALL_CAPABILITIES_KEY,
+ CallAttributes.SUPPORTS_SET_INACTIVE), true);
+ call.setTargetPhoneAccount(requestedAccountHandle);
+ }
+
call.initAnalytics(callingPackage, creationLogs.toString());
// Ensure new calls related to self-managed calls/connections are set as such. This
@@ -2502,6 +2535,10 @@
public void answerCall(Call call, int videoState) {
if (!mCalls.contains(call)) {
Log.i(this, "Request to answer a non-existent call %s", call);
+ } else if (call.isTransactionalCall()) {
+ // InCallAdapter is requesting to answer the given transactioanl call. Must get an ack
+ // from the client via a transaction before answering.
+ call.answer(videoState);
} else {
// Hold or disconnect the active call and request call focus for the incoming call.
Call activeCall = (Call) mConnectionSvrFocusMgr.getCurrentFocusCall();
@@ -3203,6 +3240,45 @@
return false;
}
+ // attempt to hold the requested call and complete the callback on the result
+ public void transactionHoldPotentialActiveCallForNewCall(Call newCall,
+ OutcomeReceiver<Boolean, CallException> callback) {
+ Call activeCall = (Call) mConnectionSvrFocusMgr.getCurrentFocusCall();
+ Log.i(this, "transactionHoldPotentialActiveCallForNewCall: "
+ + "newCall=[%s], activeCall=[%s]", newCall, activeCall);
+
+ // early exit if there is no need to hold an active call
+ if (activeCall == null || activeCall == newCall) {
+ Log.i(this, "transactionHoldPotentialActiveCallForNewCall:"
+ + " no need to hold activeCall");
+ callback.onResult(true);
+ return;
+ }
+
+ // before attempting CallsManager#holdActiveCallForNewCall(Call), check if it'll fail early
+ if (!canHold(activeCall) &&
+ !(supportsHold(activeCall) && areFromSameSource(activeCall, newCall))) {
+ Log.i(this, "transactionHoldPotentialActiveCallForNewCall: "
+ + "conditions show the call cannot be held.");
+ callback.onError(new CallException("call does not support hold",
+ CallException.CODE_CANNOT_HOLD_CURRENT_ACTIVE_CALL));
+ return;
+ }
+
+ // attempt to hold the active call
+ if (!holdActiveCallForNewCall(newCall)) {
+ Log.i(this, "transactionHoldPotentialActiveCallForNewCall: "
+ + "attempted to hold call but failed.");
+ callback.onError(new CallException("cannot hold active call failed",
+ CallException.CODE_CANNOT_HOLD_CURRENT_ACTIVE_CALL));
+ return;
+ }
+
+ // officially mark the activeCall as held
+ markCallAsOnHold(activeCall);
+ callback.onResult(true);
+ }
+
@VisibleForTesting
public void markCallAsActive(Call call) {
Log.i(this, "markCallAsActive, isSelfManaged: " + call.isSelfManaged());
@@ -3237,7 +3313,6 @@
}
}
- @VisibleForTesting
public void markCallAsOnHold(Call call) {
setCallState(call, CallState.ON_HOLD, "on-hold set explicitly");
}
@@ -3248,7 +3323,6 @@
*
* @param disconnectCause The disconnect cause, see {@link android.telecom.DisconnectCause}.
*/
- @VisibleForTesting
public void markCallAsDisconnected(Call call, DisconnectCause disconnectCause) {
int oldState = call.getState();
if (call.getState() == CallState.SIMULATED_RINGING
@@ -3318,7 +3392,7 @@
/**
* Removes an existing disconnected call, and notifies the in-call app.
*/
- void markCallAsRemoved(Call call) {
+ public void markCallAsRemoved(Call call) {
if (call.isDisconnectHandledViaFuture()) {
Log.i(this, "markCallAsRemoved; callid=%s, postingToFuture.", call.getId());
// A future is being used due to a CallDiagnosticService handling the call. We will
@@ -3795,6 +3869,10 @@
*/
@VisibleForTesting
public void addCall(Call call) {
+ if (mCalls.contains(call)) {
+ Log.i(this, "addCall(%s) is already added");
+ return;
+ }
Trace.beginSection("addCall");
Log.i(this, "addCall(%s)", call);
call.addListener(this);
@@ -3827,6 +3905,11 @@
Trace.beginSection("removeCall");
Log.v(this, "removeCall(%s)", call);
+ if (call.isTransactionalCall() && call.getTransactionServiceWrapper() != null) {
+ // remove call from wrappers
+ call.getTransactionServiceWrapper().removeCallFromWrappers(call);
+ }
+
call.setParentAndChildCall(null); // clean up parent relationship before destroying.
call.removeListener(this);
call.clearConnectionService();
@@ -5492,8 +5575,10 @@
return mConnectionSvrFocusMgr;
}
- private boolean canHold(Call call) {
- return call.can(Connection.CAPABILITY_HOLD) && call.getState() != CallState.DIALING;
+ @VisibleForTesting
+ public boolean canHold(Call call) {
+ return ((call.isTransactionalCall() && call.can(Connection.CAPABILITY_SUPPORT_HOLD)) ||
+ call.can(Connection.CAPABILITY_HOLD)) && call.getState() != CallState.DIALING;
}
private boolean supportsHold(Call call) {
@@ -5601,6 +5686,59 @@
}
}
+ // driver method to create and execute a new TransactionalFocusRequestCallback
+ public void transactionRequestNewFocusCall(Call call,
+ OutcomeReceiver<Boolean, CallException> callback) {
+ Log.d(this, "transactionRequestNewFocusCall");
+ PendingAction pendingAction = new ActionSetCallState(call, CallState.ACTIVE,
+ "transactional ActionSetCallState");
+ mConnectionSvrFocusMgr
+ .requestFocus(call,
+ new TransactionalFocusRequestCallback(pendingAction, call, callback));
+ }
+
+ // request a new call focus and ensure the request was successful
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+ public class TransactionalFocusRequestCallback implements
+ ConnectionServiceFocusManager.RequestFocusCallback {
+ private PendingAction mPendingAction;
+ @NonNull
+ private Call mTargetCallFocus;
+ private OutcomeReceiver<Boolean, CallException> mCallback;
+
+ TransactionalFocusRequestCallback(PendingAction pendingAction, @NonNull Call call,
+ OutcomeReceiver<Boolean, CallException> callback) {
+ mPendingAction = pendingAction;
+ mTargetCallFocus = call;
+ mCallback = callback;
+ }
+
+ @Override
+ public void onRequestFocusDone(ConnectionServiceFocusManager.CallFocus call) {
+ Call currentCallFocus = (Call) mConnectionSvrFocusMgr.getCurrentFocusCall();
+
+ if (currentCallFocus == null) {
+ Log.i(this, "TransactionalFocusRequestCallback: "
+ + "currentCallFocus is null.");
+ mCallback.onError(new CallException("currentCallFocus is null",
+ CallException.CODE_CALL_CANNOT_BE_SET_TO_ACTIVE));
+ return;
+ }
+
+ Log.i(this, "TransactionalFocusRequestCallback: targetId=[%s], "
+ + "currentId=[%s]", mTargetCallFocus.getId(), currentCallFocus.getId());
+ if (!currentCallFocus.getId().equals(mTargetCallFocus.getId())) {
+ Log.i(this, "TransactionalFocusRequestCallback: "
+ + "currentCallFocus is not equal to targetCallFocus.");
+ mCallback.onError(new CallException("current focus is not target focus",
+ CallException.CODE_CALL_CANNOT_BE_SET_TO_ACTIVE));
+ return;
+ }
+ mPendingAction.performAction();
+ mCallback.onResult(true);
+ }
+ }
+
public void resetConnectionTime(Call call) {
call.setConnectTimeMillis(System.currentTimeMillis());
call.setConnectElapsedTimeMillis(SystemClock.elapsedRealtime());
diff --git a/src/com/android/server/telecom/ConnectionServiceFocusManager.java b/src/com/android/server/telecom/ConnectionServiceFocusManager.java
index aa0a64f..8db98e9 100644
--- a/src/com/android/server/telecom/ConnectionServiceFocusManager.java
+++ b/src/com/android/server/telecom/ConnectionServiceFocusManager.java
@@ -154,7 +154,8 @@
}
private static final int[] PRIORITY_FOCUS_CALL_STATE = new int[] {
- CallState.ACTIVE, CallState.CONNECTING, CallState.DIALING, CallState.AUDIO_PROCESSING
+ CallState.ACTIVE, CallState.CONNECTING, CallState.DIALING, CallState.AUDIO_PROCESSING,
+ CallState.RINGING
};
private static final int MSG_REQUEST_FOCUS = 1;
@@ -348,13 +349,14 @@
public List<CallFocus> getAllCall() { return mCalls; }
private void updateConnectionServiceFocus(ConnectionServiceFocus connSvrFocus) {
+ Log.i(this, "updateConnectionServiceFocus connSvr = %s", connSvrFocus);
if (!Objects.equals(mCurrentFocus, connSvrFocus)) {
if (connSvrFocus != null) {
connSvrFocus.setConnectionServiceFocusListener(mConnectionServiceFocusListener);
connSvrFocus.connectionServiceFocusGained();
}
mCurrentFocus = connSvrFocus;
- Log.d(this, "updateConnectionServiceFocus connSvr = %s", connSvrFocus);
+ Log.i(this, "updateConnectionServiceFocus connSvr = %s", connSvrFocus);
}
}
@@ -362,6 +364,7 @@
mCurrentFocusCall = null;
if (mCurrentFocus == null) {
+ Log.d(this, "updateCurrentFocusCall: mCurrentFocus is null");
return;
}
diff --git a/src/com/android/server/telecom/PhoneAccountRegistrar.java b/src/com/android/server/telecom/PhoneAccountRegistrar.java
index db577bd..cb9e052 100644
--- a/src/com/android/server/telecom/PhoneAccountRegistrar.java
+++ b/src/com/android/server/telecom/PhoneAccountRegistrar.java
@@ -41,7 +41,6 @@
import android.provider.Settings;
import android.telecom.CallAudioState;
import android.telecom.ConnectionService;
-import android.telecom.DefaultDialerManager;
import android.telecom.Log;
import android.telecom.PhoneAccount;
import android.telecom.PhoneAccountHandle;
@@ -58,7 +57,6 @@
// TODO: Needed for move to system service: import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.util.FastXmlSerializer;
import com.android.internal.util.IndentingPrintWriter;
import com.android.internal.util.XmlUtils;
@@ -66,7 +64,6 @@
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlSerializer;
-import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
@@ -85,12 +82,9 @@
import java.util.List;
import java.util.Map;
import java.util.Objects;
-import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
-import java.util.stream.Collector;
import java.util.stream.Collectors;
-import java.util.stream.Stream;
/**
* Handles writing and reading PhoneAccountHandle registration entries. This is a simple verbatim
@@ -859,7 +853,8 @@
public void registerPhoneAccount(PhoneAccount account) {
// Enforce the requirement that a connection service for a phone account has the correct
// permission.
- if (!phoneAccountRequiresBindPermission(account.getAccountHandle())) {
+ if (!hasTransactionalCallCapabilites(account) &&
+ !phoneAccountRequiresBindPermission(account.getAccountHandle())) {
Log.w(this,
"Phone account %s does not have BIND_TELECOM_CONNECTION_SERVICE permission.",
account.getAccountHandle());
@@ -898,6 +893,15 @@
boolean isEnabled = false;
boolean isNewAccount;
+ // add self-managed capability for transactional accounts that are missing it
+ if (hasTransactionalCallCapabilites(account) &&
+ !account.hasCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED)) {
+ account = account.toBuilder()
+ .setCapabilities(account.getCapabilities()
+ | PhoneAccount.CAPABILITY_SELF_MANAGED)
+ .build();
+ }
+
PhoneAccount oldAccount = getPhoneAccountUnchecked(account.getAccountHandle());
if (oldAccount != null) {
enforceSelfManagedAccountUnmodified(account, oldAccount);
@@ -1196,6 +1200,11 @@
Log.w(this, "phoneAccount %s not found", phoneAccountHandle.getComponentName());
return false;
}
+
+ if (hasTransactionalCallCapabilites(getPhoneAccountUnchecked(phoneAccountHandle))) {
+ return false;
+ }
+
for (ResolveInfo resolveInfo : resolveInfos) {
ServiceInfo serviceInfo = resolveInfo.serviceInfo;
if (serviceInfo == null) {
@@ -1214,6 +1223,15 @@
return true;
}
+ @VisibleForTesting
+ public boolean hasTransactionalCallCapabilites(PhoneAccount phoneAccount) {
+ if (phoneAccount == null) {
+ return false;
+ }
+ return phoneAccount.hasCapabilities(
+ PhoneAccount.CAPABILITY_SUPPORTS_TRANSACTIONAL_OPERATIONS);
+ }
+
//
// Methods for retrieving PhoneAccounts and PhoneAccountHandles
//
diff --git a/src/com/android/server/telecom/TelecomServiceImpl.java b/src/com/android/server/telecom/TelecomServiceImpl.java
index ab35043..d5f9f58 100644
--- a/src/com/android/server/telecom/TelecomServiceImpl.java
+++ b/src/com/android/server/telecom/TelecomServiceImpl.java
@@ -27,6 +27,10 @@
import static android.Manifest.permission.REGISTER_SIM_SUBSCRIPTION;
import static android.Manifest.permission.WRITE_SECURE_SETTINGS;
import static android.Manifest.permission.MANAGE_OWN_CALLS;
+import static android.telecom.CallAttributes.DIRECTION_INCOMING;
+import static android.telecom.CallAttributes.DIRECTION_OUTGOING;
+import static android.telecom.TelecomManager.TELECOM_TRANSACTION_SUCCESS;
+import static android.telecom.CallException.CODE_ERROR_UNKNOWN;
import android.Manifest;
import android.app.ActivityManager;
@@ -47,10 +51,15 @@
import android.os.Binder;
import android.os.Build;
import android.os.Bundle;
+import android.os.OutcomeReceiver;
import android.os.Process;
+import android.os.RemoteException;
import android.os.UserHandle;
import android.provider.BlockedNumberContract;
import android.provider.Settings;
+import android.telecom.CallAttributes;
+
+import android.telecom.CallException;
import android.telecom.Log;
import android.telecom.PhoneAccount;
import android.telecom.PhoneAccountHandle;
@@ -62,10 +71,20 @@
import android.text.TextUtils;
import android.util.EventLog;
+import androidx.annotation.NonNull;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.telecom.ICallControl;
+import com.android.internal.telecom.ICallEventCallback;
import com.android.internal.telecom.ITelecomService;
import com.android.internal.util.IndentingPrintWriter;
import com.android.server.telecom.components.UserCallIntentProcessorFactory;
import com.android.server.telecom.settings.BlockedNumbersActivity;
+import com.android.server.telecom.voip.IncomingCallTransaction;
+import com.android.server.telecom.voip.OutgoingCallTransaction;
+import com.android.server.telecom.voip.TransactionManager;
+import com.android.server.telecom.voip.VoipCallTransaction;
+import com.android.server.telecom.voip.VoipCallTransactionResult;
import java.io.FileDescriptor;
import java.io.PrintWriter;
@@ -112,12 +131,106 @@
}
}
+ private static final String TAG = "TelecomServiceImpl";
private static final String TIME_LINE_ARG = "timeline";
private static final int DEFAULT_VIDEO_STATE = -1;
private static final String PERMISSION_HANDLE_CALL_INTENT =
"android.permission.HANDLE_CALL_INTENT";
+ private static final String ADD_CALL_ERR_MSG = "Call could not be created or found. "
+ + "Retry operation.";
private final ITelecomService.Stub mBinderImpl = new ITelecomService.Stub() {
+
+ @Override
+ public void addCall(CallAttributes callAttributes, ICallEventCallback callEventCallback,
+ String callId, String callingPackage) {
+ try {
+ Log.startSession("TSI.aC", Log.getPackageAbbreviation(callingPackage));
+ Log.i(TAG, "addCall: id=[%s], attributes=[%s]", callId, callAttributes);
+ PhoneAccountHandle handle = callAttributes.getPhoneAccountHandle();
+
+ // enforce permissions and arguments
+ enforcePermission(android.Manifest.permission.MANAGE_OWN_CALLS);
+ enforceUserHandleMatchesCaller(handle);
+ enforcePhoneAccountIsNotManaged(handle);// only allow self-managed packages (temp.)
+ enforcePhoneAccountIsRegisteredEnabled(handle, handle.getUserHandle());
+ enforceCallingPackage(callingPackage, "addCall");
+
+ VoipCallTransaction transaction = null;
+ // create transaction based on the call direction
+ switch (callAttributes.getDirection()) {
+ case DIRECTION_OUTGOING:
+ transaction = new OutgoingCallTransaction(callId, mContext, callAttributes,
+ mCallsManager);
+ break;
+ case DIRECTION_INCOMING:
+ transaction = new IncomingCallTransaction(callId, callAttributes,
+ mCallsManager);
+ break;
+ default:
+ throw new IllegalArgumentException(String.format("Invalid Call Direction. "
+ + "Was [%d] but should be within [%d,%d]",
+ callAttributes.getDirection(), DIRECTION_INCOMING,
+ DIRECTION_OUTGOING));
+ }
+
+ mTransactionManager.addTransaction(transaction, new OutcomeReceiver<>() {
+ @Override
+ public void onResult(VoipCallTransactionResult result) {
+ Log.d(TAG, "addCall: onResult");
+ Call call = result.getCall();
+
+ if (call == null || !call.getId().equals(callId)) {
+ Log.i(TAG, "addCall: onResult: call is null or id mismatch");
+ onAddCallControl(callId, callEventCallback, null,
+ new CallException(ADD_CALL_ERR_MSG, CODE_ERROR_UNKNOWN));
+ return;
+ }
+
+ TransactionalServiceWrapper serviceWrapper =
+ mTransactionalServiceRepository
+ .addNewCallForTransactionalServiceWrapper(handle,
+ callEventCallback, mCallsManager, call);
+
+ call.setTransactionServiceWrapper(serviceWrapper);
+ ICallControl clientCallControl = serviceWrapper.getICallControl();
+
+ if (clientCallControl == null) {
+ throw new IllegalStateException("TransactionalServiceWrapper"
+ + "#ICallControl is null.");
+ }
+
+ // finally, send objects back to the client
+ onAddCallControl(callId, callEventCallback, clientCallControl, null);
+ }
+
+ @Override
+ public void onError(@NonNull CallException exception) {
+ Log.d(TAG, "addCall: onError: e=[%s]", exception.toString());
+ onAddCallControl(callId, callEventCallback, null, exception);
+ }
+ });
+ } finally {
+ Log.endSession();
+ }
+ }
+
+ private void onAddCallControl(String callId, ICallEventCallback callEventCallback,
+ ICallControl callControl, CallException callException) {
+ try {
+ if (callException == null) {
+ callEventCallback.onAddCallControl(callId, TELECOM_TRANSACTION_SUCCESS,
+ callControl, null);
+ } else {
+ callEventCallback.onAddCallControl(callId,
+ CallException.CODE_ERROR_UNKNOWN,
+ null, callException);
+ }
+ } catch (RemoteException remoteException) {
+ throw remoteException.rethrowAsRuntimeException();
+ }
+ }
+
@Override
public PhoneAccountHandle getDefaultOutgoingPhoneAccount(String uriScheme,
String callingPackage, String callingFeatureId) {
@@ -2255,6 +2368,8 @@
private final SubscriptionManagerAdapter mSubscriptionManagerAdapter;
private final SettingsSecureAdapter mSettingsSecureAdapter;
private final TelecomSystem.SyncRoot mLock;
+ private TransactionManager mTransactionManager;
+ private final TransactionalServiceRepository mTransactionalServiceRepository;
public TelecomServiceImpl(
Context context,
@@ -2291,6 +2406,14 @@
defaultDialer);
mContext.sendBroadcastAsUser(intent, UserHandle.of(userId));
});
+
+ mTransactionManager = TransactionManager.getInstance();
+ mTransactionalServiceRepository = new TransactionalServiceRepository();
+ }
+
+ @VisibleForTesting
+ public void setTransactionManager(TransactionManager transactionManager){
+ mTransactionManager = transactionManager;
}
public ITelecomService.Stub getBinder() {
@@ -2403,6 +2526,29 @@
}
}
+ // Enforce that the PhoneAccountHandle is tied to a self-managed package and not managed (aka
+ // sim calling, etc.)
+ private void enforcePhoneAccountIsNotManaged(PhoneAccountHandle phoneAccountHandle) {
+ PhoneAccount phoneAccount = mPhoneAccountRegistrar.getPhoneAccount(phoneAccountHandle,
+ phoneAccountHandle.getUserHandle());
+ if (phoneAccount == null) {
+ throw new IllegalArgumentException("enforcePhoneAccountIsNotManaged:"
+ + " phoneAccount is null");
+ }
+ if (phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION)) {
+ throw new IllegalArgumentException("enforcePhoneAccountIsNotManaged:"
+ + " CAPABILITY_SIM_SUBSCRIPTION is not allowed");
+ }
+ if (phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER)) {
+ throw new IllegalArgumentException("enforcePhoneAccountIsNotManaged:"
+ + " CAPABILITY_CALL_PROVIDER is not allowed");
+ }
+ if (phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_CONNECTION_MANAGER)) {
+ throw new IllegalArgumentException("enforcePhoneAccountIsNotManaged:"
+ + " CAPABILITY_CONNECTION_MANAGER is not allowed");
+ }
+ }
+
private void enforcePhoneAccountModificationForPackage(String packageName) {
// TODO: Use a new telecomm permission for this instead of reusing modify.
diff --git a/src/com/android/server/telecom/TransactionalServiceRepository.java b/src/com/android/server/telecom/TransactionalServiceRepository.java
new file mode 100644
index 0000000..f84b934
--- /dev/null
+++ b/src/com/android/server/telecom/TransactionalServiceRepository.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom;
+
+import android.telecom.PhoneAccountHandle;
+
+import com.android.internal.telecom.ICallEventCallback;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Tracks all TransactionalServiceWrappers that have an ongoing call. Removes wrappers that have no
+ * more calls.
+ */
+public class TransactionalServiceRepository {
+
+ private static final Map<PhoneAccountHandle, TransactionalServiceWrapper> lookupTable =
+ new HashMap<>();
+
+ public TransactionalServiceRepository() {
+ }
+
+ public TransactionalServiceWrapper addNewCallForTransactionalServiceWrapper
+ (PhoneAccountHandle phoneAccountHandle, ICallEventCallback callEventCallback,
+ CallsManager callsManager, Call call) {
+
+ TransactionalServiceWrapper service = null;
+ if (!hasExistingServiceWrapper(phoneAccountHandle)) {
+ service = new TransactionalServiceWrapper(callEventCallback,
+ callsManager, phoneAccountHandle, call, this);
+ } else {
+ service = getTransactionalServiceWrapper(phoneAccountHandle);
+ if (service == null) {
+ throw new IllegalStateException("service is null");
+ } else {
+ service.trackCall(call);
+ }
+ }
+
+ lookupTable.put(phoneAccountHandle, service);
+
+ return service;
+ }
+
+ public TransactionalServiceWrapper getTransactionalServiceWrapper(PhoneAccountHandle pah) {
+ return lookupTable.get(pah);
+ }
+
+ public boolean hasExistingServiceWrapper(PhoneAccountHandle pah) {
+ return lookupTable.containsKey(pah);
+ }
+
+ public boolean removeServiceWrapper(PhoneAccountHandle pah) {
+ if (!hasExistingServiceWrapper(pah)) {
+ return false;
+ }
+ lookupTable.remove(pah);
+ return true;
+ }
+
+}
diff --git a/src/com/android/server/telecom/TransactionalServiceWrapper.java b/src/com/android/server/telecom/TransactionalServiceWrapper.java
new file mode 100644
index 0000000..8caacb8
--- /dev/null
+++ b/src/com/android/server/telecom/TransactionalServiceWrapper.java
@@ -0,0 +1,483 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom;
+
+import static android.telecom.TelecomManager.TELECOM_TRANSACTION_SUCCESS;
+import static android.telecom.CallException.CODE_CALL_IS_NOT_BEING_TRACKED;
+import static android.telecom.CallException.TRANSACTION_EXCEPTION_KEY;
+
+import android.content.ComponentName;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.OutcomeReceiver;
+import android.os.RemoteException;
+import android.os.ResultReceiver;
+import android.telecom.CallAudioState;
+import android.telecom.CallException;
+import android.telecom.DisconnectCause;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.Log;
+
+import androidx.annotation.VisibleForTesting;
+
+import com.android.internal.telecom.ICallControl;
+import com.android.internal.telecom.ICallEventCallback;
+import com.android.server.telecom.voip.CallEventCallbackAckTransaction;
+import com.android.server.telecom.voip.HoldCallTransaction;
+import com.android.server.telecom.voip.EndCallTransaction;
+import com.android.server.telecom.voip.SerialTransaction;
+import com.android.server.telecom.voip.TransactionManager;
+import com.android.server.telecom.voip.HoldActiveCallForNewCallTransaction;
+import com.android.server.telecom.voip.RequestFocusTransaction;
+import com.android.server.telecom.voip.VoipCallTransaction;
+import com.android.server.telecom.voip.VoipCallTransactionResult;
+
+import java.util.ArrayList;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * Implements {@link android.telecom.CallEventCallback} and {@link android.telecom.CallControl}
+ * on a per-client basis which is tied to a {@link PhoneAccountHandle}
+ */
+public class TransactionalServiceWrapper implements
+ ConnectionServiceFocusManager.ConnectionServiceFocus, IBinder.DeathRecipient {
+ private static final String TAG = TransactionalServiceWrapper.class.getSimpleName();
+
+ // CallControl : Client (ex. voip app) --> Telecom
+ public static final String SET_ACTIVE = "SetActive";
+ public static final String SET_INACTIVE = "SetInactive";
+ public static final String REJECT = "Reject";
+ public static final String DISCONNECT = "Disconnect";
+
+ // CallEventCallback : Telecom --> Client (ex. voip app)
+ public static final String ON_SET_ACTIVE = "onSetActive";
+ public static final String ON_SET_INACTIVE = "onSetInactive";
+ public static final String ON_ANSWER = "onAnswer";
+ public static final String ON_REJECT = "onReject";
+ public static final String ON_DISCONNECT = "onDisconnect";
+
+ private final CallsManager mCallsManager;
+ private final ICallEventCallback mICallEventCallback;
+ private final PhoneAccountHandle mPhoneAccountHandle;
+ private final TransactionalServiceRepository mRepository;
+ private ConnectionServiceFocusManager.ConnectionServiceFocusListener mConnSvrFocusListener;
+ // init when constructor is called
+ private final Hashtable<String, Call> mTrackedCalls = new Hashtable<>();
+ private final Object mLock;
+ private final String mPackageName;
+ // needs to be non-final for testing
+ private TransactionManager mTransactionManager;
+
+ public TransactionalServiceWrapper(ICallEventCallback callEventCallback,
+ CallsManager callsManager, PhoneAccountHandle phoneAccountHandle, Call call,
+ TransactionalServiceRepository repo) {
+ // passed args
+ mICallEventCallback = callEventCallback;
+ mCallsManager = callsManager;
+ mPhoneAccountHandle = phoneAccountHandle;
+ mTrackedCalls.put(call.getId(), call); // service is now tracking its first call
+ mRepository = repo;
+ // init instance vars
+ mPackageName = phoneAccountHandle.getComponentName().getPackageName();
+ mTransactionManager = TransactionManager.getInstance();
+ mLock = new Object();
+ }
+
+ @VisibleForTesting
+ public void setTransactionManager(TransactionManager transactionManager) {
+ mTransactionManager = transactionManager;
+ }
+
+ public PhoneAccountHandle getPhoneAccountHandle() {
+ return mPhoneAccountHandle;
+ }
+
+ public void trackCall(Call call) {
+ synchronized (mLock) {
+ if (call != null) {
+ mTrackedCalls.put(call.getId(), call);
+ }
+ }
+ }
+ @VisibleForTesting
+ public boolean untrackCall(Call call) {
+ Call removedCall = null;
+ synchronized (mLock) {
+ if (call != null) {
+ removedCall = mTrackedCalls.remove(call.getId());
+ if (mTrackedCalls.size() == 0) {
+ mRepository.removeServiceWrapper(mPhoneAccountHandle);
+ }
+ }
+ }
+ Log.i(TAG, "removedCall call=" + removedCall);
+ return removedCall != null;
+ }
+
+ @VisibleForTesting
+ public int getNumberOfTrackedCalls() {
+ int callCount = 0;
+ synchronized (mLock) {
+ callCount = mTrackedCalls.size();
+ }
+ return callCount;
+ }
+
+ @Override
+ public void binderDied() {
+ // remove all tacked calls from CallsManager && frameworks side
+ for (String id : mTrackedCalls.keySet()) {
+ Call call = mTrackedCalls.get(id);
+ mCallsManager.markCallAsDisconnected(call, new DisconnectCause(DisconnectCause.ERROR));
+ mCallsManager.removeCall(call);
+ // remove calls from Frameworks side
+ if (mICallEventCallback != null) {
+ try {
+ mICallEventCallback.removeCallFromTransactionalServiceWrapper(call.getId());
+ } catch (RemoteException e) {
+ // pass
+ }
+ }
+ }
+ mTrackedCalls.clear();
+ }
+
+ /***
+ *********************************************************************************************
+ ** ICallControl: Client --> Server **
+ **********************************************************************************************
+ */
+ public final ICallControl mICallControl = new ICallControl.Stub() {
+ @Override
+ public void setActive(String callId, android.os.ResultReceiver callback)
+ throws RemoteException {
+ try {
+ Log.startSession("TSW.sA");
+ createTransactions(callId, callback, SET_ACTIVE, 0);
+ } finally {
+ Log.endSession();
+ }
+ }
+
+ @Override
+ public void setInactive(String callId, android.os.ResultReceiver callback)
+ throws RemoteException {
+ try {
+ Log.startSession("TSW.sI");
+ createTransactions(callId, callback, SET_INACTIVE, 0);
+ } finally {
+ Log.endSession();
+ }
+ }
+
+ @Override
+ public void disconnect(String callId, DisconnectCause disconnectCause,
+ android.os.ResultReceiver callback)
+ throws RemoteException {
+ try {
+ Log.startSession("TSW.d");
+ createTransactions(callId, callback, DISCONNECT, disconnectCause.getCode());
+ } finally {
+ Log.endSession();
+ }
+ }
+
+ @Override
+ public void rejectCall(String callId, android.os.ResultReceiver callback)
+ throws RemoteException {
+ try {
+ Log.startSession("TSW.rC");
+ createTransactions(callId, callback, REJECT,
+ android.telecom.Call.REJECT_REASON_DECLINED);
+ } finally {
+ Log.endSession();
+ }
+ }
+
+ private void createTransactions(String callId, ResultReceiver callback, String action,
+ int code) {
+ Log.d(TAG, "createTransactions: callId=" + callId);
+ Call call = mTrackedCalls.get(callId);
+ if (call != null) {
+ switch (action) {
+ case SET_ACTIVE:
+ addTransactionsToManager(createSetActiveTransactions(call), callback);
+ break;
+ case REJECT:
+ case DISCONNECT:
+ addTransactionsToManager(new EndCallTransaction(mCallsManager,
+ action.equals(DISCONNECT), code, call), callback);
+ break;
+ case SET_INACTIVE:
+ addTransactionsToManager(
+ new HoldCallTransaction(mCallsManager, call), callback);
+ break;
+ }
+ } else {
+ Log.i(TAG, action + ": mCallsManager does not contain call with id=" + callId);
+ callback.send(CODE_CALL_IS_NOT_BEING_TRACKED, new Bundle());
+ }
+ }
+
+ private void addTransactionsToManager(VoipCallTransaction transaction,
+ ResultReceiver callback) {
+ Log.d(TAG, "addTransactionsToManager");
+
+ mTransactionManager.addTransaction(transaction, new OutcomeReceiver<>() {
+ @Override
+ public void onResult(VoipCallTransactionResult result) {
+ Log.d(TAG, "addTransactionsToManager: onResult:");
+ callback.send(TELECOM_TRANSACTION_SUCCESS, new Bundle());
+ }
+
+ @Override
+ public void onError(CallException exception) {
+ Log.d(TAG, "addTransactionsToManager: onError");
+ Bundle extras = new Bundle();
+ extras.putParcelable(TRANSACTION_EXCEPTION_KEY, exception);
+ callback.send(exception == null ? CallException.CODE_ERROR_UNKNOWN :
+ exception.getCode(), extras);
+ }
+ });
+ }
+ };
+
+ public ICallControl getICallControl() {
+ return mICallControl;
+ }
+
+ /***
+ *********************************************************************************************
+ ** ICallEventCallback: Server --> Client **
+ **********************************************************************************************
+ */
+
+ public void onSetActive(Call call) {
+ try {
+ Log.startSession("TSW.oSA");
+ Log.d(TAG, String.format(Locale.US, "onSetActive: callId=[%s]", call.getId()));
+ handleNewActiveCallCallbacks(call, ON_SET_ACTIVE, 0);
+ } finally {
+ Log.endSession();
+ }
+ }
+
+ public void onAnswer(Call call, int videoState) {
+ try {
+ Log.startSession("TSW.oA");
+ Log.d(TAG, String.format(Locale.US, "onAnswer: callId=[%s]", call.getId()));
+ handleNewActiveCallCallbacks(call, ON_ANSWER, videoState);
+ } finally {
+ Log.endSession();
+ }
+ }
+
+ // need to create multiple transactions for onSetActive and onAnswer which both seek to set
+ // the call to active
+ private void handleNewActiveCallCallbacks(Call call, String action, int videoState) {
+ // save CallsManager state before sending client state changes
+ Call foregroundCallBeforeSwap = mCallsManager.getForegroundCall();
+ boolean wasActive = foregroundCallBeforeSwap != null && foregroundCallBeforeSwap.isActive();
+
+ // create 3 serial transactions:
+ // -- hold active
+ // -- set newCall as active
+ // -- ack from client
+ SerialTransaction serialTransactions = createSetActiveTransactions(call);
+ serialTransactions.appendTransaction(
+ new CallEventCallbackAckTransaction(mICallEventCallback,
+ action, call.getId(), videoState));
+
+ // do CallsManager workload before asking client and
+ // reset CallsManager state if client does NOT ack
+ mTransactionManager.addTransaction(serialTransactions, new OutcomeReceiver<>() {
+ @Override
+ public void onResult(VoipCallTransactionResult result) {
+ Log.i(TAG, String.format(Locale.US,
+ "%s: onResult: callId=[%s]", action, call.getId()));
+ }
+
+ @Override
+ public void onError(CallException exception) {
+ maybeResetForegroundCall(foregroundCallBeforeSwap, wasActive);
+ }
+ });
+ }
+
+
+ public void onSetInactive(Call call) {
+ try {
+ Log.startSession("TSW.oSI");
+ Log.i(TAG, String.format(Locale.US, "onSetInactive: callId=[%s]", call.getId()));
+ mTransactionManager.addTransaction(
+ new CallEventCallbackAckTransaction(mICallEventCallback,
+ ON_SET_INACTIVE, call.getId(), 0), new OutcomeReceiver<>() {
+ @Override
+ public void onResult(VoipCallTransactionResult result) {
+ mCallsManager.markCallAsOnHold(call);
+ }
+ @Override
+ public void onError(CallException exception) {
+ Log.i(TAG, "onSetInactive: onError: with e=[%e]", exception);
+ }
+ });
+ } finally {
+ Log.endSession();
+ }
+ }
+
+ public void onDisconnect(Call call, DisconnectCause cause) {
+ try {
+ Log.startSession("TSW.oD");
+ Log.d(TAG, String.format(Locale.US, "onDisconnect: callId=[%s]", call.getId()));
+
+ mTransactionManager.addTransaction(
+ new CallEventCallbackAckTransaction(mICallEventCallback, ON_DISCONNECT,
+ call.getId(), 0), new OutcomeReceiver<>() {
+ @Override
+ public void onResult(VoipCallTransactionResult result) {
+ removeCallFromCallsManager(call, cause);
+ }
+
+ @Override
+ public void onError(CallException exception) {
+ removeCallFromCallsManager(call, cause);
+ }
+ }
+ );
+ } finally {
+ Log.endSession();
+ }
+ }
+
+ public void onReject(Call call, @android.telecom.Call.RejectReason int rejectReason) {
+ try {
+ Log.startSession("TSW.oR");
+ Log.d(TAG, String.format(Locale.US, "onReject: callId=[%s]", call.getId()));
+
+ mTransactionManager.addTransaction(
+ new CallEventCallbackAckTransaction(mICallEventCallback, ON_REJECT,
+ call.getId(), 0), new OutcomeReceiver<>() {
+ @Override
+ public void onResult(VoipCallTransactionResult result) {
+ removeCallFromCallsManager(call,
+ new DisconnectCause(DisconnectCause.REJECTED));
+ }
+
+ @Override
+ public void onError(CallException exception) {
+ removeCallFromCallsManager(call,
+ new DisconnectCause(DisconnectCause.REJECTED));
+ }
+ }
+ );
+ } finally {
+ Log.endSession();
+ }
+ }
+
+ // TODO:: replace with onCallEndpointChanged when CLs are merged
+ public void onCallAudioStateChanged(Call call, CallAudioState callAudioState) {
+ if (call != null) {
+ try {
+ mICallEventCallback.onCallAudioStateChanged(call.getId(), callAudioState);
+ } catch (RemoteException e) {
+ }
+ }
+ }
+
+ public void removeCallFromWrappers(Call call) {
+ if (call != null) {
+ try {
+ // remove the call from frameworks wrapper (client side)
+ mICallEventCallback.removeCallFromTransactionalServiceWrapper(call.getId());
+ // remove the call from this class/wrapper (server side)
+ untrackCall(call);
+ } catch (RemoteException e) {
+ }
+ }
+ }
+
+ /***
+ *********************************************************************************************
+ ** Helpers **
+ **********************************************************************************************
+ */
+ private void maybeResetForegroundCall(Call foregroundCallBeforeSwap, boolean wasActive) {
+ if (foregroundCallBeforeSwap == null) {
+ return;
+ }
+ if (wasActive && !foregroundCallBeforeSwap.isActive()) {
+ mCallsManager.markCallAsActive(foregroundCallBeforeSwap);
+ }
+ }
+
+ private void removeCallFromCallsManager(Call call, DisconnectCause cause) {
+ if (cause.getCode() != DisconnectCause.REJECTED) {
+ mCallsManager.markCallAsDisconnected(call, cause);
+ }
+ mCallsManager.removeCall(call);
+ }
+
+ private SerialTransaction createSetActiveTransactions(Call call) {
+ // create list for multiple transactions
+ List<VoipCallTransaction> transactions = new ArrayList<>();
+
+ // add t1. hold potential active call
+ transactions.add(new HoldActiveCallForNewCallTransaction(mCallsManager, call));
+
+ // add t2. answer current call
+ transactions.add(new RequestFocusTransaction(mCallsManager, call));
+
+ // send off to Transaction Manager to process
+ return new SerialTransaction(transactions);
+ }
+
+ /***
+ *********************************************************************************************
+ ** FocusManager **
+ **********************************************************************************************
+ */
+
+ @Override
+ public void connectionServiceFocusLost() {
+ if (mConnSvrFocusListener != null) {
+ mConnSvrFocusListener.onConnectionServiceReleased(this);
+ }
+ Log.i(TAG, String.format(Locale.US, "connectionServiceFocusLost for package=[%s]",
+ mPackageName));
+ }
+
+ @Override
+ public void connectionServiceFocusGained() {
+ Log.i(TAG, String.format(Locale.US, "connectionServiceFocusGained for package=[%s]",
+ mPackageName));
+ }
+
+ @Override
+ public void setConnectionServiceFocusListener(
+ ConnectionServiceFocusManager.ConnectionServiceFocusListener listener) {
+ mConnSvrFocusListener = listener;
+ }
+
+ @Override
+ public ComponentName getComponentName() {
+ return mPhoneAccountHandle.getComponentName();
+ }
+}
diff --git a/src/com/android/server/telecom/voip/CallEventCallbackAckTransaction.java b/src/com/android/server/telecom/voip/CallEventCallbackAckTransaction.java
new file mode 100644
index 0000000..5ff4c8d
--- /dev/null
+++ b/src/com/android/server/telecom/voip/CallEventCallbackAckTransaction.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom.voip;
+
+import static android.telecom.TelecomManager.TELECOM_TRANSACTION_SUCCESS;
+import static android.telecom.CallException.CODE_OPERATION_TIMED_OUT;
+
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.os.ResultReceiver;
+import android.util.Log;
+
+import com.android.internal.telecom.ICallEventCallback;
+import com.android.server.telecom.TransactionalServiceWrapper;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * SRP: using the ICallEventCallback binder, reach out to the client for the pending call event and
+ * get an acknowledgement that the call event can be completed.
+ */
+public class CallEventCallbackAckTransaction extends VoipCallTransaction {
+ private static final String TAG = CallEventCallbackAckTransaction.class.getSimpleName();
+ private final ICallEventCallback mICallEventCallback;
+ private final String mAction;
+ private final String mCallId;
+ private final int mVideoState;
+ private final VoipCallTransactionResult TRANSACTION_FAILED = new VoipCallTransactionResult(
+ CODE_OPERATION_TIMED_OUT, "failed to complete the operation before timeout");
+
+ private static class AckResultReceiver extends ResultReceiver {
+ CountDownLatch mCountDownLatch;
+
+ public AckResultReceiver(CountDownLatch latch) {
+ super(null);
+ mCountDownLatch = latch;
+ }
+
+ @Override
+ protected void onReceiveResult(int resultCode, Bundle resultData) {
+ if (resultCode == TELECOM_TRANSACTION_SUCCESS) {
+ mCountDownLatch.countDown();
+ }
+ }
+ }
+
+ public CallEventCallbackAckTransaction(ICallEventCallback service, String action, String callId,
+ int videoState) {
+ mICallEventCallback = service;
+ mAction = action;
+ mCallId = callId;
+ mVideoState = videoState;
+ }
+
+
+ @Override
+ public CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
+ Log.d(TAG, "processTransaction");
+ CountDownLatch latch = new CountDownLatch(1);
+ ResultReceiver receiver = new AckResultReceiver(latch);
+
+ try {
+ switch (mAction) {
+ case TransactionalServiceWrapper.ON_SET_INACTIVE:
+ mICallEventCallback.onSetInactive(mCallId, receiver);
+ break;
+ case TransactionalServiceWrapper.ON_DISCONNECT:
+ mICallEventCallback.onDisconnect(mCallId, receiver);
+ break;
+ case TransactionalServiceWrapper.ON_REJECT:
+ mICallEventCallback.onReject(mCallId, receiver);
+ break;
+ case TransactionalServiceWrapper.ON_SET_ACTIVE:
+ mICallEventCallback.onSetActive(mCallId, receiver);
+ break;
+ case TransactionalServiceWrapper.ON_ANSWER:
+ mICallEventCallback.onAnswer(mCallId, mVideoState, receiver);
+ break;
+ }
+ } catch (RemoteException remoteException) {
+ return CompletableFuture.completedFuture(TRANSACTION_FAILED);
+ }
+
+ try {
+ // wait for the client to ack that CallEventCallback
+ boolean success = latch.await(VoipCallTransaction.TIMEOUT_LIMIT, TimeUnit.MILLISECONDS);
+ if (!success) {
+ // client send onError and failed to complete transaction
+ return CompletableFuture.completedFuture(TRANSACTION_FAILED);
+ } else {
+ // success
+ return CompletableFuture.completedFuture(
+ new VoipCallTransactionResult(VoipCallTransactionResult.RESULT_SUCCEED,
+ "success"));
+ }
+ } catch (InterruptedException ie) {
+ return CompletableFuture.completedFuture(TRANSACTION_FAILED);
+ }
+ }
+}
diff --git a/src/com/android/server/telecom/voip/EndCallTransaction.java b/src/com/android/server/telecom/voip/EndCallTransaction.java
new file mode 100644
index 0000000..9b738b3
--- /dev/null
+++ b/src/com/android/server/telecom/voip/EndCallTransaction.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom.voip;
+
+import android.telecom.DisconnectCause;
+import android.util.Log;
+
+import com.android.server.telecom.Call;
+import com.android.server.telecom.CallsManager;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
+
+/**
+ * This transaction should only be created for a CallControl action.
+ */
+public class EndCallTransaction extends VoipCallTransaction {
+ private static final String TAG = EndCallTransaction.class.getSimpleName();
+ private final CallsManager mCallsManager;
+ private final boolean mIsDisconnect;
+ private final int mCode;
+ private final Call mCall;
+
+ public EndCallTransaction(CallsManager callsManager, boolean isDisconnect, int code,
+ Call call) {
+ mCallsManager = callsManager;
+ mIsDisconnect = isDisconnect;
+ mCode = code;
+ mCall = call;
+ }
+
+ @Override
+ public CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
+ Log.d(TAG, String.format("processTransaction: isDisconnect=[%b]", mIsDisconnect));
+
+ if (mIsDisconnect) {
+ mCallsManager.markCallAsDisconnected(mCall, new DisconnectCause(mCode));
+ } else {
+ mCallsManager.rejectCall(mCall, mCode);
+ }
+ mCallsManager.markCallAsRemoved(mCall);
+
+ return CompletableFuture.completedFuture(
+ new VoipCallTransactionResult(VoipCallTransactionResult.RESULT_SUCCEED,
+ "endCallTransaction complete"));
+ }
+}
diff --git a/src/com/android/server/telecom/voip/HoldActiveCallForNewCallTransaction.java b/src/com/android/server/telecom/voip/HoldActiveCallForNewCallTransaction.java
new file mode 100644
index 0000000..c42cb07
--- /dev/null
+++ b/src/com/android/server/telecom/voip/HoldActiveCallForNewCallTransaction.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom.voip;
+
+import android.os.OutcomeReceiver;
+import android.telecom.CallException;
+import android.util.Log;
+
+import com.android.server.telecom.Call;
+import com.android.server.telecom.CallsManager;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
+
+public class HoldActiveCallForNewCallTransaction extends VoipCallTransaction {
+
+ private static final String TAG = HoldActiveCallForNewCallTransaction.class.getSimpleName();
+ private final CallsManager mCallsManager;
+ private final Call mCall;
+
+ public HoldActiveCallForNewCallTransaction(CallsManager callsManager, Call call) {
+ mCallsManager = callsManager;
+ mCall = call;
+ }
+
+ @Override
+ public CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
+ Log.d(TAG, "processTransaction");
+ CompletableFuture<VoipCallTransactionResult> future = new CompletableFuture<>();
+
+ mCallsManager.transactionHoldPotentialActiveCallForNewCall(mCall, new OutcomeReceiver<>() {
+ @Override
+ public void onResult(Boolean result) {
+ Log.d(TAG, "processTransaction: onResult");
+ future.complete(new VoipCallTransactionResult(
+ VoipCallTransactionResult.RESULT_SUCCEED, null));
+ }
+
+ @Override
+ public void onError(CallException exception) {
+ Log.d(TAG, "processTransaction: onError");
+ future.complete(new VoipCallTransactionResult(
+ VoipCallTransactionResult.RESULT_FAILED, null));
+ }
+ });
+
+ return future;
+ }
+}
diff --git a/src/com/android/server/telecom/voip/HoldCallTransaction.java b/src/com/android/server/telecom/voip/HoldCallTransaction.java
new file mode 100644
index 0000000..530727a
--- /dev/null
+++ b/src/com/android/server/telecom/voip/HoldCallTransaction.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom.voip;
+
+import android.util.Log;
+
+import com.android.server.telecom.Call;
+import com.android.server.telecom.CallsManager;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
+
+public class HoldCallTransaction extends VoipCallTransaction {
+
+ private static final String TAG = HoldCallTransaction.class.getSimpleName();
+ private final CallsManager mCallsManager;
+ private final Call mCall;
+
+ public HoldCallTransaction(CallsManager callsManager, Call call) {
+ mCallsManager = callsManager;
+ mCall = call;
+ }
+
+ @Override
+ public CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
+ Log.d(TAG, "processTransaction");
+
+ mCallsManager.markCallAsOnHold(mCall);
+
+ return CompletableFuture.completedFuture(
+ new VoipCallTransactionResult(VoipCallTransactionResult.RESULT_SUCCEED,
+ "holdCallTransaction complete"));
+ }
+}
diff --git a/src/com/android/server/telecom/voip/IncomingCallTransaction.java b/src/com/android/server/telecom/voip/IncomingCallTransaction.java
new file mode 100644
index 0000000..7bb9736
--- /dev/null
+++ b/src/com/android/server/telecom/voip/IncomingCallTransaction.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom.voip;
+
+import static android.telecom.CallAttributes.CALL_CAPABILITIES_KEY;
+
+import android.os.Bundle;
+import android.telecom.CallAttributes;
+import android.telecom.CallControl;
+import android.telecom.CallException;
+import android.telecom.TelecomManager;
+import android.util.Log;
+
+import com.android.server.telecom.Call;
+import com.android.server.telecom.CallsManager;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
+
+public class IncomingCallTransaction extends VoipCallTransaction {
+
+ private static final String TAG = IncomingCallTransaction.class.getSimpleName();
+ private final String mCallId;
+ private final CallAttributes mCallAttributes;
+ private final CallsManager mCallsManager;
+
+ public IncomingCallTransaction(String callId, CallAttributes callAttributes,
+ CallsManager callsManager) {
+ mCallId = callId;
+ mCallAttributes = callAttributes;
+ mCallsManager = callsManager;
+ }
+
+ @Override
+ public CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
+ Log.d(TAG, "processTransaction");
+
+ if (mCallsManager.isIncomingCallPermitted(mCallAttributes.getPhoneAccountHandle())) {
+ Log.d(TAG, "processTransaction: incoming call permitted");
+
+ Call call = mCallsManager.processIncomingCallIntent(
+ mCallAttributes.getPhoneAccountHandle(),
+ generateExtras(mCallAttributes), false);
+
+ return CompletableFuture.completedFuture(
+ new VoipCallTransactionResult(
+ VoipCallTransactionResult.RESULT_SUCCEED, call, "success"));
+ } else {
+ Log.d(TAG, "processTransaction: incoming call is not permitted at this time");
+
+ return CompletableFuture.completedFuture(
+ new VoipCallTransactionResult(
+ CallException.CODE_CALL_NOT_PERMITTED_AT_PRESENT_TIME,
+ "incoming call not permitted at the current time"));
+ }
+ }
+
+ private Bundle generateExtras(CallAttributes callAttributes){
+ Bundle extras = new Bundle();
+ extras.putString(TelecomManager.TRANSACTION_CALL_ID_KEY, mCallId);
+ extras.putInt(CALL_CAPABILITIES_KEY, callAttributes.getCallCapabilities());
+ extras.putInt(TelecomManager.EXTRA_INCOMING_VIDEO_STATE, callAttributes.getCallType());
+ return extras;
+ }
+}
diff --git a/src/com/android/server/telecom/voip/OutgoingCallTransaction.java b/src/com/android/server/telecom/voip/OutgoingCallTransaction.java
new file mode 100644
index 0000000..169fc48
--- /dev/null
+++ b/src/com/android/server/telecom/voip/OutgoingCallTransaction.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom.voip;
+
+import static android.Manifest.permission.CALL_PRIVILEGED;
+import static android.telecom.CallAttributes.CALL_CAPABILITIES_KEY;
+import static android.telecom.CallException.CODE_CALL_NOT_PERMITTED_AT_PRESENT_TIME;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import android.telecom.CallAttributes;
+import android.telecom.TelecomManager;
+import android.util.Log;
+
+import com.android.server.telecom.Call;
+import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.LoggedHandlerExecutor;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
+
+public class OutgoingCallTransaction extends VoipCallTransaction {
+
+ private static final String TAG = OutgoingCallTransaction.class.getSimpleName();
+ private final String mCallId;
+ private final Context mContext;
+ private final String mCallingPackage;
+ private final CallAttributes mCallAttributes;
+ private final CallsManager mCallsManager;
+
+ public OutgoingCallTransaction(String callId, Context context, CallAttributes callAttributes,
+ CallsManager callsManager) {
+ mCallId = callId;
+ mContext = context;
+ mCallingPackage = mContext.getOpPackageName();
+ mCallAttributes = callAttributes;
+ mCallsManager = callsManager;
+ }
+
+ @Override
+ public CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
+ Log.d(TAG, "processTransaction");
+
+ final boolean hasCallPrivilegedPermission = mContext.checkCallingPermission(
+ CALL_PRIVILEGED) == PackageManager.PERMISSION_GRANTED;
+
+ final Intent intent = new Intent(hasCallPrivilegedPermission ?
+ Intent.ACTION_CALL_PRIVILEGED : Intent.ACTION_CALL, mCallAttributes.getAddress());
+
+ if (mCallsManager.isOutgoingCallPermitted(mCallAttributes.getPhoneAccountHandle())) {
+ Log.d(TAG, "processTransaction: outgoing call permitted");
+
+ CompletableFuture<Call> callFuture =
+ mCallsManager.startOutgoingCall(mCallAttributes.getAddress(),
+ mCallAttributes.getPhoneAccountHandle(),
+ generateExtras(mCallAttributes),
+ mCallAttributes.getPhoneAccountHandle().getUserHandle(),
+ intent,
+ mCallingPackage);
+
+ if (callFuture == null) {
+ return CompletableFuture.completedFuture(
+ new VoipCallTransactionResult(
+ CODE_CALL_NOT_PERMITTED_AT_PRESENT_TIME,
+ "incoming call not permitted at the current time"));
+ }
+ CompletionStage<VoipCallTransactionResult> result = callFuture.thenComposeAsync(
+ (call) -> {
+
+ Log.d(TAG, "processTransaction: completing future");
+
+ if (call == null) {
+ Log.d(TAG, "processTransaction: call is null");
+ return CompletableFuture.completedFuture(
+ new VoipCallTransactionResult(
+ CODE_CALL_NOT_PERMITTED_AT_PRESENT_TIME,
+ "call could not be created at this time"));
+ } else {
+ Log.d(TAG, "processTransaction: call done. id=" + call.getId());
+ }
+
+ return CompletableFuture.completedFuture(
+ new VoipCallTransactionResult(
+ VoipCallTransactionResult.RESULT_SUCCEED,
+ call, null));
+ }
+ , new LoggedHandlerExecutor(mHandler, "OCT.pT", null));
+
+ return result;
+ } else {
+ return CompletableFuture.completedFuture(
+ new VoipCallTransactionResult(
+ CODE_CALL_NOT_PERMITTED_AT_PRESENT_TIME,
+ "incoming call not permitted at the current time"));
+
+ }
+ }
+
+ private Bundle generateExtras(CallAttributes callAttributes){
+ Bundle extras = new Bundle();
+ extras.setDefusable(true);
+ extras.putString(TelecomManager.TRANSACTION_CALL_ID_KEY, mCallId);
+ extras.putInt(CALL_CAPABILITIES_KEY, callAttributes.getCallCapabilities());
+ extras.putInt(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE,
+ callAttributes.getCallType());
+ return extras;
+ }
+}
diff --git a/src/com/android/server/telecom/voip/RequestFocusTransaction.java b/src/com/android/server/telecom/voip/RequestFocusTransaction.java
new file mode 100644
index 0000000..210bd37
--- /dev/null
+++ b/src/com/android/server/telecom/voip/RequestFocusTransaction.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom.voip;
+
+import android.os.OutcomeReceiver;
+import android.telecom.CallException;
+import android.util.Log;
+
+import com.android.server.telecom.Call;
+import com.android.server.telecom.CallsManager;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
+
+public class RequestFocusTransaction extends VoipCallTransaction {
+
+ private static final String TAG = RequestFocusTransaction.class.getSimpleName();
+ private final CallsManager mCallsManager;
+ private final Call mCall;
+
+ public RequestFocusTransaction(CallsManager callsManager, Call call) {
+ mCallsManager = callsManager;
+ mCall = call;
+ }
+
+ @Override
+ public CompletionStage<VoipCallTransactionResult> processTransaction(Void v) {
+ Log.d(TAG, "processTransaction");
+ CompletableFuture<VoipCallTransactionResult> future = new CompletableFuture<>();
+
+ mCallsManager.transactionRequestNewFocusCall(mCall, new OutcomeReceiver<>() {
+ @Override
+ public void onResult(Boolean result) {
+ Log.d(TAG, "processTransaction: onResult");
+ future.complete(new VoipCallTransactionResult(
+ VoipCallTransactionResult.RESULT_SUCCEED, null));
+ }
+
+ @Override
+ public void onError(CallException exception) {
+ Log.d(TAG, "processTransaction: onError");
+
+ future.complete(new VoipCallTransactionResult(
+ VoipCallTransactionResult.RESULT_FAILED, null));
+ }
+ });
+
+ return future;
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/server/telecom/voip/SerialTransaction.java b/src/com/android/server/telecom/voip/SerialTransaction.java
index 9fe547c..87bc9ec 100644
--- a/src/com/android/server/telecom/voip/SerialTransaction.java
+++ b/src/com/android/server/telecom/voip/SerialTransaction.java
@@ -26,6 +26,10 @@
super(subTransactions);
}
+ public void appendTransaction(VoipCallTransaction transaction){
+ mSubTransactions.add(transaction);
+ }
+
@Override
public void start() {
// post timeout work
diff --git a/src/com/android/server/telecom/voip/TransactionManager.java b/src/com/android/server/telecom/voip/TransactionManager.java
index 27e8069..a0955c8 100644
--- a/src/com/android/server/telecom/voip/TransactionManager.java
+++ b/src/com/android/server/telecom/voip/TransactionManager.java
@@ -17,6 +17,9 @@
package com.android.server.telecom.voip;
import android.os.OutcomeReceiver;
+import android.telecom.TelecomManager;
+import android.telecom.CallException;
+import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
@@ -38,7 +41,6 @@
private TransactionManager() {
mTransactions = new ArrayDeque<>();
mCurrentTransaction = null;
-
}
public static TransactionManager getInstance() {
@@ -56,24 +58,27 @@
}
public void addTransaction(VoipCallTransaction transaction,
- OutcomeReceiver<VoipCallTransactionResult, Exception> receiver) {
+ OutcomeReceiver<VoipCallTransactionResult, CallException> receiver) {
synchronized (sLock) {
mTransactions.add(transaction);
transaction.setCompleteListener(new TransactionCompleteListener() {
@Override
public void onTransactionCompleted(VoipCallTransactionResult result,
- String transactionName) {
- if (result.getResult() == 0
- /* TODO: change this to static value in TelecomManager */) {
+ String transactionName){
+ Log.i(TAG, String.format("transaction completed: with result=[%d]",
+ result.getResult()));
+ if (result.getResult() == TelecomManager.TELECOM_TRANSACTION_SUCCESS) {
receiver.onResult(result);
} else {
- receiver.onError(new Exception());
+ receiver.onError(
+ new CallException(result.getMessage(),
+ result.getResult()));
}
finishTransaction();
}
@Override
- public void onTransactionTimeout(String transactionName) {
+ public void onTransactionTimeout(String transactionName){
receiver.onResult(new VoipCallTransactionResult(
VoipCallTransactionResult.RESULT_FAILED, transactionName + " timeout"));
finishTransaction();
diff --git a/src/com/android/server/telecom/voip/VoipCallTransactionResult.java b/src/com/android/server/telecom/voip/VoipCallTransactionResult.java
index c610e06..2916fc6 100644
--- a/src/com/android/server/telecom/voip/VoipCallTransactionResult.java
+++ b/src/com/android/server/telecom/voip/VoipCallTransactionResult.java
@@ -47,6 +47,10 @@
return mMessage;
}
+ public Call getCall(){
+ return mCall;
+ }
+
@Override
public boolean equals(Object o) {
if (this == o) return true;
@@ -59,4 +63,15 @@
public int hashCode() {
return Objects.hash(mResult, mMessage);
}
+
+ @Override
+ public String toString() {
+ return new StringBuilder().
+ append("{ VoipCallTransactionResult: [mResult: ").
+ append(mResult).
+ append("], [mCall: ").
+ append(mCall.toString()).
+ append("], [mMessage=").
+ append(mMessage).append("] }").toString();
+ }
}
diff --git a/testapps/transactionalVoipApp/Android.bp b/testapps/transactionalVoipApp/Android.bp
new file mode 100644
index 0000000..68089e2
--- /dev/null
+++ b/testapps/transactionalVoipApp/Android.bp
@@ -0,0 +1,28 @@
+//
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+ name: "transactionalVoipApp",
+ static_libs: [
+ "androidx.legacy_legacy-support-v4",
+ "guava",
+ ],
+ srcs: ["src/**/*.java"],
+}
diff --git a/testapps/transactionalVoipApp/AndroidManifest.xml b/testapps/transactionalVoipApp/AndroidManifest.xml
new file mode 100644
index 0000000..d0aa50b
--- /dev/null
+++ b/testapps/transactionalVoipApp/AndroidManifest.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ coreApp="true"
+ package="com.android.server.telecom.transactionalVoipApp">
+
+ <uses-sdk android:minSdkVersion="28"
+ android:targetSdkVersion="33"/>
+
+ <uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />
+
+ <application android:label="Transactional Voip">
+ <uses-library android:name="android.test.runner"/>
+
+ <activity android:name="com.android.server.telecom.transactionalVoipApp.VoipAppMainActivity"
+ android:exported="true"
+ android:label="Transactional Voip">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+
+ </application>
+</manifest>
diff --git a/testapps/transactionalVoipApp/res/drawable-hdpi/ic_android_black_24dp.png b/testapps/transactionalVoipApp/res/drawable-hdpi/ic_android_black_24dp.png
new file mode 100644
index 0000000..ed3ee45
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/drawable-hdpi/ic_android_black_24dp.png
Binary files differ
diff --git a/testapps/transactionalVoipApp/res/drawable-mdpi/ic_android_black_24dp.png b/testapps/transactionalVoipApp/res/drawable-mdpi/ic_android_black_24dp.png
new file mode 100644
index 0000000..a4add51
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/drawable-mdpi/ic_android_black_24dp.png
Binary files differ
diff --git a/testapps/transactionalVoipApp/res/drawable-xhdpi/ic_android_black_24dp.png b/testapps/transactionalVoipApp/res/drawable-xhdpi/ic_android_black_24dp.png
new file mode 100644
index 0000000..41558f2
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/drawable-xhdpi/ic_android_black_24dp.png
Binary files differ
diff --git a/testapps/transactionalVoipApp/res/drawable-xxhdpi/ic_android_black_24dp.png b/testapps/transactionalVoipApp/res/drawable-xxhdpi/ic_android_black_24dp.png
new file mode 100644
index 0000000..6006b12
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/drawable-xxhdpi/ic_android_black_24dp.png
Binary files differ
diff --git a/testapps/transactionalVoipApp/res/drawable-xxxhdpi/ic_android_black_24dp.png b/testapps/transactionalVoipApp/res/drawable-xxxhdpi/ic_android_black_24dp.png
new file mode 100644
index 0000000..4f935bf
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/drawable-xxxhdpi/ic_android_black_24dp.png
Binary files differ
diff --git a/testapps/transactionalVoipApp/res/layout/main_activity.xml b/testapps/transactionalVoipApp/res/layout/main_activity.xml
new file mode 100644
index 0000000..86d8e20
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/layout/main_activity.xml
@@ -0,0 +1,90 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2022 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/app_name"/>
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <Button
+ android:id="@+id/registerButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/register_phone_account"/>
+
+ <ToggleButton
+ android:id="@+id/callDirectionButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textOff="@string/direction_outgoing"
+ android:textOn="@string/direction_incoming"
+ />
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+ <Button
+ android:id="@+id/add_call_1_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/add_call_1"/>
+
+ <Button
+ android:id="@+id/disconnect_call_1_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/disconnect_call_1"/>
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <Button
+ android:id="@+id/add_call_2_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/add_call_2"/>
+
+ <Button
+ android:id="@+id/set_call_2_active_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/set_call_active"/>
+
+ <Button
+ android:id="@+id/disconnect_call_2_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/disconnect_call_2"/>
+ </LinearLayout>
+ </LinearLayout>
+</LinearLayout>
diff --git a/testapps/transactionalVoipApp/res/values/strings.xml b/testapps/transactionalVoipApp/res/values/strings.xml
new file mode 100644
index 0000000..038adc1
--- /dev/null
+++ b/testapps/transactionalVoipApp/res/values/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2022 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources>
+ <string name="app_name">Transactional API test Activity</string>
+ <string name="register_phone_account">Register Phone Account</string>
+ <string name="direction_outgoing">outgoing</string>
+ <string name="direction_incoming">incoming</string>
+ <string name="add_call_1">add call 1</string>
+ <string name="disconnect_call_1">disconnect call 1</string>
+ <string name="add_call_2">add call 2</string>
+ <string name="set_call_active">set call 2 active</string>
+ <string name="disconnect_call_2">disconnect call 2</string>
+
+</resources>
\ No newline at end of file
diff --git a/testapps/transactionalVoipApp/src/com/android/server/telecom/transactionalVoipApp/MyVoipCall.java b/testapps/transactionalVoipApp/src/com/android/server/telecom/transactionalVoipApp/MyVoipCall.java
new file mode 100644
index 0000000..5244be8
--- /dev/null
+++ b/testapps/transactionalVoipApp/src/com/android/server/telecom/transactionalVoipApp/MyVoipCall.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom.transactionalVoipApp;
+
+
+import android.telecom.CallAudioState;
+import android.telecom.CallControl;
+import android.telecom.CallEventCallback;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+
+import java.util.function.Consumer;
+
+public class MyVoipCall implements CallEventCallback {
+
+ private static final String TAG = "MyVoipCall";
+ private final String mCallId;
+ CallControl mCallControl;
+
+ MyVoipCall(String id) {
+ mCallId = id;
+ }
+
+ public void onAddCallControl(@NonNull CallControl callControl) {
+ mCallControl = callControl;
+ }
+
+ @Override
+ public void onSetActive(@NonNull Consumer<Boolean> wasCompleted) {
+ Log.i(TAG, String.format("onSetActive: callId=[%s]", mCallId));
+ wasCompleted.accept(Boolean.TRUE);
+ }
+
+ @Override
+ public void onSetInactive(@NonNull Consumer<Boolean> wasCompleted) {
+ Log.i(TAG, String.format("onSetInactive: callId=[%s]", mCallId));
+ wasCompleted.accept(Boolean.TRUE);
+ }
+
+ @Override
+ public void onAnswer(int videoState, @NonNull Consumer<Boolean> wasCompleted) {
+ Log.i(TAG, String.format("onAnswer: callId=[%s]", mCallId));
+ wasCompleted.accept(Boolean.TRUE);
+ }
+
+ @Override
+ public void onReject(@NonNull Consumer<Boolean> wasCompleted) {
+ Log.i(TAG, String.format("onReject: callId=[%s]", mCallId));
+ wasCompleted.accept(Boolean.TRUE);
+ }
+
+ @Override
+ public void onDisconnect(@NonNull Consumer<Boolean> wasCompleted) {
+ Log.i(TAG, String.format("onDisconnect: callId=[%s]", mCallId));
+ wasCompleted.accept(Boolean.TRUE);
+ }
+
+ @Override
+ public void onCallAudioStateChanged(@NonNull CallAudioState callAudioState) {
+ Log.i(TAG, String.format("onCallAudioStateChanged: state=[%s]", callAudioState.toString()));
+ }
+}
diff --git a/testapps/transactionalVoipApp/src/com/android/server/telecom/transactionalVoipApp/VoipAppMainActivity.java b/testapps/transactionalVoipApp/src/com/android/server/telecom/transactionalVoipApp/VoipAppMainActivity.java
new file mode 100644
index 0000000..775947b
--- /dev/null
+++ b/testapps/transactionalVoipApp/src/com/android/server/telecom/transactionalVoipApp/VoipAppMainActivity.java
@@ -0,0 +1,169 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom.transactionalVoipApp;
+
+import static android.telecom.CallAttributes.DIRECTION_INCOMING;
+import static android.telecom.CallAttributes.DIRECTION_OUTGOING;
+
+import android.app.Activity;
+import android.content.ComponentName;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.OutcomeReceiver;
+import android.telecom.CallAttributes;
+import android.telecom.CallControl;
+import android.telecom.CallException;
+import android.telecom.DisconnectCause;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import android.util.Log;
+import android.view.View;
+import android.widget.ToggleButton;
+
+public class VoipAppMainActivity extends Activity {
+
+ private static final String TAG = "VoipAppMainActivity";
+ private static TelecomManager mTelecomManager;
+ private MyVoipCall mCall1;
+ private MyVoipCall mCall2;
+ private ToggleButton mCallDirectionButton;
+
+ PhoneAccountHandle handle = new PhoneAccountHandle(
+ new ComponentName("com.android.server.telecom.transactionalVoipApp",
+ "com.android.server.telecom.transactionalVoipApp.VoipAppMainActivity"), "123");
+
+ PhoneAccount mPhoneAccount = PhoneAccount.builder(handle, "test label")
+ .setCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED |
+ PhoneAccount.CAPABILITY_SUPPORTS_TRANSACTIONAL_OPERATIONS).build();
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.main_activity);
+
+ mTelecomManager = getSystemService(TelecomManager.class);
+ mCallDirectionButton = findViewById(R.id.callDirectionButton);
+
+ // register account
+ findViewById(R.id.registerButton).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mTelecomManager.registerPhoneAccount(mPhoneAccount);
+ }
+ });
+
+ // call 1 buttons
+ findViewById(R.id.add_call_1_button).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Bundle extras = new Bundle();
+ extras.putString("testKey", "testValue");
+ mCall1 = new MyVoipCall("1");
+ addCall(mCall1, true);
+ }
+ });
+
+ findViewById(R.id.disconnect_call_1_button).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ disconnectCall(mCall1);
+ }
+ });
+
+
+ //call 2 buttons
+ findViewById(R.id.add_call_2_button).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Bundle extras = new Bundle();
+ extras.putString("call2extraKey", "call2Value");
+ mCall2 = new MyVoipCall("2");
+ addCall(mCall2, false);
+ }
+ });
+
+ findViewById(R.id.set_call_2_active_button).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ setCallActive(mCall2);
+ }
+ });
+
+ findViewById(R.id.disconnect_call_2_button).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ disconnectCall(mCall2);
+ }
+ });
+ }
+
+ private void addCall(MyVoipCall call, boolean setActive) {
+ int direction = (mCallDirectionButton.isChecked() ? DIRECTION_INCOMING
+ : DIRECTION_OUTGOING);
+
+ CallAttributes callAttributes = new CallAttributes.Builder(handle, direction, "Alan Turing",
+ Uri.fromParts("tel", "abc", "123")).build();
+
+ mTelecomManager.addCall(callAttributes, Runnable::run,
+ new OutcomeReceiver<CallControl, CallException>() {
+ @Override
+ public void onResult(CallControl callControl) {
+ Log.i(TAG, "addCall: onResult: callback fired");
+ call.onAddCallControl(callControl);
+ if (setActive) {
+ setCallActive(call);
+ }
+ }
+
+ @Override
+ public void onError(CallException exception) {
+
+ }
+ },
+ call);
+ }
+
+ private void setCallActive(MyVoipCall call) {
+ call.mCallControl.setActive(Runnable::run, new OutcomeReceiver<Void, CallException>() {
+ @Override
+ public void onResult(Void result) {
+ Log.i(TAG, "setCallActive: onResult");
+ }
+
+ @Override
+ public void onError(CallException exception) {
+ Log.i(TAG, "setCallActive: onError");
+ }
+ });
+ }
+
+ private void disconnectCall(MyVoipCall call) {
+ call.mCallControl.disconnect(new DisconnectCause(DisconnectCause.LOCAL), Runnable::run,
+ new OutcomeReceiver<Void, CallException>() {
+ @Override
+ public void onResult(Void result) {
+ Log.i(TAG, "disconnectCall: onResult");
+ }
+
+ @Override
+ public void onError(CallException exception) {
+ Log.i(TAG, "disconnectCall: onError");
+ }
+ });
+ }
+}
diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml
index e8c69d4..d9756fa 100644
--- a/tests/AndroidManifest.xml
+++ b/tests/AndroidManifest.xml
@@ -21,13 +21,13 @@
<uses-sdk
android:minSdkVersion="23"
- android:targetSdkVersion="23" />
+ android:targetSdkVersion="33" />
<!-- TODO: Needed because we call BluetoothAdapter.getDefaultAdapter() statically, and
BluetoothAdapter is a final class. -->
<uses-permission android:name="android.permission.BLUETOOTH" />
+ <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_PRIVILEGED" />
-
<!-- TODO: Needed because we call ActivityManager.getCurrentUser() statically. -->
<uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" />
<uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" />
diff --git a/tests/src/com/android/server/telecom/tests/CallAttributesTests.java b/tests/src/com/android/server/telecom/tests/CallAttributesTests.java
new file mode 100644
index 0000000..acb913e
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/CallAttributesTests.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom.tests;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+import static org.mockito.Mockito.isA;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.content.ComponentName;
+import android.net.Uri;
+import android.os.Parcel;
+import android.telecom.CallAttributes;
+import android.telecom.PhoneAccountHandle;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+public class CallAttributesTests extends TelecomTestCase {
+
+ private static final PhoneAccountHandle mHandle = new PhoneAccountHandle(
+ new ComponentName("foo", "bar"), "1");
+ private static final String TEST_NAME = "Larry Page";
+ private static final Uri TEST_URI = Uri.fromParts("tel", "abc", "123");
+ @Mock private Parcel mParcel;
+
+ @Override
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ MockitoAnnotations.initMocks(this);
+ }
+
+ @Override
+ @After
+ public void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ @Test
+ public void testRequiredAttributes() {
+ CallAttributes callAttributes = new CallAttributes.Builder(mHandle,
+ CallAttributes.DIRECTION_OUTGOING, TEST_NAME, TEST_URI).build();
+
+ assertEquals(CallAttributes.DIRECTION_OUTGOING, callAttributes.getDirection());
+ assertEquals(mHandle, callAttributes.getPhoneAccountHandle());
+ }
+
+ @Test
+ public void testInvalidDirectionAttributes() {
+ assertThrows(IllegalArgumentException.class, () ->
+ new CallAttributes.Builder(mHandle, -1, TEST_NAME, TEST_URI).build()
+ );
+ }
+
+ @Test
+ public void testInvalidCallType() {
+ assertThrows(IllegalArgumentException.class, () ->
+ new CallAttributes.Builder(mHandle, CallAttributes.DIRECTION_OUTGOING,
+ TEST_NAME, TEST_URI).setCallType(-1).build()
+ );
+ }
+
+ @Test
+ public void testOptionalAttributes() {
+ CallAttributes callAttributes = new CallAttributes.Builder(mHandle,
+ CallAttributes.DIRECTION_OUTGOING, TEST_NAME, TEST_URI)
+ .setCallCapabilities(CallAttributes.SUPPORTS_SET_INACTIVE)
+ .setCallType(CallAttributes.AUDIO_CALL)
+ .build();
+
+ assertEquals(CallAttributes.DIRECTION_OUTGOING, callAttributes.getDirection());
+ assertEquals(mHandle, callAttributes.getPhoneAccountHandle());
+ assertEquals(CallAttributes.SUPPORTS_SET_INACTIVE, callAttributes.getCallCapabilities());
+ assertEquals(CallAttributes.AUDIO_CALL, callAttributes.getCallType());
+ assertEquals(TEST_URI, callAttributes.getAddress());
+ assertEquals(TEST_NAME, callAttributes.getDisplayName());
+ }
+
+ @Test
+ public void testDescribeContents() {
+ CallAttributes callAttributes = new CallAttributes.Builder(mHandle,
+ CallAttributes.DIRECTION_OUTGOING, TEST_NAME, TEST_URI).build();
+
+ assertEquals(0, callAttributes.describeContents());
+ }
+
+ @Test
+ public void testWriteToParcel() {
+ // GIVEN
+ CallAttributes callAttributes = new CallAttributes.Builder(mHandle,
+ CallAttributes.DIRECTION_OUTGOING, TEST_NAME, TEST_URI)
+ .setCallCapabilities(CallAttributes.SUPPORTS_SET_INACTIVE)
+ .setCallType(CallAttributes.AUDIO_CALL)
+ .build();
+
+ // WHEN
+ callAttributes.writeToParcel(mParcel, 0);
+
+ // THEN
+ verify(mParcel, times(1))
+ .writeParcelable(isA(PhoneAccountHandle.class), isA(Integer.class));
+ verify(mParcel, times(1)).writeCharSequence(isA(CharSequence.class));
+ verify(mParcel, times(1))
+ .writeParcelable(isA(Uri.class), isA(Integer.class));
+ verify(mParcel, times(3)).writeInt(isA(Integer.class));
+ }
+}
diff --git a/tests/src/com/android/server/telecom/tests/CallControlTest.java b/tests/src/com/android/server/telecom/tests/CallControlTest.java
new file mode 100644
index 0000000..279fcc8
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/CallControlTest.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom.tests;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.content.ComponentName;
+import android.os.OutcomeReceiver;
+import android.telecom.CallControl;
+import android.telecom.CallException;
+import android.telecom.PhoneAccountHandle;
+
+import com.android.internal.telecom.ClientTransactionalServiceRepository;
+import com.android.internal.telecom.ICallControl;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.UUID;
+
+public class CallControlTest extends TelecomTestCase {
+
+ private static final PhoneAccountHandle mHandle = new PhoneAccountHandle(
+ new ComponentName("foo", "bar"), "1");
+
+ @Mock
+ private ICallControl mICallControl;
+ @Mock
+ private ClientTransactionalServiceRepository mRepository;
+ private static final String CALL_ID_1 = UUID.randomUUID().toString();
+
+ @Override
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ MockitoAnnotations.initMocks(this);
+ }
+
+ @Override
+ @After
+ public void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ @Test
+ public void testGetCallId() {
+ CallControl control = new CallControl(CALL_ID_1, mICallControl, mRepository, mHandle);
+ assertEquals(CALL_ID_1, control.getCallId().toString());
+ }
+
+ @Test
+ public void testCallControlHitsIllegalStateException() {
+ CallControl control = new CallControl(CALL_ID_1, null, mRepository, mHandle);
+ assertThrows(IllegalStateException.class, () ->
+ control.setInactive(Runnable::run, result -> {
+ }));
+ }
+
+ @Test
+ public void testClose() {
+ // GIVEN
+ CallControl control = new CallControl(CALL_ID_1, mICallControl, mRepository, mHandle);
+
+ // WHEN
+ control.close();
+
+ // THEN
+ verify(mRepository, times(1))
+ .removeCallFromServiceWrapper(mHandle, CALL_ID_1);
+ }
+}
diff --git a/tests/src/com/android/server/telecom/tests/CallExceptionTests.java b/tests/src/com/android/server/telecom/tests/CallExceptionTests.java
new file mode 100644
index 0000000..3f8336d
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/CallExceptionTests.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom.tests;
+
+import static android.telecom.CallException.CODE_CALL_CANNOT_BE_SET_TO_ACTIVE;
+
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.isA;
+import static org.junit.Assert.assertEquals;
+
+import android.os.Parcel;
+import android.telecom.CallException;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+public class CallExceptionTests extends TelecomTestCase {
+
+ @Mock private Parcel mParcel;
+
+ @Override
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ MockitoAnnotations.initMocks(this);
+ }
+
+ @Override
+ @After
+ public void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ @Test
+ public void testSimpleException() {
+ String message = "test message";
+ CallException exception = new CallException(message);
+ assertTrue(exception.getMessage().contains(message));
+ assertEquals(CallException.CODE_ERROR_UNKNOWN, exception.getCode());
+ }
+
+ @Test
+ public void testExceptionWithCode() {
+ String message = "test message";
+ CallException exception = new CallException(message, CODE_CALL_CANNOT_BE_SET_TO_ACTIVE);
+ assertTrue(exception.getMessage().contains(message));
+ assertEquals(CODE_CALL_CANNOT_BE_SET_TO_ACTIVE, exception.getCode());
+ }
+
+ @Test
+ public void testDescribeContents() {
+ String message = "test message";
+ CallException exception = new CallException(message);
+ assertEquals(0, exception.describeContents());
+ }
+
+ @Test
+ public void testWriteToParcel() {
+ // GIVEN
+ String message = "test message";
+ CallException exception = new CallException(message);
+
+ // WHEN
+ exception.writeToParcel(mParcel, 0);
+
+ // THEN
+ verify(mParcel, times(1)).writeString8(isA(String.class));
+ verify(mParcel, times(1)).writeInt(isA(Integer.class));
+ }
+}
diff --git a/tests/src/com/android/server/telecom/tests/CallTest.java b/tests/src/com/android/server/telecom/tests/CallTest.java
index 0492f0a..a7619ad 100644
--- a/tests/src/com/android/server/telecom/tests/CallTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallTest.java
@@ -18,15 +18,18 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.verifyZeroInteractions;
@@ -54,6 +57,7 @@
import com.android.server.telecom.PhoneAccountRegistrar;
import com.android.server.telecom.PhoneNumberUtilsAdapter;
import com.android.server.telecom.TelecomSystem;
+import com.android.server.telecom.TransactionalServiceWrapper;
import com.android.server.telecom.ui.ToastFactory;
import org.junit.After;
@@ -87,6 +91,7 @@
@Mock private Toast mMockToast;
@Mock private PhoneNumberUtilsAdapter mMockPhoneNumberUtilsAdapter;
@Mock private ConnectionServiceWrapper mMockConnectionService;
+ @Mock private TransactionalServiceWrapper mMockTransactionalService;
private final TelecomSystem.SyncRoot mLock = new TelecomSystem.SyncRoot() { };
@@ -372,4 +377,87 @@
assertTrue(call.wasDndCheckComputedForCall());
assertTrue(call.isCallSuppressedByDoNotDisturb());
}
+
+ @Test
+ public void testGetConnectionServiceWrapper() {
+ Call call = new Call(
+ "1", /* callId */
+ mContext,
+ mMockCallsManager,
+ mLock,
+ null /* ConnectionServiceRepository */,
+ mMockPhoneNumberUtilsAdapter,
+ TEST_ADDRESS,
+ null /* GatewayInfo */,
+ null /* connectionManagerPhoneAccountHandle */,
+ SIM_1_HANDLE,
+ Call.CALL_DIRECTION_UNDEFINED,
+ false /* shouldAttachToExistingConnection*/,
+ true /* isConference */,
+ mMockClockProxy,
+ mMockToastProxy);
+
+ assertNull(call.getConnectionServiceWrapper());
+ assertFalse(call.isTransactionalCall());
+ call.setConnectionService(mMockConnectionService);
+ assertEquals(mMockConnectionService, call.getConnectionServiceWrapper());
+ call.setIsTransactionalCall(true);
+ assertTrue(call.isTransactionalCall());
+ assertNull(call.getConnectionServiceWrapper());
+ call.setTransactionServiceWrapper(mMockTransactionalService);
+ assertEquals(mMockTransactionalService, call.getTransactionServiceWrapper());
+ }
+
+ @Test
+ public void testCallEventCallbacksWereCalled() {
+ Call call = new Call(
+ "1", /* callId */
+ mContext,
+ mMockCallsManager,
+ mLock,
+ null /* ConnectionServiceRepository */,
+ mMockPhoneNumberUtilsAdapter,
+ TEST_ADDRESS,
+ null /* GatewayInfo */,
+ null /* connectionManagerPhoneAccountHandle */,
+ SIM_1_HANDLE,
+ Call.CALL_DIRECTION_UNDEFINED,
+ false /* shouldAttachToExistingConnection*/,
+ true /* isConference */,
+ mMockClockProxy,
+ mMockToastProxy);
+
+ // setup
+ call.setIsTransactionalCall(true);
+ assertTrue(call.isTransactionalCall());
+ assertNull(call.getConnectionServiceWrapper());
+ call.setTransactionServiceWrapper(mMockTransactionalService);
+ assertEquals(mMockTransactionalService, call.getTransactionServiceWrapper());
+
+ // assert CallEventCallback#onSetInactive is called
+ call.setState(CallState.ACTIVE, "test");
+ call.hold();
+ verify(mMockTransactionalService, times(1)).onSetInactive(call);
+
+ // assert CallEventCallback#onSetActive is called
+ call.setState(CallState.ON_HOLD, "test");
+ call.unhold();
+ verify(mMockTransactionalService, times(1)).onSetActive(call);
+
+ // assert CallEventCallback#onAnswer is called
+ call.setState(CallState.RINGING, "test");
+ call.answer(0);
+ verify(mMockTransactionalService, times(1)).onAnswer(call, 0);
+
+ // assert CallEventCallback#onReject is called
+ call.setState(CallState.RINGING, "test");
+ call.reject(0);
+ verify(mMockTransactionalService, times(1)).onReject(call, 0);
+
+ // assert CallEventCallback#onDisconnect is called
+ call.setState(CallState.ACTIVE, "test");
+ call.disconnect();
+ verify(mMockTransactionalService, times(1)).onDisconnect(call,
+ call.getDisconnectCause());
+ }
}
diff --git a/tests/src/com/android/server/telecom/tests/CallsManagerTest.java b/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
index 8fe64e2..59efb56 100644
--- a/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallsManagerTest.java
@@ -50,6 +50,7 @@
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
+import android.os.OutcomeReceiver;
import android.os.Process;
import android.os.SystemClock;
import android.os.UserHandle;
@@ -60,6 +61,7 @@
import android.telecom.PhoneAccount;
import android.telecom.PhoneAccountHandle;
import android.telecom.TelecomManager;
+import android.telecom.CallException;
import android.telecom.VideoProfile;
import android.telephony.TelephonyManager;
import android.test.suitebuilder.annotation.MediumTest;
@@ -122,7 +124,6 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
-import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
@@ -1774,6 +1775,88 @@
assertTrue(argumentCaptor.getValue().contains("Unavailable phoneAccountHandle"));
}
+ public class LatchedOutcomeReceiver implements OutcomeReceiver<Boolean,
+ CallException> {
+ CountDownLatch mCountDownLatch;
+ Boolean mIsOnResultExpected;
+
+ public LatchedOutcomeReceiver(CountDownLatch latch, boolean isOnResultExpected){
+ mCountDownLatch = latch;
+ mIsOnResultExpected = isOnResultExpected;
+ }
+
+ @Override
+ public void onResult(Boolean result) {
+ if(mIsOnResultExpected) {
+ mCountDownLatch.countDown();
+ }
+ }
+
+ @Override
+ public void onError(CallException error) {
+ OutcomeReceiver.super.onError(error);
+ if(!mIsOnResultExpected){
+ mCountDownLatch.countDown();
+ }
+ }
+ }
+
+ @SmallTest
+ @Test
+ public void testCanHold() {
+ Call newCall = addSpyCall();
+ when(newCall.isTransactionalCall()).thenReturn(true);
+ when(newCall.can(Connection.CAPABILITY_SUPPORT_HOLD)).thenReturn(false);
+ assertFalse(mCallsManager.canHold(newCall));
+ when(newCall.can(Connection.CAPABILITY_SUPPORT_HOLD)).thenReturn(true);
+ assertTrue(mCallsManager.canHold(newCall));
+ }
+
+ @MediumTest
+ @Test
+ public void testHoldTransactional() throws Exception {
+ CountDownLatch latch = new CountDownLatch(1);
+ Call newCall = addSpyCall();
+
+ // case 1: no active call, no need to put the call on hold
+ when(mConnectionSvrFocusMgr.getCurrentFocusCall()).thenReturn(null);
+ mCallsManager.transactionHoldPotentialActiveCallForNewCall(newCall,
+ new LatchedOutcomeReceiver(latch, true));
+ waitForCountDownLatch(latch);
+
+ // case 2: active call == new call, no need to put the call on hold
+ latch = new CountDownLatch(1);
+ when(mConnectionSvrFocusMgr.getCurrentFocusCall()).thenReturn(newCall);
+ mCallsManager.transactionHoldPotentialActiveCallForNewCall(newCall,
+ new LatchedOutcomeReceiver(latch, true));
+ waitForCountDownLatch(latch);
+
+ // case 3: cannot hold current active call early check
+ Call cannotHoldCall = addSpyCall(SIM_1_HANDLE, null,
+ CallState.ACTIVE, 0, 0);
+ latch = new CountDownLatch(1);
+ when(mConnectionSvrFocusMgr.getCurrentFocusCall()).thenReturn(cannotHoldCall);
+ mCallsManager.transactionHoldPotentialActiveCallForNewCall(newCall,
+ new LatchedOutcomeReceiver(latch, false));
+ waitForCountDownLatch(latch);
+
+ // case 4: activeCall != newCall && canHold(activeCall)
+ Call canHoldCall = addSpyCall(SIM_1_HANDLE, null,
+ CallState.ACTIVE, Connection.CAPABILITY_HOLD, 0);
+ latch = new CountDownLatch(1);
+ when(mConnectionSvrFocusMgr.getCurrentFocusCall()).thenReturn(canHoldCall);
+ mCallsManager.transactionHoldPotentialActiveCallForNewCall(newCall,
+ new LatchedOutcomeReceiver(latch, true));
+ waitForCountDownLatch(latch);
+ }
+
+ public void waitForCountDownLatch(CountDownLatch latch) throws InterruptedException {
+ boolean success = latch.await(5000, TimeUnit.MILLISECONDS);
+ if (!success) {
+ fail("assertOnResultWasReceived success failed");
+ }
+ }
+
private Call addSpyCall() {
return addSpyCall(SIM_2_HANDLE, CallState.ACTIVE);
}
diff --git a/tests/src/com/android/server/telecom/tests/PhoneAccountRegistrarTest.java b/tests/src/com/android/server/telecom/tests/PhoneAccountRegistrarTest.java
index 26f0f6d..efa83ea 100644
--- a/tests/src/com/android/server/telecom/tests/PhoneAccountRegistrarTest.java
+++ b/tests/src/com/android/server/telecom/tests/PhoneAccountRegistrarTest.java
@@ -684,6 +684,75 @@
assertEquals(TEST_LABEL, registeredAccount.getLabel());
}
+ @MediumTest
+ @Test
+ public void testSecurityExceptionIsThrownWhenSelfManagedLacksPermissions() {
+ PhoneAccountHandle handle = makeQuickAccountHandle(
+ new ComponentName("self", "managed"), "selfie1");
+
+ PhoneAccount accountWithoutCapability = new PhoneAccount.Builder(handle, "label")
+ .setCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED)
+ .build();
+
+ assertFalse(mRegistrar.hasTransactionalCallCapabilites(accountWithoutCapability));
+
+ try {
+ mRegistrar.registerPhoneAccount(accountWithoutCapability);
+ fail("should not be able to register account");
+ } catch (SecurityException securityException) {
+ // test pass
+ } finally {
+ mRegistrar.unregisterPhoneAccount(handle);
+ }
+ }
+
+ @MediumTest
+ @Test
+ public void testSelfManagedPhoneAccountWithTransactionalOperations() {
+ PhoneAccountHandle handle = makeQuickAccountHandle(
+ new ComponentName("self", "managed"), "selfie1");
+
+ PhoneAccount accountWithCapability = new PhoneAccount.Builder(handle, "label")
+ .setCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED |
+ PhoneAccount.CAPABILITY_SUPPORTS_TRANSACTIONAL_OPERATIONS)
+ .build();
+
+ assertTrue(mRegistrar.hasTransactionalCallCapabilites(accountWithCapability));
+
+ try {
+ mRegistrar.registerPhoneAccount(accountWithCapability);
+ PhoneAccount registeredAccount = mRegistrar.getPhoneAccountUnchecked(handle);
+ assertEquals(TEST_LABEL, registeredAccount.getLabel().toString());
+ } finally {
+ mRegistrar.unregisterPhoneAccount(handle);
+ }
+ }
+
+ @MediumTest
+ @Test
+ public void testRegisterPhoneAccountAmendsSelfManagedCapabilityInternally() {
+ // GIVEN
+ PhoneAccountHandle handle = makeQuickAccountHandle(
+ new ComponentName("self", "managed"), "selfie1");
+ PhoneAccount accountWithCapability = new PhoneAccount.Builder(handle, "label")
+ .setCapabilities(
+ PhoneAccount.CAPABILITY_SUPPORTS_TRANSACTIONAL_OPERATIONS)
+ .build();
+
+ assertTrue(mRegistrar.hasTransactionalCallCapabilites(accountWithCapability));
+
+ try {
+ // WHEN
+ mRegistrar.registerPhoneAccount(accountWithCapability);
+ PhoneAccount registeredAccount = mRegistrar.getPhoneAccountUnchecked(handle);
+ // THEN
+ assertEquals(PhoneAccount.CAPABILITY_SELF_MANAGED, (registeredAccount.getCapabilities()
+ & PhoneAccount.CAPABILITY_SELF_MANAGED));
+ } finally {
+ mRegistrar.unregisterPhoneAccount(handle);
+ }
+ }
+
/**
* Tests to ensure that when registering a self-managed PhoneAccount, it cannot also be defined
* as a call provider, connection manager, or sim subscription.
diff --git a/tests/src/com/android/server/telecom/tests/TelecomServiceImplTest.java b/tests/src/com/android/server/telecom/tests/TelecomServiceImplTest.java
index a5aa936..e8d4226 100644
--- a/tests/src/com/android/server/telecom/tests/TelecomServiceImplTest.java
+++ b/tests/src/com/android/server/telecom/tests/TelecomServiceImplTest.java
@@ -36,9 +36,11 @@
import android.os.Binder;
import android.os.Build;
import android.os.Bundle;
+import android.os.OutcomeReceiver;
import android.os.RemoteException;
import android.os.UserHandle;
import android.os.UserManager;
+import android.telecom.CallAttributes;
import android.telecom.PhoneAccount;
import android.telecom.PhoneAccountHandle;
import android.telecom.TelecomManager;
@@ -46,6 +48,7 @@
import android.telephony.TelephonyManager;
import android.test.suitebuilder.annotation.SmallTest;
+import com.android.internal.telecom.ICallEventCallback;
import com.android.internal.telecom.ITelecomService;
import com.android.server.telecom.Call;
import com.android.server.telecom.CallIntentProcessor;
@@ -57,6 +60,9 @@
import com.android.server.telecom.TelecomSystem;
import com.android.server.telecom.components.UserCallIntentProcessor;
import com.android.server.telecom.components.UserCallIntentProcessorFactory;
+import com.android.server.telecom.voip.IncomingCallTransaction;
+import com.android.server.telecom.voip.OutgoingCallTransaction;
+import com.android.server.telecom.voip.TransactionManager;
import org.junit.After;
import org.junit.Before;
@@ -67,7 +73,6 @@
import org.mockito.ArgumentMatcher;
import org.mockito.Mock;
-import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.Executor;
@@ -95,14 +100,18 @@
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.isA;
import static org.mockito.Mockito.when;
@RunWith(JUnit4.class)
public class TelecomServiceImplTest extends TelecomTestCase {
private static final String CALLING_PACKAGE = TelecomServiceImplTest.class.getPackageName();
+ private static final String TEST_NAME = "Alan Turing";
+ private static final Uri TEST_URI = Uri.fromParts("tel", "abc", "123");
public static final String TEST_PACKAGE = "com.test";
public static final String PACKAGE_NAME = "test";
@@ -176,6 +185,8 @@
@Mock private UserCallIntentProcessor mUserCallIntentProcessor;
private PackageManager mPackageManager;
@Mock private ApplicationInfo mApplicationInfo;
+ @Mock private ICallEventCallback mICallEventCallback;
+ @Mock private TransactionManager mTransactionManager;
private final TelecomSystem.SyncRoot mLock = new TelecomSystem.SyncRoot() { };
@@ -225,6 +236,7 @@
mSubscriptionManagerAdapter,
mSettingsSecureAdapter,
mLock);
+ telecomServiceImpl.setTransactionManager(mTransactionManager);
mTSIBinder = telecomServiceImpl.getBinder();
mComponentContextFixture.setTelecomManager(mTelecomManager);
when(mTelecomManager.getDefaultDialerPackage()).thenReturn(DEFAULT_DIALER_PACKAGE);
@@ -356,6 +368,82 @@
.setUserSelectedOutgoingPhoneAccount(eq(TEL_PA_HANDLE_16), any(UserHandle.class));
}
+ @Test
+ public void testAddCallWithOutgoingCall() throws RemoteException {
+ // GIVEN
+ CallAttributes mOutgoingCallAttributes = new CallAttributes.Builder(TEL_PA_HANDLE_CURRENT,
+ CallAttributes.DIRECTION_OUTGOING, TEST_NAME, TEST_URI)
+ .setCallType(CallAttributes.AUDIO_CALL)
+ .setCallCapabilities(CallAttributes.SUPPORTS_SET_INACTIVE)
+ .build();
+ PhoneAccount phoneAccount = makeMultiUserPhoneAccount(TEL_PA_HANDLE_CURRENT).build();
+ phoneAccount.setIsEnabled(true);
+
+ // WHEN
+ when(mFakePhoneAccountRegistrar.getPhoneAccountUnchecked(TEL_PA_HANDLE_CURRENT)).thenReturn(
+ phoneAccount);
+
+ doReturn(phoneAccount).when(mFakePhoneAccountRegistrar).getPhoneAccount(
+ eq(TEL_PA_HANDLE_CURRENT), any(UserHandle.class));
+
+ mTSIBinder.addCall(mOutgoingCallAttributes, mICallEventCallback, "1", CALLING_PACKAGE);
+
+ // THEN
+ verify(mTransactionManager, times(1))
+ .addTransaction(isA(OutgoingCallTransaction.class), isA(OutcomeReceiver.class));
+ }
+
+ @Test
+ public void testAddCallWithIncomingCall() throws RemoteException {
+ // GIVEN
+ CallAttributes mIncomingCallAttributes = new CallAttributes.Builder(TEL_PA_HANDLE_CURRENT,
+ CallAttributes.DIRECTION_INCOMING, TEST_NAME, TEST_URI)
+ .setCallType(CallAttributes.AUDIO_CALL)
+ .setCallCapabilities(CallAttributes.SUPPORTS_SET_INACTIVE)
+ .build();
+ PhoneAccount phoneAccount = makeMultiUserPhoneAccount(TEL_PA_HANDLE_CURRENT).build();
+ phoneAccount.setIsEnabled(true);
+
+ // WHEN
+ when(mFakePhoneAccountRegistrar.getPhoneAccountUnchecked(TEL_PA_HANDLE_CURRENT)).thenReturn(
+ phoneAccount);
+
+ doReturn(phoneAccount).when(mFakePhoneAccountRegistrar).getPhoneAccount(
+ eq(TEL_PA_HANDLE_CURRENT), any(UserHandle.class));
+
+ mTSIBinder.addCall(mIncomingCallAttributes, mICallEventCallback, "1", CALLING_PACKAGE);
+
+ // THEN
+ verify(mTransactionManager, times(1))
+ .addTransaction(isA(IncomingCallTransaction.class), isA(OutcomeReceiver.class));
+ }
+
+ @Test
+ public void testAddCallWithManagedPhoneAccount() throws RemoteException {
+ // GIVEN
+ CallAttributes attributes = new CallAttributes.Builder(TEL_PA_HANDLE_CURRENT,
+ CallAttributes.DIRECTION_OUTGOING, TEST_NAME, TEST_URI).build();
+ PhoneAccount phoneAccount = makeMultiUserPhoneAccount(TEL_PA_HANDLE_CURRENT)
+ .setCapabilities(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION)
+ .build();
+ phoneAccount.setIsEnabled(true);
+
+ // WHEN
+ when(mFakePhoneAccountRegistrar.getPhoneAccountUnchecked(TEL_PA_HANDLE_CURRENT)).thenReturn(
+ phoneAccount);
+
+ doReturn(phoneAccount).when(mFakePhoneAccountRegistrar).getPhoneAccount(
+ eq(TEL_PA_HANDLE_CURRENT), any(UserHandle.class));
+
+ // THEN
+ try {
+ mTSIBinder.addCall(attributes, mICallEventCallback, "1", CALLING_PACKAGE);
+ fail("should have thrown IllegalArgumentException");
+ } catch (IllegalArgumentException e) {
+ // pass
+ }
+ }
+
@SmallTest
@Test
public void testSetUserSelectedOutgoingPhoneAccountFailure() throws RemoteException {
diff --git a/tests/src/com/android/server/telecom/tests/TransactionTests.java b/tests/src/com/android/server/telecom/tests/TransactionTests.java
new file mode 100644
index 0000000..9ae05eb
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/TransactionTests.java
@@ -0,0 +1,268 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom.tests;
+
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.isA;
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.nullable;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.when;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.OutcomeReceiver;
+import android.os.UserHandle;
+import android.telecom.CallAttributes;
+import android.telecom.DisconnectCause;
+import android.telecom.PhoneAccountHandle;
+
+import com.android.server.telecom.Call;
+import com.android.server.telecom.CallState;
+import com.android.server.telecom.CallerInfoLookupHelper;
+import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.ClockProxy;
+import com.android.server.telecom.PhoneNumberUtilsAdapter;
+import com.android.server.telecom.TelecomSystem;
+import com.android.server.telecom.ui.ToastFactory;
+import com.android.server.telecom.voip.EndCallTransaction;
+import com.android.server.telecom.voip.HoldCallTransaction;
+import com.android.server.telecom.voip.IncomingCallTransaction;
+import com.android.server.telecom.voip.OutgoingCallTransaction;
+import com.android.server.telecom.voip.HoldActiveCallForNewCallTransaction;
+import com.android.server.telecom.voip.RequestFocusTransaction;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+
+public class TransactionTests extends TelecomTestCase {
+
+ private static final String CALL_ID_1 = "1";
+
+ private static final PhoneAccountHandle mHandle = new PhoneAccountHandle(
+ new ComponentName("foo", "bar"), "1");
+ private static final String TEST_NAME = "Sergey Brin";
+ private static final Uri TEST_URI = Uri.fromParts("tel", "abc", "123");
+
+ @Mock private Call mMockCall1;
+ @Mock private Context mMockContext;
+ @Mock private CallsManager mCallsManager;
+ @Mock private ToastFactory mToastFactory;
+ @Mock private ClockProxy mClockProxy;
+ @Mock private PhoneNumberUtilsAdapter mPhoneNumberUtilsAdapter;
+ @Mock private CallerInfoLookupHelper mCallerInfoLookupHelper;
+
+ private final TelecomSystem.SyncRoot mLock = new TelecomSystem.SyncRoot() {
+ };
+ private static final Uri TEST_ADDRESS = Uri.parse("tel:555-1212");
+
+ @Override
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ MockitoAnnotations.initMocks(this);
+ Mockito.when(mMockCall1.getId()).thenReturn(CALL_ID_1);
+ }
+
+ @Override
+ @After
+ public void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ @Test
+ public void testEndCallTransactionWithDisconnect() throws Exception {
+ // GIVEN
+ EndCallTransaction transaction =
+ new EndCallTransaction(mCallsManager, true, 0, mMockCall1);
+
+ // WHEN
+ transaction.processTransaction(null);
+
+ // THEN
+ verify(mCallsManager, times(1))
+ .markCallAsDisconnected(mMockCall1, new DisconnectCause(0));
+ verify(mCallsManager, never())
+ .rejectCall(mMockCall1, 0);
+ verify(mCallsManager, times(1))
+ .markCallAsRemoved(mMockCall1);
+ }
+
+ @Test
+ public void testEndCallTransactionWithReject() throws Exception {
+ // GIVEN
+ EndCallTransaction transaction =
+ new EndCallTransaction(mCallsManager, false, 0, mMockCall1);
+
+ // WHEN
+ transaction.processTransaction(null);
+
+ // THEN
+ verify(mCallsManager, never())
+ .markCallAsDisconnected(mMockCall1, new DisconnectCause(0));
+ verify(mCallsManager, times(1))
+ .rejectCall(mMockCall1, 0);
+ verify(mCallsManager, times(1))
+ .markCallAsRemoved(mMockCall1);
+ }
+
+ @Test
+ public void testHoldCallTransaction() throws Exception {
+ // GIVEN
+ Call spyCall = createSpyCall(null, CallState.ACTIVE, CALL_ID_1);
+
+ HoldCallTransaction transaction =
+ new HoldCallTransaction(mCallsManager, spyCall);
+
+ // WHEN
+ doAnswer(invocation -> {
+ Call call = invocation.getArgument(0);
+ call.setState(CallState.ON_HOLD, "manual set");
+ return null;
+ }).when(mCallsManager).markCallAsOnHold(spyCall);
+
+ transaction.processTransaction(null);
+
+ // THEN
+ verify(mCallsManager, times(1))
+ .markCallAsOnHold(spyCall);
+
+ assertEquals(CallState.ON_HOLD, spyCall.getState());
+ }
+
+ @Test
+ public void testTransactionalRequestFocus() throws Exception {
+ // GIVEN
+ RequestFocusTransaction transaction =
+ new RequestFocusTransaction(mCallsManager, mMockCall1);
+
+ // WHEN
+ transaction.processTransaction(null);
+
+ // THEN
+ verify(mCallsManager, times(1))
+ .transactionRequestNewFocusCall(eq(mMockCall1), isA(OutcomeReceiver.class));
+ }
+
+ @Test
+ public void testTransactionalHoldActiveCallForNewCall() throws Exception {
+ // GIVEN
+ HoldActiveCallForNewCallTransaction transaction =
+ new HoldActiveCallForNewCallTransaction(mCallsManager, mMockCall1);
+
+ // WHEN
+ transaction.processTransaction(null);
+
+ // THEN
+ verify(mCallsManager, times(1))
+ .transactionHoldPotentialActiveCallForNewCall(eq(mMockCall1),
+ isA(OutcomeReceiver.class));
+ }
+
+ @Test
+ public void testOutgoingCallTransaction() throws Exception {
+ // GIVEN
+ CallAttributes callAttributes = new CallAttributes.Builder(mHandle,
+ CallAttributes.DIRECTION_OUTGOING, TEST_NAME, TEST_URI).build();
+
+ OutgoingCallTransaction transaction =
+ new OutgoingCallTransaction(CALL_ID_1, mMockContext, callAttributes, mCallsManager);
+
+ // WHEN
+ when(mMockContext.getOpPackageName()).thenReturn("testPackage");
+ when(mMockContext.checkCallingPermission(android.Manifest.permission.CALL_PRIVILEGED))
+ .thenReturn(PackageManager.PERMISSION_GRANTED);
+ when(mCallsManager.isOutgoingCallPermitted(callAttributes.getPhoneAccountHandle()))
+ .thenReturn(true);
+ transaction.processTransaction(null);
+
+ // THEN
+ verify(mCallsManager, times(1))
+ .startOutgoingCall(isA(Uri.class),
+ isA(PhoneAccountHandle.class),
+ isA(Bundle.class),
+ isA(UserHandle.class),
+ isA(Intent.class),
+ nullable(String.class));
+ }
+
+ @Test
+ public void testIncomingCallTransaction() throws Exception {
+ // GIVEN
+ CallAttributes callAttributes = new CallAttributes.Builder(mHandle,
+ CallAttributes.DIRECTION_INCOMING, TEST_NAME, TEST_URI).build();
+
+ IncomingCallTransaction transaction =
+ new IncomingCallTransaction(CALL_ID_1, callAttributes, mCallsManager);
+
+ // WHEN
+ when(mCallsManager.isIncomingCallPermitted(callAttributes.getPhoneAccountHandle()))
+ .thenReturn(true);
+ transaction.processTransaction(null);
+
+ // THEN
+ verify(mCallsManager, times(1))
+ .processIncomingCallIntent(isA(PhoneAccountHandle.class),
+ isA(Bundle.class),
+ isA(Boolean.class));
+ }
+
+ private Call createSpyCall(PhoneAccountHandle targetPhoneAccount, int initialState, String id) {
+ when(mCallsManager.getCallerInfoLookupHelper()).thenReturn(mCallerInfoLookupHelper);
+
+ Call call = new Call(id,
+ mMockContext,
+ mCallsManager,
+ mLock, /* ConnectionServiceRepository */
+ null,
+ mPhoneNumberUtilsAdapter,
+ TEST_ADDRESS,
+ null /* GatewayInfo */,
+ null /* ConnectionManagerAccount */,
+ targetPhoneAccount,
+ Call.CALL_DIRECTION_INCOMING,
+ false /* shouldAttachToExistingConnection*/,
+ false /* isConference */,
+ mClockProxy,
+ mToastFactory);
+
+ Call callSpy = Mockito.spy(call);
+
+ callSpy.setState(initialState, "manual set in test");
+
+ // Mocks some methods to not call the real method.
+ doNothing().when(callSpy).unhold();
+ doNothing().when(callSpy).hold();
+ doNothing().when(callSpy).disconnect();
+
+ return callSpy;
+ }
+}
\ No newline at end of file
diff --git a/tests/src/com/android/server/telecom/tests/TransactionalServiceWrapperTest.java b/tests/src/com/android/server/telecom/tests/TransactionalServiceWrapperTest.java
new file mode 100644
index 0000000..1ea81e7
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/TransactionalServiceWrapperTest.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom.tests;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.isA;
+
+
+import android.content.ComponentName;
+import android.os.OutcomeReceiver;
+import android.os.RemoteException;
+import android.os.ResultReceiver;
+import android.telecom.DisconnectCause;
+import android.telecom.PhoneAccountHandle;
+
+import com.android.internal.telecom.ICallControl;
+import com.android.internal.telecom.ICallEventCallback;
+import com.android.server.telecom.Call;
+import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.TransactionalServiceRepository;
+import com.android.server.telecom.TransactionalServiceWrapper;
+import com.android.server.telecom.voip.EndCallTransaction;
+import com.android.server.telecom.voip.HoldCallTransaction;
+import com.android.server.telecom.voip.SerialTransaction;
+import com.android.server.telecom.voip.TransactionManager;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(JUnit4.class)
+public class TransactionalServiceWrapperTest extends TelecomTestCase {
+
+ private static final PhoneAccountHandle SERVICE_HANDLE = new PhoneAccountHandle(
+ ComponentName.unflattenFromString("com.foo/.Blah"), "Service1");
+
+ private static final String CALL_ID_1 = "1";
+ private static final String CALL_ID_2 = "2";
+
+ TransactionalServiceWrapper mTransactionalServiceWrapper;
+
+ @Mock private Call mMockCall1;
+ @Mock private Call mMockCall2;
+ @Mock private CallsManager mCallsManager;
+ @Mock private TransactionManager mTransactionManager;
+ @Mock private ICallEventCallback mCallEventCallback;
+ @Mock private TransactionalServiceRepository mRepository;
+
+ @Override
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ MockitoAnnotations.initMocks(this);
+ Mockito.when(mMockCall1.getId()).thenReturn(CALL_ID_1);
+ Mockito.when(mMockCall2.getId()).thenReturn(CALL_ID_2);
+
+ mTransactionalServiceWrapper = new TransactionalServiceWrapper(mCallEventCallback,
+ mCallsManager, SERVICE_HANDLE, mMockCall1, mRepository);
+
+ mTransactionalServiceWrapper.setTransactionManager(mTransactionManager);
+ }
+
+ @Override
+ @After
+ public void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ @Test
+ public void testTransactionalServiceWrapperStartState() throws Exception {
+ TransactionalServiceWrapper service =
+ new TransactionalServiceWrapper(mCallEventCallback,
+ mCallsManager, SERVICE_HANDLE, mMockCall1, mRepository);
+
+ assertEquals(SERVICE_HANDLE, service.getPhoneAccountHandle());
+ assertEquals(1, service.getNumberOfTrackedCalls());
+ }
+
+ @Test
+ public void testTransactionalServiceWrapperCallCount() throws Exception {
+ TransactionalServiceWrapper service =
+ new TransactionalServiceWrapper(mCallEventCallback,
+ mCallsManager, SERVICE_HANDLE, mMockCall1, mRepository);
+
+ assertEquals(1, service.getNumberOfTrackedCalls());
+ service.trackCall(mMockCall2);
+ assertEquals(2, service.getNumberOfTrackedCalls());
+
+ assertTrue(service.untrackCall(mMockCall2));
+ assertEquals(1, service.getNumberOfTrackedCalls());
+
+ assertTrue(service.untrackCall(mMockCall1));
+ assertFalse(service.untrackCall(mMockCall1));
+ assertEquals(0, service.getNumberOfTrackedCalls());
+ }
+
+ @Test
+ public void testCallControlSetActive() throws RemoteException {
+ // GIVEN
+ mTransactionalServiceWrapper.trackCall(mMockCall1);
+ //when(mCallsManager.getCallObjectFromCallId(CALL_ID_1)).thenReturn(mMockCall1);
+
+ // WHEN
+ ICallControl callControl = mTransactionalServiceWrapper.getICallControl();
+ callControl.setActive(CALL_ID_1, new ResultReceiver(null));
+
+ //THEN
+ verify(mTransactionManager, times(1))
+ .addTransaction(isA(SerialTransaction.class), isA(OutcomeReceiver.class));
+ }
+
+ @Test
+ public void testCallControlRejectCall() throws RemoteException {
+ // GIVEN
+ mTransactionalServiceWrapper.trackCall(mMockCall1);
+ //when(mCallsManager.getCallObjectFromCallId(CALL_ID_1)).thenReturn(mMockCall1);
+
+ // WHEN
+ ICallControl callControl = mTransactionalServiceWrapper.getICallControl();
+ callControl.rejectCall(CALL_ID_1, new ResultReceiver(null));
+
+ //THEN
+ verify(mTransactionManager, times(1))
+ .addTransaction(isA(EndCallTransaction.class), isA(OutcomeReceiver.class));
+ }
+
+ @Test
+ public void testCallControlDisconnectCall() throws RemoteException {
+ // GIVEN
+ mTransactionalServiceWrapper.trackCall(mMockCall1);
+ //when(mCallsManager.getCallObjectFromCallId(CALL_ID_1)).thenReturn(mMockCall1);
+
+ // WHEN
+ ICallControl callControl = mTransactionalServiceWrapper.getICallControl();
+ callControl.disconnect(CALL_ID_1, new DisconnectCause(DisconnectCause.LOCAL),
+ new ResultReceiver(null));
+
+ //THEN
+ verify(mTransactionManager, times(1))
+ .addTransaction(isA(EndCallTransaction.class), isA(OutcomeReceiver.class));
+ }
+
+ @Test
+ public void testCallControlSetInactive() throws RemoteException {
+ // GIVEN
+ mTransactionalServiceWrapper.trackCall(mMockCall1);
+ //when(mCallsManager.getCallObjectFromCallId(CALL_ID_1)).thenReturn(mMockCall1);
+
+ // WHEN
+ ICallControl callControl = mTransactionalServiceWrapper.getICallControl();
+ callControl.setInactive(CALL_ID_1, new ResultReceiver(null));
+
+ //THEN
+ verify(mTransactionManager, times(1))
+ .addTransaction(isA(HoldCallTransaction.class), isA(OutcomeReceiver.class));
+ }
+}
diff --git a/tests/src/com/android/server/telecom/tests/VoipCallTransactionTest.java b/tests/src/com/android/server/telecom/tests/VoipCallTransactionTest.java
index d57a163..58e4a77 100644
--- a/tests/src/com/android/server/telecom/tests/VoipCallTransactionTest.java
+++ b/tests/src/com/android/server/telecom/tests/VoipCallTransactionTest.java
@@ -20,6 +20,7 @@
import static org.junit.Assert.assertTrue;
import android.os.OutcomeReceiver;
+import android.telecom.CallException;
import android.util.Log;
import androidx.test.filters.SmallTest;
@@ -121,7 +122,7 @@
subTransactions.add(t2);
subTransactions.add(t3);
CompletableFuture<VoipCallTransactionResult> resultFuture = new CompletableFuture<>();
- OutcomeReceiver<VoipCallTransactionResult, Exception> outcomeReceiver =
+ OutcomeReceiver<VoipCallTransactionResult, CallException> outcomeReceiver =
resultFuture::complete;
String expectedLog = "t1 success;\nt2 success;\nt3 success;\n";
mTransactionManager.addTransaction(new SerialTransaction(subTransactions), outcomeReceiver);
@@ -145,15 +146,15 @@
subTransactions.add(t2);
subTransactions.add(t3);
CompletableFuture<String> exceptionFuture = new CompletableFuture<>();
- OutcomeReceiver<VoipCallTransactionResult, Exception> outcomeReceiver =
- new OutcomeReceiver<VoipCallTransactionResult, Exception>() {
+ OutcomeReceiver<VoipCallTransactionResult, CallException> outcomeReceiver =
+ new OutcomeReceiver<VoipCallTransactionResult, CallException>() {
@Override
public void onResult(VoipCallTransactionResult result) {
}
@Override
- public void onError(Exception e) {
+ public void onError(CallException e) {
exceptionFuture.complete(e.getMessage());
}
};
@@ -179,7 +180,7 @@
subTransactions.add(t2);
subTransactions.add(t3);
CompletableFuture<VoipCallTransactionResult> resultFuture = new CompletableFuture<>();
- OutcomeReceiver<VoipCallTransactionResult, Exception> outcomeReceiver =
+ OutcomeReceiver<VoipCallTransactionResult, CallException> outcomeReceiver =
resultFuture::complete;
mTransactionManager.addTransaction(new ParallelTransaction(subTransactions),
outcomeReceiver);
@@ -206,15 +207,15 @@
subTransactions.add(t2);
subTransactions.add(t3);
CompletableFuture<String> exceptionFuture = new CompletableFuture<>();
- OutcomeReceiver<VoipCallTransactionResult, Exception> outcomeReceiver =
- new OutcomeReceiver<VoipCallTransactionResult, Exception>() {
+ OutcomeReceiver<VoipCallTransactionResult, CallException> outcomeReceiver =
+ new OutcomeReceiver<VoipCallTransactionResult, CallException>() {
@Override
public void onResult(VoipCallTransactionResult result) {
}
@Override
- public void onError(Exception e) {
+ public void onError(CallException e) {
exceptionFuture.complete(e.getMessage());
}
};
@@ -231,7 +232,7 @@
VoipCallTransaction t = new TestVoipCallTransaction("t", 10000L,
TestVoipCallTransaction.SUCCESS);
CompletableFuture<VoipCallTransactionResult> resultFuture = new CompletableFuture<>();
- OutcomeReceiver<VoipCallTransactionResult, Exception> outcomeReceiver =
+ OutcomeReceiver<VoipCallTransactionResult, CallException> outcomeReceiver =
resultFuture::complete;
mTransactionManager.addTransaction(t, outcomeReceiver);
VoipCallTransactionResult result = resultFuture.get(7000L, TimeUnit.MILLISECONDS);