Merge "Base Implementation of SipTransport" am: 29df53bc97
Original change: https://android-review.googlesource.com/c/platform/packages/services/Telephony/+/1499038
Change-Id: Ice15d191c46b76f12e7e344e9e2e3324011af0fc
diff --git a/src/com/android/phone/ImsRcsController.java b/src/com/android/phone/ImsRcsController.java
index 58f3646..52069b8 100644
--- a/src/com/android/phone/ImsRcsController.java
+++ b/src/com/android/phone/ImsRcsController.java
@@ -16,10 +16,12 @@
package com.android.phone;
+import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Binder;
import android.os.RemoteException;
import android.os.ServiceSpecificException;
+import android.os.UserHandle;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyFrameworkInitializer;
import android.telephony.ims.DelegateRequest;
@@ -413,10 +415,22 @@
}
@Override
- public void createSipDelegate(int subId, DelegateRequest request,
+ public void createSipDelegate(int subId, DelegateRequest request, String packageName,
ISipDelegateConnectionStateCallback delegateState,
ISipDelegateMessageCallback delegateMessage) {
enforceModifyPermission();
+ if (!UserHandle.getUserHandleForUid(Binder.getCallingUid()).isSystem()) {
+ throw new ServiceSpecificException(ImsException.CODE_ERROR_UNSUPPORTED_OPERATION,
+ "SipDelegate creation is only available to primary user.");
+ }
+ try {
+ int remoteUid = mApp.getPackageManager().getPackageUid(packageName, 0 /*flags*/);
+ if (Binder.getCallingUid() != remoteUid) {
+ throw new SecurityException("passed in packageName does not match the caller");
+ }
+ } catch (PackageManager.NameNotFoundException e) {
+ throw new SecurityException("Passed in PackageName can not be found on device");
+ }
final long identity = Binder.clearCallingIdentity();
SipTransportController transport = getRcsFeatureController(subId).getFeature(
@@ -426,7 +440,8 @@
"This subscription does not support the creation of SIP delegates");
}
try {
- transport.createSipDelegate(subId, request, delegateState, delegateMessage);
+ transport.createSipDelegate(subId, request, packageName, delegateState,
+ delegateMessage);
} catch (ImsException e) {
throw new ServiceSpecificException(e.getCode(), e.getMessage());
} finally {
@@ -440,7 +455,12 @@
final long identity = Binder.clearCallingIdentity();
try {
- // Do nothing yet, we do not support this API yet.
+ SipTransportController transport = getRcsFeatureController(subId).getFeature(
+ SipTransportController.class);
+ if (transport == null) {
+ return;
+ }
+ transport.destroySipDelegate(subId, connection, reason);
} finally {
Binder.restoreCallingIdentity(identity);
}
diff --git a/src/com/android/services/telephony/rcs/DelegateBinderStateManager.java b/src/com/android/services/telephony/rcs/DelegateBinderStateManager.java
new file mode 100644
index 0000000..39e9965
--- /dev/null
+++ b/src/com/android/services/telephony/rcs/DelegateBinderStateManager.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2020 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.services.telephony.rcs;
+
+import android.telephony.ims.DelegateRegistrationState;
+import android.telephony.ims.DelegateRequest;
+import android.telephony.ims.FeatureTagState;
+import android.telephony.ims.SipDelegateImsConfiguration;
+import android.telephony.ims.aidl.ISipDelegate;
+import android.telephony.ims.aidl.ISipDelegateMessageCallback;
+import android.telephony.ims.aidl.ISipTransport;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.Executor;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+
+/**
+ * Defines the interface to be used to manage the state of a SipDelegate on the ImsService side.
+ */
+public interface DelegateBinderStateManager {
+
+ /**
+ * Callback interface that allows listeners to listen to changes in registration or
+ * configuration state.
+ */
+ interface StateCallback {
+ /**
+ * The SipDelegate has notified telephony that the registration state has changed.
+ */
+ void onRegistrationStateChanged(DelegateRegistrationState registrationState);
+
+ /**
+ * The SipDelegate has notified telephony that the IMS configuration has changed.
+ */
+ void onImsConfigurationChanged(SipDelegateImsConfiguration config);
+ }
+
+ /** Allow for mocks to be created for testing. */
+ @VisibleForTesting
+ interface Factory {
+ /**
+ * Create a new instance of this interface, which may change depending on the tags being
+ * denied. See {@link SipDelegateBinderConnectionStub} and
+ * {@link SipDelegateBinderConnection}
+ */
+ DelegateBinderStateManager create(int subId, ISipTransport sipTransport,
+ DelegateRequest requestedConfig, Set<FeatureTagState> transportDeniedTags,
+ Executor executor, List<StateCallback> stateCallbacks);
+ }
+
+ /**
+ * Start the process to create a SipDelegate on the ImsService.
+ * @param cb The Binder interface that the SipDelegate should use to notify new incoming SIP
+ * messages as well as acknowledge whether or not an outgoing SIP message was
+ * successfully sent.
+ * @param createdConsumer The consumer that will be notified when the creation process has
+ * completed. Contains the ISipDelegate interface to communicate with the SipDelegate
+ * and the feature tags the SipDelegate itself denied.
+ * @return true if the creation process started, false if the remote process died. If false, the
+ * consumers will not be notified.
+ */
+ boolean create(ISipDelegateMessageCallback cb,
+ BiConsumer<ISipDelegate, Set<FeatureTagState>> createdConsumer);
+
+ /**
+ * Destroy the existing SipDelegate managed by this object.
+ * <p>
+ * This instance should be cleaned up after this call.
+ * @param reason The reason for why this delegate is being destroyed.
+ * @param destroyedConsumer The consumer that will be notified when this operation completes.
+ * Contains the reason the SipDelegate reported it was destroyed.
+ */
+ void destroy(int reason, Consumer<Integer> destroyedConsumer);
+}
diff --git a/src/com/android/services/telephony/rcs/DelegateStateTracker.java b/src/com/android/services/telephony/rcs/DelegateStateTracker.java
new file mode 100644
index 0000000..e287813
--- /dev/null
+++ b/src/com/android/services/telephony/rcs/DelegateStateTracker.java
@@ -0,0 +1,203 @@
+/*
+ * Copyright (C) 2020 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.services.telephony.rcs;
+
+import android.os.RemoteException;
+import android.telephony.ims.DelegateRegistrationState;
+import android.telephony.ims.FeatureTagState;
+import android.telephony.ims.SipDelegateImsConfiguration;
+import android.telephony.ims.aidl.ISipDelegate;
+import android.telephony.ims.aidl.ISipDelegateConnectionStateCallback;
+import android.telephony.ims.stub.DelegateConnectionStateCallback;
+import android.util.LocalLog;
+import android.util.Log;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Manages the events sent back to the remote IMS application using the AIDL backing for the
+ * {@link DelegateConnectionStateCallback} interface.
+ */
+public class DelegateStateTracker implements DelegateBinderStateManager.StateCallback {
+ private static final String LOG_TAG = "DelegateST";
+
+ private final int mSubId;
+ private final ISipDelegateConnectionStateCallback mAppStateCallback;
+ private final ISipDelegate mLocalDelegateImpl;
+
+ private final LocalLog mLocalLog = new LocalLog(SipTransportController.LOG_SIZE);
+
+ private List<FeatureTagState> mDelegateDeniedTags;
+ private DelegateRegistrationState mLastRegState;
+ private boolean mCreatedCalled = false;
+ private int mRegistrationStateOverride = -1;
+
+ public DelegateStateTracker(int subId, ISipDelegateConnectionStateCallback appStateCallback,
+ ISipDelegate localDelegateImpl) {
+ mSubId = subId;
+ mAppStateCallback = appStateCallback;
+ mLocalDelegateImpl = localDelegateImpl;
+ }
+
+ /**
+ * Notify this state tracker that a new internal SipDelegate has been connected.
+ *
+ * Registration and state updates will be send via the
+ * {@link SipDelegateBinderConnection.StateCallback} callback implemented by this class as they
+ * arrive.
+ * @param deniedTags The tags denied by the SipTransportController and ImsService creating the
+ * SipDelegate. These tags will need to be notified back to the IMS application.
+ */
+ public void sipDelegateConnected(Set<FeatureTagState> deniedTags) {
+ logi("SipDelegate connected with denied tags:" + deniedTags);
+ // From the IMS application perspective, we only call onCreated/onDestroyed once and
+ // provide the local implementation of ISipDelegate, which doesn't change, even though
+ // SipDelegates may be changing underneath.
+ if (!mCreatedCalled) {
+ mCreatedCalled = true;
+ notifySipDelegateCreated();
+ }
+ mRegistrationStateOverride = -1;
+ mDelegateDeniedTags = new ArrayList<>(deniedTags);
+ }
+
+ /**
+ * The underlying SipDelegate is changing due to a state change in the SipDelegateController.
+ *
+ * This will trigger an override of the IMS application's registration state. All feature tags
+ * in the REGISTERED state will be overridden to move to the deregistering state specified until
+ * a new SipDelegate was successfully created and {@link #sipDelegateConnected(Set)} was called
+ * or it was destroyed and {@link #sipDelegateDestroyed(int)} was called.
+ * @param deregisteringReason The new deregistering reason that all feature tags in the
+ * registered state should now report.
+ */
+ public void sipDelegateChanging(int deregisteringReason) {
+ logi("SipDelegate Changing");
+ mRegistrationStateOverride = deregisteringReason;
+ if (mLastRegState == null) {
+ logw("sipDelegateChanging: invalid state, onRegistrationStateChanged never called.");
+ mLastRegState = new DelegateRegistrationState.Builder().build();
+ }
+ onRegistrationStateChanged(mLastRegState);
+ }
+
+ /**
+ * The underlying SipDelegate has been destroyed.
+ *
+ * This should only be called when the entire {@link SipDelegateController} is going down
+ * because the application has requested that the SipDelegate be destroyed.
+ *
+ * This can also be called in error conditions where the IMS application or ImsService has
+ * crashed.
+ * @param reason The reason that will be sent to the IMS application for why the SipDelegate
+ * is being destroyed.
+ */
+ public void sipDelegateDestroyed(int reason) {
+ logi("SipDelegate destroyed:" + reason);
+ mRegistrationStateOverride = -1;
+ try {
+ mAppStateCallback.onDestroyed(reason);
+ } catch (RemoteException e) {
+ logw("sipDelegateDestroyed: IMS application is dead: " + e);
+ }
+ }
+
+ /**
+ * The underlying SipDelegate has reported that its registration state has changed.
+ * @param registrationState The RegistrationState reported by the SipDelegate to be sent to the
+ * IMS application.
+ */
+ @Override
+ public void onRegistrationStateChanged(DelegateRegistrationState registrationState) {
+ mLastRegState = registrationState;
+ if (mRegistrationStateOverride > DelegateRegistrationState.DEREGISTERED_REASON_UNKNOWN) {
+ logi("onRegistrationStateChanged: overriding registered state to "
+ + mRegistrationStateOverride);
+ registrationState = overrideRegistrationForDelegateChange(mRegistrationStateOverride,
+ registrationState);
+ }
+ logi("onRegistrationStateChanged: sending reg state" + registrationState);
+ try {
+ mAppStateCallback.onFeatureTagStatusChanged(registrationState, mDelegateDeniedTags);
+ } catch (RemoteException e) {
+ logw("onRegistrationStateChanged: IMS application is dead: " + e);
+ }
+ }
+
+ /**
+ * THe underlying SipDelegate has reported that the IMS configuration has changed.
+ * @param config The config to be sent to the IMS application.
+ */
+ @Override
+ public void onImsConfigurationChanged(SipDelegateImsConfiguration config) {
+ logi("onImsConfigurationChanged: Sending new IMS configuration.");
+ try {
+ mAppStateCallback.onImsConfigurationChanged(config);
+ } catch (RemoteException e) {
+ logw("onImsConfigurationChanged: IMS application is dead: " + e);
+ }
+ }
+
+ /** Write state about this tracker into the PrintWriter to be included in the dumpsys */
+ public void dump(PrintWriter printWriter) {
+ printWriter.println("Last reg state: " + mLastRegState);
+ printWriter.println("Denied tags: " + mDelegateDeniedTags);
+ printWriter.println("Most recent logs: ");
+ printWriter.println();
+ mLocalLog.dump(printWriter);
+ }
+
+ private DelegateRegistrationState overrideRegistrationForDelegateChange(
+ int registerOverrideReason, DelegateRegistrationState state) {
+ Set<String> registeredFeatures = state.getRegisteredFeatureTags();
+ DelegateRegistrationState.Builder overriddenState = new DelegateRegistrationState.Builder();
+ // Override REGISTERED only
+ for (String ft : registeredFeatures) {
+ overriddenState.addDeregisteringFeatureTag(ft, registerOverrideReason);
+ }
+ // keep other deregistering/deregistered tags the same.
+ for (FeatureTagState dereging : state.getDeregisteringFeatureTags()) {
+ overriddenState.addDeregisteringFeatureTag(dereging.getFeatureTag(),
+ dereging.getState());
+ }
+ for (FeatureTagState dereged : state.getDeregisteredFeatureTags()) {
+ overriddenState.addDeregisteredFeatureTag(dereged.getFeatureTag(),
+ dereged.getState());
+ }
+ return overriddenState.build();
+ }
+
+ private void notifySipDelegateCreated() {
+ try {
+ mAppStateCallback.onCreated(mLocalDelegateImpl);
+ } catch (RemoteException e) {
+ logw("notifySipDelegateCreated: IMS application is dead: " + e);
+ }
+ }
+
+ private void logi(String log) {
+ Log.i(SipTransportController.LOG_TAG, LOG_TAG + "[" + mSubId + "] " + log);
+ mLocalLog.log("[I] " + log);
+ }
+ private void logw(String log) {
+ Log.w(SipTransportController.LOG_TAG, LOG_TAG + "[" + mSubId + "] " + log);
+ mLocalLog.log("[W] " + log);
+ }
+}
diff --git a/src/com/android/services/telephony/rcs/MessageTransportStateTracker.java b/src/com/android/services/telephony/rcs/MessageTransportStateTracker.java
new file mode 100644
index 0000000..531ed7d
--- /dev/null
+++ b/src/com/android/services/telephony/rcs/MessageTransportStateTracker.java
@@ -0,0 +1,478 @@
+/*
+ * Copyright (C) 2020 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.services.telephony.rcs;
+
+import android.os.Binder;
+import android.os.RemoteException;
+import android.telephony.ims.DelegateMessageCallback;
+import android.telephony.ims.DelegateRegistrationState;
+import android.telephony.ims.FeatureTagState;
+import android.telephony.ims.SipDelegateImsConfiguration;
+import android.telephony.ims.SipDelegateManager;
+import android.telephony.ims.SipMessage;
+import android.telephony.ims.aidl.ISipDelegate;
+import android.telephony.ims.aidl.ISipDelegateMessageCallback;
+import android.telephony.ims.stub.SipDelegate;
+import android.util.LocalLog;
+import android.util.Log;
+
+import java.io.PrintWriter;
+import java.util.Set;
+import java.util.concurrent.Executor;
+import java.util.function.Consumer;
+
+/**
+ * Tracks the SIP message path both from the IMS application to the SipDelegate and from the
+ * SipDelegate back to the IMS Application.
+ * <p>
+ * Responsibilities include:
+ * 1) Queue incoming and outgoing SIP messages and deliver to IMS application and SipDelegate in
+ * order. If there is an error delivering the message, notify the caller.
+ * 2) TODO Perform basic validation of outgoing messages.
+ * 3) TODO Record the status of ongoing SIP Dialogs and trigger the completion of pending
+ * consumers when they are finished or call closeDialog to clean up the SIP
+ * dialogs that did not complete within the allotted timeout time.
+ * <p>
+ * Note: This handles incoming binder calls, so all calls from other processes should be handled on
+ * the provided Executor.
+ */
+public class MessageTransportStateTracker implements DelegateBinderStateManager.StateCallback {
+ private static final String TAG = "MessageST";
+
+ /**
+ * Communicates the result of verifying whether a SIP message should be sent based on the
+ * contents of the SIP message as well as if the transport is in an available state for the
+ * intended recipient of the message.
+ */
+ private static class VerificationResult {
+ public static final VerificationResult SUCCESS = new VerificationResult();
+
+ /**
+ * If {@code true}, the requested SIP message has been verified to be sent to the remote. If
+ * {@code false}, the SIP message has failed verification and should not be sent to the
+ * result. The {@link #restrictedReason} field will contain the reason for the verification
+ * failure.
+ */
+ public final boolean isVerified;
+
+ /**
+ * The reason associated with why the SIP message was not verified and generated a
+ * {@code false} result for {@link #isVerified}.
+ */
+ public final int restrictedReason;
+
+ /**
+ * Communicates a verified result of success. Use {@link #SUCCESS} instead.
+ */
+ private VerificationResult() {
+ isVerified = true;
+ restrictedReason = SipDelegateManager.MESSAGE_FAILURE_REASON_UNKNOWN;
+ }
+
+ /**
+ * The result of verifying that the SIP Message should be sent.
+ * @param reason The reason associated with why the SIP message was not verified and
+ * generated a {@code false} result for {@link #isVerified}.
+ */
+ VerificationResult(@SipDelegateManager.MessageFailureReason int reason) {
+ isVerified = false;
+ restrictedReason = reason;
+ }
+ }
+
+ // SipDelegateConnection(IMS Application) -> SipDelegate(ImsService)
+ private final ISipDelegate.Stub mSipDelegateConnection = new ISipDelegate.Stub() {
+ /**
+ * The IMS application is acknowledging that it has successfully received and processed an
+ * incoming SIP message sent by the SipDelegate in
+ * {@link ISipDelegateMessageCallback#onMessageReceived(SipMessage)}.
+ */
+ @Override
+ public void notifyMessageReceived(String viaTransactionId) {
+ long token = Binder.clearCallingIdentity();
+ try {
+ mExecutor.execute(() -> {
+ if (mSipDelegate == null) {
+ logw("notifyMessageReceived called when SipDelegate is not associated for "
+ + "transaction id: " + viaTransactionId);
+ return;
+ }
+ try {
+ // TODO track the SIP Dialogs created/destroyed on the associated
+ // SipDelegate.
+ mSipDelegate.notifyMessageReceived(viaTransactionId);
+ } catch (RemoteException e) {
+ logw("SipDelegate not available when notifyMessageReceived was called "
+ + "for transaction id: " + viaTransactionId);
+ }
+ });
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ /**
+ * The IMS application is acknowledging that it received an incoming SIP message sent by the
+ * SipDelegate in {@link ISipDelegateMessageCallback#onMessageReceived(SipMessage)} but it
+ * was unable to process it.
+ */
+ @Override
+ public void notifyMessageReceiveError(String viaTransactionId, int reason) {
+ long token = Binder.clearCallingIdentity();
+ try {
+ mExecutor.execute(() -> {
+ if (mSipDelegate == null) {
+ logw("notifyMessageReceiveError called when SipDelegate is not associated "
+ + "for transaction id: " + viaTransactionId);
+ return;
+ }
+ try {
+ // TODO track the SIP Dialogs created/destroyed on the associated
+ // SipDelegate.
+ mSipDelegate.notifyMessageReceiveError(viaTransactionId, reason);
+ } catch (RemoteException e) {
+ logw("SipDelegate not available when notifyMessageReceiveError was called "
+ + "for transaction id: " + viaTransactionId);
+ }
+ });
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ /**
+ * The IMS application is sending an outgoing SIP message to the SipDelegate to be processed
+ * and sent over the network.
+ */
+ @Override
+ public void sendMessage(SipMessage sipMessage, int configVersion) {
+ long token = Binder.clearCallingIdentity();
+ try {
+ mExecutor.execute(() -> {
+ VerificationResult result = verifyOutgoingMessage(sipMessage);
+ if (!result.isVerified) {
+ notifyDelegateSendError("Outgoing messages restricted", sipMessage,
+ result.restrictedReason);
+ return;
+ }
+ try {
+ // TODO track the SIP Dialogs created/destroyed on the associated
+ // SipDelegate.
+ mSipDelegate.sendMessage(sipMessage, configVersion);
+ logi("sendMessage: message sent - " + sipMessage + ", configVersion: "
+ + configVersion);
+ } catch (RemoteException e) {
+ notifyDelegateSendError("RemoteException: " + e, sipMessage,
+ SipDelegateManager.MESSAGE_FAILURE_REASON_DELEGATE_DEAD);
+ }
+ });
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ /**
+ * The SipDelegateConnection is requesting that the resources associated with an ongoing SIP
+ * dialog be released as the SIP dialog is now closed.
+ */
+ @Override
+ public void closeDialog(String callId) {
+ long token = Binder.clearCallingIdentity();
+ try {
+ mExecutor.execute(() -> {
+ if (mSipDelegate == null) {
+ logw("closeDialog called when SipDelegate is not associated, callId: "
+ + callId);
+ return;
+ }
+ try {
+ // TODO track the SIP Dialogs created/destroyed on the associated
+ // SipDelegate.
+ mSipDelegate.closeDialog(callId);
+ } catch (RemoteException e) {
+ logw("SipDelegate not available when closeDialog was called "
+ + "for call id: " + callId);
+ }
+ });
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+ };
+
+ // SipDelegate(ImsService) -> SipDelegateConnection(IMS Application)
+ private final ISipDelegateMessageCallback.Stub mDelegateConnectionMessageCallback =
+ new ISipDelegateMessageCallback.Stub() {
+ /**
+ * An Incoming SIP Message has been received by the SipDelegate and is being routed
+ * to the IMS application for processing.
+ * <p>
+ * IMS application will call {@link ISipDelegate#notifyMessageReceived(String)} to
+ * acknowledge receipt of this incoming message.
+ */
+ @Override
+ public void onMessageReceived(SipMessage message) {
+ long token = Binder.clearCallingIdentity();
+ try {
+ mExecutor.execute(() -> {
+ VerificationResult result = verifyIncomingMessage(message);
+ if (!result.isVerified) {
+ notifyAppReceiveError("Incoming messages restricted", message,
+ result.restrictedReason);
+ return;
+ }
+ try {
+ // TODO track the SIP Dialogs created/destroyed on the associated
+ // SipDelegate.
+ mAppCallback.onMessageReceived(message);
+ logi("onMessageReceived: received " + message);
+ } catch (RemoteException e) {
+ notifyAppReceiveError("RemoteException: " + e, message,
+ SipDelegateManager.MESSAGE_FAILURE_REASON_DELEGATE_DEAD);
+ }
+ });
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ /**
+ * An outgoing SIP message sent previously by the SipDelegateConnection to the SipDelegate
+ * using {@link ISipDelegate#sendMessage(SipMessage, int)} as been successfully sent.
+ */
+ @Override
+ public void onMessageSent(String viaTransactionId) {
+ long token = Binder.clearCallingIdentity();
+ try {
+ mExecutor.execute(() -> {
+ if (mSipDelegate == null) {
+ logw("Unexpected state, onMessageSent called when SipDelegate is not "
+ + "associated");
+ }
+ try {
+ mAppCallback.onMessageSent(viaTransactionId);
+ } catch (RemoteException e) {
+ logw("Error sending onMessageSent to SipDelegateConnection, remote not"
+ + "available for transaction ID: " + viaTransactionId);
+ }
+ });
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ /**
+ * An outgoing SIP message sent previously by the SipDelegateConnection to the SipDelegate
+ * using {@link ISipDelegate#sendMessage(SipMessage, int)} failed to be sent.
+ */
+ @Override
+ public void onMessageSendFailure(String viaTransactionId, int reason) {
+ long token = Binder.clearCallingIdentity();
+ try {
+ mExecutor.execute(() -> {
+ if (mSipDelegate == null) {
+ logw("Unexpected state, onMessageSendFailure called when SipDelegate is not"
+ + "associated");
+ }
+ try {
+ mAppCallback.onMessageSendFailure(viaTransactionId, reason);
+ } catch (RemoteException e) {
+ logw("Error sending onMessageSendFailure to SipDelegateConnection, remote"
+ + " not available for transaction ID: " + viaTransactionId);
+ }
+ });
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+ };
+
+ private final ISipDelegateMessageCallback mAppCallback;
+ private final Executor mExecutor;
+ private final int mSubId;
+ private final LocalLog mLocalLog = new LocalLog(SipTransportController.LOG_SIZE);
+
+ private ISipDelegate mSipDelegate;
+ private Consumer<Boolean> mPendingClosedConsumer;
+ private int mDelegateClosingReason = -1;
+ private int mDelegateClosedReason = -1;
+
+ public MessageTransportStateTracker(int subId, Executor executor,
+ ISipDelegateMessageCallback appMessageCallback) {
+ mSubId = subId;
+ mAppCallback = appMessageCallback;
+ mExecutor = executor;
+ }
+
+ @Override
+ public void onRegistrationStateChanged(DelegateRegistrationState registrationState) {
+ // TODO: integrate registration changes to SipMessage verification checks.
+ }
+
+ @Override
+ public void onImsConfigurationChanged(SipDelegateImsConfiguration config) {
+ // Not needed for this Tracker
+ }
+
+ /**
+ * Open the transport and allow SIP messages to be sent/received on the delegate specified.
+ * @param delegate The delegate connection to send SIP messages to on the ImsService.
+ * @param deniedFeatureTags Feature tags that have been denied. Outgoing SIP messages relating
+ * to these tags will be denied.
+ */
+ public void openTransport(ISipDelegate delegate, Set<FeatureTagState> deniedFeatureTags) {
+ mSipDelegate = delegate;
+ mDelegateClosingReason = -1;
+ mDelegateClosedReason = -1;
+ // TODO: integrate denied tags to SipMessage verification checks.
+ }
+
+ /** Dump state about this tracker that should be included in the dumpsys */
+ public void dump(PrintWriter printWriter) {
+ mLocalLog.dump(printWriter);
+ }
+
+ /**
+ * @return SipDelegate implementation to be sent to IMS application.
+ */
+ public ISipDelegate getDelegateConnection() {
+ return mSipDelegateConnection;
+ }
+
+ /**
+ * @return MessageCallback implementation to be sent to the ImsService.
+ */
+ public ISipDelegateMessageCallback getMessageCallback() {
+ return mDelegateConnectionMessageCallback;
+ }
+
+ /**
+ * Gradually close all SIP Dialogs by:
+ * 1) denying all new outgoing SIP Dialog requests with the reason specified and
+ * 2) only allowing existing SIP Dialogs to continue.
+ * <p>
+ * This will allow traffic to continue on existing SIP Dialogs until a BYE is sent and the
+ * SIP Dialogs are closed or a timeout is hit and {@link SipDelegate#closeDialog(String)} is
+ * forcefully called on all open SIP Dialogs.
+ * <p>
+ * Any outgoing out-of-dialog traffic on this transport will be denied with the provided reason.
+ * <p>
+ * Incoming out-of-dialog traffic will continue to be set up until the SipDelegate is fully
+ * closed.
+ * @param delegateClosingReason The reason code to return to
+ * {@link DelegateMessageCallback#onMessageSendFailure(String, int)} if a new out-of-dialog SIP
+ * message is received while waiting for existing Dialogs.
+ * @param closedReason reason to return to new outgoing SIP messages via
+ * {@link SipDelegate#notifyMessageReceiveError(String, int)} once the transport
+ * transitions to the fully closed state.
+ * @param resultConsumer The consumer called when the message transport has been closed. It will
+ * return {@code true} if the procedure completed successfully or {@link false} if the
+ * transport needed to be closed forcefully due to the application not responding before
+ * a timeout occurred.
+ */
+ public void closeGracefully(int delegateClosingReason, int closedReason,
+ Consumer<Boolean> resultConsumer) {
+ mDelegateClosingReason = delegateClosingReason;
+ mPendingClosedConsumer = resultConsumer;
+ mExecutor.execute(() -> {
+ // TODO: Track SIP Dialogs and complete when there are no SIP dialogs open anymore or
+ // the timeout occurs.
+ mPendingClosedConsumer.accept(true);
+ mPendingClosedConsumer = null;
+ closeTransport(closedReason);
+ });
+ }
+
+ /**
+ * Close all ongoing SIP Dialogs immediately and respond to any incoming/outgoing messages with
+ * the provided reason.
+ * @param closedReason The failure reason to provide to incoming/outgoing SIP messages
+ * if an attempt is made to send/receive a message after this method is called.
+ */
+ public void close(int closedReason) {
+ closeTransport(closedReason);
+ }
+
+ // Clean up all state related to the existing SipDelegate immediately.
+ private void closeTransport(int closedReason) {
+ // TODO: add logic to forcefully close open SIP dialogs once they are being tracked.
+ mSipDelegate = null;
+ if (mPendingClosedConsumer != null) {
+ mExecutor.execute(() -> {
+ logw("closeTransport: transport close forced with pending consumer.");
+ mPendingClosedConsumer.accept(false /*closedGracefully*/);
+ mPendingClosedConsumer = null;
+ });
+ }
+ mDelegateClosingReason = -1;
+ mDelegateClosedReason = closedReason;
+ }
+
+ private VerificationResult verifyOutgoingMessage(SipMessage message) {
+ if (mDelegateClosingReason > -1) {
+ return new VerificationResult(mDelegateClosingReason);
+ }
+ if (mDelegateClosedReason > -1) {
+ return new VerificationResult(mDelegateClosedReason);
+ }
+ if (mSipDelegate == null) {
+ logw("sendMessage called when SipDelegate is not associated." + message);
+ return new VerificationResult(SipDelegateManager.MESSAGE_FAILURE_REASON_DELEGATE_DEAD);
+ }
+ return VerificationResult.SUCCESS;
+ }
+
+ private VerificationResult verifyIncomingMessage(SipMessage message) {
+ // Do not restrict incoming based on closing reason.
+ if (mDelegateClosedReason > -1) {
+ return new VerificationResult(mDelegateClosedReason);
+ }
+ return VerificationResult.SUCCESS;
+ }
+
+ private void notifyDelegateSendError(String logReason, SipMessage message, int reasonCode) {
+ // TODO parse SipMessage header for viaTransactionId.
+ logw("Error sending SipMessage[id: " + null + ", code: " + reasonCode + "] -> SipDelegate "
+ + "for reason: " + logReason);
+ try {
+ mAppCallback.onMessageSendFailure(null, reasonCode);
+ } catch (RemoteException e) {
+ logw("notifyDelegateSendError, SipDelegate is not available: " + e);
+ }
+ }
+
+ private void notifyAppReceiveError(String logReason, SipMessage message, int reasonCode) {
+ // TODO parse SipMessage header for viaTransactionId.
+ logw("Error sending SipMessage[id: " + null + ", code: " + reasonCode + "] -> "
+ + "SipDelegateConnection for reason: " + logReason);
+ try {
+ mSipDelegate.notifyMessageReceiveError(null, reasonCode);
+ } catch (RemoteException e) {
+ logw("notifyAppReceiveError, SipDelegate is not available: " + e);
+ }
+ }
+
+ private void logi(String log) {
+ Log.w(SipTransportController.LOG_TAG, TAG + "[" + mSubId + "] " + log);
+ mLocalLog.log("[I] " + log);
+ }
+
+ private void logw(String log) {
+ Log.w(SipTransportController.LOG_TAG, TAG + "[" + mSubId + "] " + log);
+ mLocalLog.log("[W] " + log);
+ }
+}
diff --git a/src/com/android/services/telephony/rcs/RcsFeatureController.java b/src/com/android/services/telephony/rcs/RcsFeatureController.java
index 8c6fce0..304a74d 100644
--- a/src/com/android/services/telephony/rcs/RcsFeatureController.java
+++ b/src/com/android/services/telephony/rcs/RcsFeatureController.java
@@ -74,6 +74,12 @@
* Called when the feature should be destroyed.
*/
void onDestroy();
+
+ /**
+ * Called when a dumpsys is being generated for this RcsFeatureController for all Features
+ * to report their status.
+ */
+ void dump(PrintWriter pw);
}
/**
@@ -427,6 +433,14 @@
pw.print("connected=");
synchronized (mLock) {
pw.println(mFeatureManager != null);
+ pw.println();
+ pw.println("RcsFeatureControllers:");
+ pw.increaseIndent();
+ for (Feature f : mFeatures.values()) {
+ f.dump(pw);
+ pw.println();
+ }
+ pw.decreaseIndent();
}
}
diff --git a/src/com/android/services/telephony/rcs/SipDelegateBinderConnection.java b/src/com/android/services/telephony/rcs/SipDelegateBinderConnection.java
new file mode 100644
index 0000000..8ae1936
--- /dev/null
+++ b/src/com/android/services/telephony/rcs/SipDelegateBinderConnection.java
@@ -0,0 +1,214 @@
+/*
+ * Copyright (C) 2020 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.services.telephony.rcs;
+
+import android.os.Binder;
+import android.os.RemoteException;
+import android.telephony.ims.DelegateRegistrationState;
+import android.telephony.ims.DelegateRequest;
+import android.telephony.ims.FeatureTagState;
+import android.telephony.ims.SipDelegateImsConfiguration;
+import android.telephony.ims.aidl.ISipDelegate;
+import android.telephony.ims.aidl.ISipDelegateMessageCallback;
+import android.telephony.ims.aidl.ISipDelegateStateCallback;
+import android.telephony.ims.aidl.ISipTransport;
+import android.telephony.ims.stub.SipDelegate;
+import android.util.LocalLog;
+import android.util.Log;
+
+import java.io.PrintWriter;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.Executor;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+
+/**
+ * Container for the active connection to the {@link SipDelegate} active on the ImsService.
+ * <p>
+ * New instances of this class will be created and destroyed new {@link SipDelegate}s are created
+ * and destroyed by the {@link SipDelegateController}.
+ */
+public class SipDelegateBinderConnection implements DelegateBinderStateManager {
+ private static final String LOG_TAG = "BinderConn";
+
+ protected final int mSubId;
+ protected final Set<FeatureTagState> mDeniedTags;
+ protected final Executor mExecutor;
+ protected final List<StateCallback> mStateCallbacks;
+
+ private final LocalLog mLocalLog = new LocalLog(SipTransportController.LOG_SIZE);
+
+ // Callback interface from ImsService to this Connection. State Events will be forwarded to IMS
+ // application through DelegateStateTracker.
+ private final ISipDelegateStateCallback mSipDelegateStateCallback =
+ new ISipDelegateStateCallback.Stub() {
+ @Override
+ public void onCreated(ISipDelegate delegate,
+ List<FeatureTagState> deniedFeatureTags) {
+ long token = Binder.clearCallingIdentity();
+ try {
+ mExecutor.execute(() ->
+ notifySipDelegateCreated(delegate, deniedFeatureTags));
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ @Override
+ public void onFeatureTagRegistrationChanged(
+ DelegateRegistrationState registrationState) {
+ long token = Binder.clearCallingIdentity();
+ try {
+ mExecutor.execute(() -> {
+ logi("onFeatureTagRegistrationChanged:" + registrationState);
+ for (StateCallback c : mStateCallbacks) {
+ c.onRegistrationStateChanged(registrationState);
+ }
+ });
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ @Override
+ public void onImsConfigurationChanged(
+ SipDelegateImsConfiguration registeredSipConfig) {
+ long token = Binder.clearCallingIdentity();
+ try {
+ mExecutor.execute(() -> {
+ logi("onImsConfigurationChanged");
+ for (StateCallback c : mStateCallbacks) {
+ c.onImsConfigurationChanged(registeredSipConfig);
+ }
+ });
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ @Override
+ public void onDestroyed(int reason) {
+ long token = Binder.clearCallingIdentity();
+ try {
+ mExecutor.execute(() -> notifySipDelegateDestroyed(reason));
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+ };
+
+ private final ISipTransport mSipTransport;
+ private final DelegateRequest mRequestedConfig;
+
+ private ISipDelegate mDelegateBinder;
+ private BiConsumer<ISipDelegate, Set<FeatureTagState>> mPendingCreatedConsumer;
+ private Consumer<Integer> mPendingDestroyedConsumer;
+
+ /**
+ * Create a new Connection object to manage the creation and destruction of a
+ * {@link SipDelegate}.
+ * @param subId The subid that this SipDelegate is being created for.
+ * @param sipTransport The SipTransport implementation that will be used to manage SipDelegates.
+ * @param requestedConfig The DelegateRequest to be sent to the ImsService.
+ * @param transportDeniedTags The feature tags that have already been denied by the
+ * SipTransportController and should not be requested.
+ * @param executor The Executor that all binder calls from the remote process will be executed
+ * on.
+ * @param stateCallbacks A list of callbacks that will each be called when the state of the
+ * SipDelegate changes. This will be called on the supplied executor.
+ */
+ public SipDelegateBinderConnection(int subId, ISipTransport sipTransport,
+ DelegateRequest requestedConfig, Set<FeatureTagState> transportDeniedTags,
+ Executor executor, List<StateCallback> stateCallbacks) {
+ mSubId = subId;
+ mSipTransport = sipTransport;
+ mRequestedConfig = requestedConfig;
+ mDeniedTags = transportDeniedTags;
+ mExecutor = executor;
+ mStateCallbacks = stateCallbacks;
+ }
+
+ @Override
+ public boolean create(ISipDelegateMessageCallback cb,
+ BiConsumer<ISipDelegate, Set<FeatureTagState>> createdConsumer) {
+ try {
+ mSipTransport.createSipDelegate(mSubId, mRequestedConfig, mSipDelegateStateCallback,
+ cb);
+ } catch (RemoteException e) {
+ logw("create called on unreachable SipTransport:" + e);
+ return false;
+ }
+ mPendingCreatedConsumer = createdConsumer;
+ return true;
+ }
+
+ @Override
+ public void destroy(int reason, Consumer<Integer> destroyedConsumer) {
+ mPendingDestroyedConsumer = destroyedConsumer;
+ try {
+ if (mDelegateBinder != null) {
+ mSipTransport.destroySipDelegate(mDelegateBinder, reason);
+ } else {
+ mExecutor.execute(() -> notifySipDelegateDestroyed(reason));
+ }
+ mStateCallbacks.clear();
+ } catch (RemoteException e) {
+ logw("destroy called on unreachable SipTransport:" + e);
+ mExecutor.execute(() -> notifySipDelegateDestroyed(reason));
+ }
+ }
+
+ private void notifySipDelegateCreated(ISipDelegate delegate,
+ List<FeatureTagState> deniedFeatureTags) {
+ logi("Delegate Created: " + delegate + ", deniedTags:" + deniedFeatureTags);
+ if (delegate == null) {
+ logw("Invalid null delegate returned!");
+ }
+ mDelegateBinder = delegate;
+ // Add denied feature tags from SipDelegate to the ones denied by the transport
+ if (deniedFeatureTags != null) {
+ mDeniedTags.addAll(deniedFeatureTags);
+ }
+ if (mPendingCreatedConsumer == null) return;
+ mPendingCreatedConsumer.accept(delegate, mDeniedTags);
+ mPendingCreatedConsumer = null;
+ }
+
+ private void notifySipDelegateDestroyed(int reason) {
+ logi("Delegate Destroyed, reason: " + reason);
+ mDelegateBinder = null;
+ if (mPendingDestroyedConsumer == null) return;
+ mPendingDestroyedConsumer.accept(reason);
+ mPendingDestroyedConsumer = null;
+ }
+
+ /** Dump state about this binder connection that should be included in the dumpsys. */
+ public void dump(PrintWriter printWriter) {
+ mLocalLog.dump(printWriter);
+ }
+
+ protected final void logi(String log) {
+ Log.i(SipTransportController.LOG_TAG, LOG_TAG + "[" + mSubId + "] " + log);
+ mLocalLog.log("[I] " + log);
+ }
+
+ protected final void logw(String log) {
+ Log.w(SipTransportController.LOG_TAG, LOG_TAG + "[" + mSubId + "] " + log);
+ mLocalLog.log("[W] " + log);
+ }
+}
diff --git a/src/com/android/services/telephony/rcs/SipDelegateBinderConnectionStub.java b/src/com/android/services/telephony/rcs/SipDelegateBinderConnectionStub.java
new file mode 100644
index 0000000..888af94
--- /dev/null
+++ b/src/com/android/services/telephony/rcs/SipDelegateBinderConnectionStub.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2020 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.services.telephony.rcs;
+
+import android.telephony.ims.DelegateRegistrationState;
+import android.telephony.ims.FeatureTagState;
+import android.telephony.ims.aidl.ISipDelegate;
+import android.telephony.ims.aidl.ISipDelegateMessageCallback;
+import android.telephony.ims.stub.SipDelegate;
+
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.Executor;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+
+/**
+ * Stub implementation used when a SipDelegate needs to be set up in specific cases, but there
+ * is no underlying implementation in the ImsService.
+ *
+ * This is used in cases where all of the requested feature tags were denied for various reasons
+ * from the SipTransportController. In this case, we will "connect", send a update to include the
+ * denied feature tags, and then do nothing until this stub is torn down.
+ */
+public class SipDelegateBinderConnectionStub implements DelegateBinderStateManager {
+ protected final Set<FeatureTagState> mDeniedTags;
+ protected final Executor mExecutor;
+ protected final List<StateCallback> mStateCallbacks;
+
+ /**
+ * Create a new Connection object to manage the creation and destruction of a
+ * {@link SipDelegate}.
+ * @param transportDeniedTags The feature tags that have already been denied by the
+ * SipTransportController and should not be requested.
+ * @param executor The Executor that all binder calls from the remote process will be executed
+ * on.
+ * @param stateCallbacks A list of callbacks that will each be called when the state of the
+ * SipDelegate changes. This will be called on the supplied executor.
+ */
+ public SipDelegateBinderConnectionStub(Set<FeatureTagState> transportDeniedTags,
+ Executor executor, List<StateCallback> stateCallbacks) {
+ mDeniedTags = transportDeniedTags;
+ mExecutor = executor;
+ mStateCallbacks = stateCallbacks;
+ }
+
+ @Override
+ public boolean create(ISipDelegateMessageCallback cb,
+ BiConsumer<ISipDelegate, Set<FeatureTagState>> createdConsumer) {
+ mExecutor.execute(() -> {
+ createdConsumer.accept(null, (mDeniedTags));
+ for (SipDelegateBinderConnection.StateCallback c: mStateCallbacks) {
+ c.onRegistrationStateChanged(new DelegateRegistrationState.Builder().build());
+ }
+ });
+ return true;
+ }
+
+ @Override
+ public void destroy(int reason, Consumer<Integer> destroyedConsumer) {
+ mExecutor.execute(() -> {
+ mStateCallbacks.clear();
+ destroyedConsumer.accept(reason);
+ });
+ }
+}
diff --git a/src/com/android/services/telephony/rcs/SipDelegateController.java b/src/com/android/services/telephony/rcs/SipDelegateController.java
new file mode 100644
index 0000000..ee9d4a7
--- /dev/null
+++ b/src/com/android/services/telephony/rcs/SipDelegateController.java
@@ -0,0 +1,372 @@
+/*
+ * Copyright (C) 2020 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.services.telephony.rcs;
+
+import android.telephony.ims.DelegateRegistrationState;
+import android.telephony.ims.DelegateRequest;
+import android.telephony.ims.FeatureTagState;
+import android.telephony.ims.SipDelegateConnection;
+import android.telephony.ims.SipDelegateManager;
+import android.telephony.ims.aidl.ISipDelegate;
+import android.telephony.ims.aidl.ISipDelegateConnectionStateCallback;
+import android.telephony.ims.aidl.ISipDelegateMessageCallback;
+import android.telephony.ims.aidl.ISipTransport;
+import android.telephony.ims.stub.DelegateConnectionStateCallback;
+import android.telephony.ims.stub.SipDelegate;
+import android.util.ArraySet;
+import android.util.LocalLog;
+import android.util.Log;
+import android.util.Pair;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.IndentingPrintWriter;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ScheduledExecutorService;
+
+/**
+ * Created when an IMS application wishes to open up a {@link SipDelegateConnection} and manages the
+ * resulting {@link SipDelegate} that may be created on the ImsService side.
+ */
+public class SipDelegateController {
+ static final String LOG_TAG = "SipDelegateC";
+
+ private DelegateBinderStateManager.Factory mBinderConnectionFactory =
+ new DelegateBinderStateManager.Factory() {
+ @Override
+ public DelegateBinderStateManager create(int subId, ISipTransport sipTransport,
+ DelegateRequest requestedConfig, Set<FeatureTagState> transportDeniedTags,
+ Executor executor, List<DelegateBinderStateManager.StateCallback> stateCallbacks) {
+ // We should not actually create a SipDelegate in this case.
+ if (requestedConfig.getFeatureTags().isEmpty()) {
+ return new SipDelegateBinderConnectionStub(transportDeniedTags, executor,
+ stateCallbacks);
+ }
+ return new SipDelegateBinderConnection(mSubId, mSipTransportImpl, requestedConfig,
+ transportDeniedTags, mExecutorService, stateCallbacks);
+ }
+ };
+
+ private final int mSubId;
+ private final String mPackageName;
+ private final DelegateRequest mInitialRequest;
+ private final ISipTransport mSipTransportImpl;
+ private final ScheduledExecutorService mExecutorService;
+ private final MessageTransportStateTracker mMessageTransportStateTracker;
+ private final DelegateStateTracker mDelegateStateTracker;
+ private final LocalLog mLocalLog = new LocalLog(SipTransportController.LOG_SIZE);
+
+ private DelegateBinderStateManager mBinderConnection;
+ private Set<String> mTrackedFeatureTags = new ArraySet<>();
+
+ public SipDelegateController(int subId, DelegateRequest initialRequest, String packageName,
+ ISipTransport sipTransportImpl, ScheduledExecutorService executorService,
+ ISipDelegateConnectionStateCallback stateCallback,
+ ISipDelegateMessageCallback messageCallback) {
+ mSubId = subId;
+ mPackageName = packageName;
+ mInitialRequest = initialRequest;
+ mSipTransportImpl = sipTransportImpl;
+ mExecutorService = executorService;
+
+ mMessageTransportStateTracker = new MessageTransportStateTracker(mSubId, executorService,
+ messageCallback);
+ mDelegateStateTracker = new DelegateStateTracker(mSubId, stateCallback,
+ mMessageTransportStateTracker.getDelegateConnection());
+ }
+
+ /**
+ * Inject dependencies for testing only.
+ */
+ @VisibleForTesting
+ public SipDelegateController(int subId, DelegateRequest initialRequest, String packageName,
+ ISipTransport sipTransportImpl, ScheduledExecutorService executorService,
+ MessageTransportStateTracker messageTransportStateTracker,
+ DelegateStateTracker delegateStateTracker,
+ DelegateBinderStateManager.Factory connectionFactory) {
+ mSubId = subId;
+ mInitialRequest = initialRequest;
+ mPackageName = packageName;
+ mSipTransportImpl = sipTransportImpl;
+ mExecutorService = executorService;
+ mMessageTransportStateTracker = messageTransportStateTracker;
+ mDelegateStateTracker = delegateStateTracker;
+ mBinderConnectionFactory = connectionFactory;
+ }
+
+ /**
+ * @return The InitialRequest from the IMS application. The feature tags that are actually set
+ * up may differ from this request based on the state of this controller.
+ */
+ public DelegateRequest getInitialRequest() {
+ return mInitialRequest;
+ }
+
+ /**
+ * @return The package name of the IMS application associated with this SipDelegateController.
+ */
+ public String getPackageName() {
+ return mPackageName;
+ }
+
+ public ISipDelegate getSipDelegateInterface() {
+ return mMessageTransportStateTracker.getDelegateConnection();
+ }
+
+ /**
+ * Create the underlying SipDelegate.
+ * <p>
+ * This may not happen instantly, The CompletableFuture returned will not complete until
+ * {@link DelegateConnectionStateCallback#onCreated(SipDelegateConnection)} is called by the
+ * SipDelegate or DelegateStateTracker state is updated in the case that all requested features
+ * were denied.
+ * @return A CompletableFuture that will complete once the SipDelegate has been created. If true
+ * is returned, the SipDelegate has been created successfully. If false, the ImsService is not
+ * reachable and the process should be aborted.
+ */
+ public CompletableFuture<Boolean> create(Set<String> supportedSet,
+ Set<FeatureTagState> deniedSet) {
+ logi("create, supported: " + supportedSet + ", denied: " + deniedSet);
+ mTrackedFeatureTags = supportedSet;
+ DelegateBinderStateManager connection =
+ createBinderConnection(supportedSet, deniedSet);
+ CompletableFuture<Pair<ISipDelegate, Set<FeatureTagState>>> pendingCreate =
+ createSipDelegate(connection);
+ // May need to implement special case handling where SipDelegate denies all in supportedSet,
+ // however that should be a very rare case. For now, if that happens, just keep the
+ // SipDelegate bound.
+ return pendingCreate.thenApplyAsync((resultPair) -> {
+ if (resultPair == null) {
+ logw("create: resultPair returned null");
+ return false;
+ }
+ mBinderConnection = connection;
+ logi("create: created, delegate denied: " + resultPair.second);
+ mMessageTransportStateTracker.openTransport(resultPair.first, resultPair.second);
+ mDelegateStateTracker.sipDelegateConnected(resultPair.second);
+ return true;
+ }, mExecutorService);
+ }
+
+ /**
+ * Modify the SipTransport to reflect the new Feature Tag set that the IMS application has
+ * access to.
+ * <p>
+ * This involves the following operations if the new supported tag set does not match the
+ * the existing set:
+ * 1) destroy the existing underlying SipDelegate. If there are SIP Dialogs that are active
+ * on the SipDelegate that is pending to be destroyed, we must move the feature tags into a
+ * deregistering state via
+ * {@link DelegateRegistrationState#DEREGISTERING_REASON_FEATURE_TAGS_CHANGING} to signal to the
+ * IMS application to close all dialogs before the operation can proceed. If any outgoing
+ * out-of-dialog messages are sent at this time, they will also fail with reason
+ * {@link SipDelegateManager#MESSAGE_FAILURE_REASON_INTERNAL_DELEGATE_STATE_TRANSITION}.
+ * 2) create a new underlying SipDelegate and notify trackers, allowing the transport to
+ * re-open.
+ * @param newSupportedSet The new supported set of feature tags that the SipDelegate should
+ * be opened for.
+ * @param deniedSet The new set of tags that have been denied as well as the reason for the
+ * denial to be reported back to the IMS Application.
+ * @return A CompletableFuture containing the pending operation that will change the supported
+ * feature tags. Any operations to change the supported feature tags of the associated
+ * SipDelegate after this should not happen until this pending operation completes. Will
+ * complete with {@code true} if the operation was successful or {@code false} if the
+ * IMS service was unreachable.
+ */
+ public CompletableFuture<Boolean> changeSupportedFeatureTags(Set<String> newSupportedSet,
+ Set<FeatureTagState> deniedSet) {
+ logi("Received feature tag set change, old: [" + mTrackedFeatureTags + "], new: "
+ + newSupportedSet + ",denied: [" + deniedSet + "]");
+ if (mTrackedFeatureTags.equals(newSupportedSet)) {
+ logi("changeSupportedFeatureTags: no change, returning");
+ return CompletableFuture.completedFuture(true);
+ }
+
+ mTrackedFeatureTags = newSupportedSet;
+ // Next perform the destroy operation.
+ CompletableFuture<Integer> pendingDestroy = destroySipDelegate(false/*force*/,
+ SipDelegateManager.MESSAGE_FAILURE_REASON_INTERNAL_DELEGATE_STATE_TRANSITION,
+ SipDelegateManager.MESSAGE_FAILURE_REASON_INTERNAL_DELEGATE_STATE_TRANSITION,
+ DelegateRegistrationState.DEREGISTERING_REASON_FEATURE_TAGS_CHANGING,
+ SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_REQUESTED_BY_APP);
+
+ // Next perform the create operation with the new set of supported feature tags.
+ return pendingDestroy.thenComposeAsync((reasonFromService) -> {
+ logi("changeSupportedFeatureTags: destroy stage complete, reason reported: "
+ + reasonFromService);
+ return create(newSupportedSet, deniedSet);
+ }, mExecutorService);
+ }
+
+ /**
+ * Destroy this SipDelegate. This controller should be disposed of after this method is
+ * called.
+ * <p>
+ * This may not happen instantly if there are SIP Dialogs that are active on this SipDelegate.
+ * In this case, the CompletableFuture will not complete until
+ * {@link DelegateConnectionStateCallback#onDestroyed(int)} is called by the SipDelegate.
+ * @param force If set true, we will close the transport immediately and call
+ * {@link SipDelegate#closeDialog(String)} on any open dialogs. If false, we will wait for the
+ * SIP Dialogs to close or the close timer to timeout before destroying the underlying
+ * SipDelegate.
+ * @param destroyReason The reason for why this SipDelegate is being destroyed.
+ * @return A CompletableFuture that will complete once the SipDelegate has been destroyed.
+ */
+ public CompletableFuture<Integer> destroy(boolean force, int destroyReason) {
+ logi("destroy, forced " + force + ", destroyReason: " + destroyReason);
+
+ CompletableFuture<Integer> pendingOperationComplete =
+ destroySipDelegate(force, SipDelegateManager.MESSAGE_FAILURE_REASON_DELEGATE_CLOSED,
+ getMessageFailReasonFromDestroyReason(destroyReason),
+ DelegateRegistrationState.DEREGISTERING_REASON_DESTROY_PENDING,
+ destroyReason);
+ return pendingOperationComplete.thenApplyAsync((reasonFromDelegate) -> {
+ logi("destroy, operation complete, notifying trackers, reason" + reasonFromDelegate);
+ mDelegateStateTracker.sipDelegateDestroyed(reasonFromDelegate);
+ return reasonFromDelegate;
+ }, mExecutorService);
+ };
+
+ private static int getMessageFailReasonFromDestroyReason(int destroyReason) {
+ switch (destroyReason) {
+ case SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_SERVICE_DEAD:
+ return SipDelegateManager.MESSAGE_FAILURE_REASON_DELEGATE_DEAD;
+ case SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_REQUESTED_BY_APP:
+ case SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_USER_DISABLED_RCS:
+ return SipDelegateManager.MESSAGE_FAILURE_REASON_DELEGATE_CLOSED;
+ default:
+ return SipDelegateManager.MESSAGE_FAILURE_REASON_UNKNOWN;
+ }
+ }
+
+ /**
+ * @param force If set true, we will close the transport immediately and call
+ * {@link SipDelegate#closeDialog(String)} on any open dialogs. If false, we will wait for the
+ * SIP Dialogs to close or the close timer to timeout before destroying the underlying
+ * SipDelegate.
+ * @param messageDestroyingReason The reason to send back to the IMS application in the case
+ * that a new outgoing SIP message is sent that is out-of-dialog while the message
+ * transport is closing.
+ * @param messageDestroyedReason The reason to send back to the IMS application in the case
+ * that a new outgoing SIP message is sent once the underlying transport is closed.
+ * @param deregisteringReason The deregistering state reported to the IMS application for all
+ * registered feature tags.
+ * @param delegateDestroyedReason The reason to send to the underlying SipDelegate that is being
+ * destroyed.
+ * @return A CompletableFuture containing the reason from the SipDelegate for why it was
+ * destroyed.
+ */
+ private CompletableFuture<Integer> destroySipDelegate(boolean force,
+ int messageDestroyingReason, int messageDestroyedReason, int deregisteringReason,
+ int delegateDestroyedReason) {
+ if (mBinderConnection == null) {
+ logi("destroySipDelegate, called when binder connection is already null");
+ return CompletableFuture.completedFuture(delegateDestroyedReason);
+ }
+ // First, bring down the message transport.
+ CompletableFuture<Boolean> pendingTransportClosed = new CompletableFuture<>();
+ if (force) {
+ logi("destroySipDelegate, forced");
+ mMessageTransportStateTracker.close(messageDestroyedReason);
+ pendingTransportClosed.complete(true);
+ } else {
+ mMessageTransportStateTracker.closeGracefully(messageDestroyingReason,
+ messageDestroyedReason, pendingTransportClosed::complete);
+ }
+
+ // Do not send an intermediate pending state to app if there are no open SIP dialogs to
+ // worry about.
+ if (!pendingTransportClosed.isDone()) {
+ mDelegateStateTracker.sipDelegateChanging(deregisteringReason);
+ } else {
+ logi("destroySipDelegate, skip DEREGISTERING_REASON_DESTROY_PENDING");
+ }
+
+ // Next, destroy the SipDelegate.
+ return pendingTransportClosed.thenComposeAsync((wasGraceful) -> {
+ logi("destroySipDelegate, transport gracefully closed = " + wasGraceful);
+ CompletableFuture<Integer> pendingDestroy = new CompletableFuture<>();
+ mBinderConnection.destroy(delegateDestroyedReason, pendingDestroy::complete);
+ return pendingDestroy;
+ }, mExecutorService);
+ }
+
+ /**
+ * @return a CompletableFuture that returns a Pair containing SipDelegate Binder interface as
+ * well as rejected feature tags or a {@code null} Pair instance if the ImsService is not
+ * available.
+ */
+ private CompletableFuture<Pair<ISipDelegate, Set<FeatureTagState>>> createSipDelegate(
+ DelegateBinderStateManager connection) {
+ CompletableFuture<Pair<ISipDelegate, Set<FeatureTagState>>> createdFuture =
+ new CompletableFuture<>();
+ boolean isStarted = connection.create(mMessageTransportStateTracker.getMessageCallback(),
+ (delegate, delegateDeniedTags) ->
+ createdFuture.complete(new Pair<>(delegate, delegateDeniedTags)));
+ if (!isStarted) {
+ logw("Couldn't create binder connection, ImsService is not available.");
+ connection.destroy(SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_SERVICE_DEAD, null);
+ return CompletableFuture.completedFuture(null);
+ }
+ return createdFuture;
+ }
+
+ private DelegateBinderStateManager createBinderConnection(Set<String> supportedSet,
+ Set<FeatureTagState> deniedSet) {
+
+ List<DelegateBinderStateManager.StateCallback> stateCallbacks = new ArrayList<>(2);
+ stateCallbacks.add(mDelegateStateTracker);
+ stateCallbacks.add(mMessageTransportStateTracker);
+
+ return mBinderConnectionFactory.create(mSubId, mSipTransportImpl,
+ new DelegateRequest(supportedSet), deniedSet, mExecutorService, stateCallbacks);
+ }
+
+ /**
+ * Write the current state of this controller in String format using the PrintWriter provided
+ * for dumpsys.
+ */
+ public void dump(PrintWriter printWriter) {
+ IndentingPrintWriter pw = new IndentingPrintWriter(printWriter, " ");
+ pw.println("SipDelegateController" + "[" + mSubId + "]:");
+ pw.increaseIndent();
+ pw.println("DelegateStateTracker:");
+ pw.increaseIndent();
+ mDelegateStateTracker.dump(printWriter);
+ pw.decreaseIndent();
+ pw.println("MessageStateTracker:");
+ pw.increaseIndent();
+ mMessageTransportStateTracker.dump(printWriter);
+ pw.decreaseIndent();
+ pw.decreaseIndent();
+ }
+
+ private void logi(String log) {
+ Log.w(SipTransportController.LOG_TAG, LOG_TAG + "[" + mSubId + "] " + log);
+ mLocalLog.log("[I] " + log);
+ }
+
+ private void logw(String log) {
+ Log.w(SipTransportController.LOG_TAG, LOG_TAG + "[" + mSubId + "] " + log);
+ mLocalLog.log("[W] " + log);
+ }
+}
diff --git a/src/com/android/services/telephony/rcs/SipTransportController.java b/src/com/android/services/telephony/rcs/SipTransportController.java
index 813834a..dd06cc1 100644
--- a/src/com/android/services/telephony/rcs/SipTransportController.java
+++ b/src/com/android/services/telephony/rcs/SipTransportController.java
@@ -16,26 +16,51 @@
package com.android.services.telephony.rcs;
+import android.app.role.OnRoleHoldersChangedListener;
+import android.app.role.RoleManager;
import android.content.Context;
+import android.os.UserHandle;
import android.telephony.ims.DelegateRequest;
+import android.telephony.ims.FeatureTagState;
import android.telephony.ims.ImsException;
import android.telephony.ims.ImsService;
+import android.telephony.ims.SipDelegateManager;
import android.telephony.ims.aidl.ISipDelegate;
import android.telephony.ims.aidl.ISipDelegateConnectionStateCallback;
import android.telephony.ims.aidl.ISipDelegateMessageCallback;
+import android.telephony.ims.aidl.ISipTransport;
import android.telephony.ims.stub.DelegateConnectionMessageCallback;
import android.telephony.ims.stub.DelegateConnectionStateCallback;
import android.telephony.ims.stub.SipDelegate;
+import android.text.TextUtils;
+import android.util.ArraySet;
+import android.util.LocalLog;
import android.util.Log;
+import androidx.annotation.NonNull;
+
import com.android.ims.RcsFeatureManager;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.IndentingPrintWriter;
+import com.google.common.base.Objects;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
import java.util.concurrent.Callable;
+import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
/**
* Manages the creation and destruction of SipDelegates in response to an IMS application requesting
@@ -46,16 +71,179 @@
* instead of requiring that the IMS application manage its own IMS registration over-the-top. This
* is required for some cellular carriers, which mandate that all IMS SIP traffic must be sent
* through a single IMS registration managed by the system IMS service.
+ *
+ * //TODO: Support other roles besides SMS
+ * //TODO: Bring in carrier provisioning to influence features that can be created.
+ * //TODO: Generate registration change events.
*/
-public class SipTransportController implements RcsFeatureController.Feature {
- private static final String LOG_TAG = "SipTransportC";
+public class SipTransportController implements RcsFeatureController.Feature,
+ OnRoleHoldersChangedListener {
+ static final int LOG_SIZE = 50;
+ static final String LOG_TAG = "SipTransportC";
- private final Context mContext;
+ /**See {@link TimerAdapter#getReevaluateThrottleTimerMilliseconds()}.*/
+ private static final int REEVALUATE_THROTTLE_DEFAULT_MS = 1000;
+ /**See {@link TimerAdapter#getUpdateRegistrationDelayMilliseconds()}.*/
+ private static final int TRIGGER_UPDATE_REGISTRATION_DELAY_DEFAULT_MS = 1000;
+
+ /**
+ * {@link RoleManager} is final so we have to wrap the implementation for testing.
+ */
+ @VisibleForTesting
+ public interface RoleManagerAdapter {
+ /** See {@link RoleManager#getRoleHolders(String)} */
+ List<String> getRoleHolders(String roleName);
+ /** See {@link RoleManager#addOnRoleHoldersChangedListenerAsUser} */
+ void addOnRoleHoldersChangedListenerAsUser(Executor executor,
+ OnRoleHoldersChangedListener listener, UserHandle user);
+ /** See {@link RoleManager#removeOnRoleHoldersChangedListenerAsUser} */
+ void removeOnRoleHoldersChangedListenerAsUser(OnRoleHoldersChangedListener listener,
+ UserHandle user);
+ }
+
+ /**
+ * Adapter for timers related to this class so they can be modified during testing.
+ */
+ @VisibleForTesting
+ public interface TimerAdapter {
+ /**
+ * Time we will delay after a {@link #createSipDelegate} or {@link #destroySipDelegate}
+ * command to re-evaluate and apply any changes to the list of tracked
+ * SipDelegateControllers.
+ * <p>
+ * Another create/destroy request sent during this time will not postpone re-evaluation
+ * again.
+ */
+ int getReevaluateThrottleTimerMilliseconds();
+
+ /**
+ * Time after re-evaluate we will wait to trigger the update of IMS registration.
+ * <p>
+ * Another re-evaluate while waiting to trigger a registration update will cause this
+ * controller to cancel and reschedule the event again, further delaying the trigger to send
+ * a registration update.
+ */
+ int getUpdateRegistrationDelayMilliseconds();
+ }
+
+ private static class TimerAdapterImpl implements TimerAdapter {
+
+ @Override
+ public int getReevaluateThrottleTimerMilliseconds() {
+ return REEVALUATE_THROTTLE_DEFAULT_MS;
+ }
+
+ @Override
+ public int getUpdateRegistrationDelayMilliseconds() {
+ return TRIGGER_UPDATE_REGISTRATION_DELAY_DEFAULT_MS;
+ }
+ }
+
+ private static class RoleManagerAdapterImpl implements RoleManagerAdapter {
+
+ private final RoleManager mRoleManager;
+
+ private RoleManagerAdapterImpl(Context context) {
+ mRoleManager = context.getSystemService(RoleManager.class);
+ }
+
+ @Override
+ public List<String> getRoleHolders(String roleName) {
+ return mRoleManager.getRoleHolders(roleName);
+ }
+
+ @Override
+ public void addOnRoleHoldersChangedListenerAsUser(Executor executor,
+ OnRoleHoldersChangedListener listener, UserHandle user) {
+ mRoleManager.addOnRoleHoldersChangedListenerAsUser(executor, listener, user);
+ }
+
+ @Override
+ public void removeOnRoleHoldersChangedListenerAsUser(OnRoleHoldersChangedListener listener,
+ UserHandle user) {
+ mRoleManager.removeOnRoleHoldersChangedListenerAsUser(listener, user);
+ }
+ }
+
+ /**
+ * Used in {@link #destroySipDelegate(int, ISipDelegate, int)} to store pending destroy
+ * requests.
+ */
+ private static final class DestroyRequest {
+ public final SipDelegateController controller;
+ public final int reason;
+
+ DestroyRequest(SipDelegateController c, int r) {
+ controller = c;
+ reason = r;
+ }
+
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ DestroyRequest that = (DestroyRequest) o;
+ return reason == that.reason
+ && controller.equals(that.controller);
+ }
+
+ @Override
+ public int hashCode() {
+ return java.util.Objects.hash(controller, reason);
+ }
+
+ @Override
+ public String toString() {
+ return "DestroyRequest{" + "controller=" + controller + ", reason=" + reason + '}';
+ }
+ }
+
+ /**
+ * Allow the ability for tests to easily mock out the SipDelegateController for testing.
+ */
+ @VisibleForTesting
+ public interface SipDelegateControllerFactory {
+ /** See {@link SipDelegateController} */
+ SipDelegateController create(int subId, DelegateRequest initialRequest, String packageName,
+ ISipTransport sipTransportImpl, ScheduledExecutorService executorService,
+ ISipDelegateConnectionStateCallback stateCallback,
+ ISipDelegateMessageCallback messageCallback);
+ }
+
+ private SipDelegateControllerFactory mDelegateControllerFactory = SipDelegateController::new;
private final int mSlotId;
private final ScheduledExecutorService mExecutorService;
+ private final RoleManagerAdapter mRoleManagerAdapter;
+ private final TimerAdapter mTimerAdapter;
+ private final LocalLog mLocalLog = new LocalLog(LOG_SIZE);
+ // A priority queue of active SipDelegateControllers, where the oldest SipDelegate gets
+ // access to the feature tag if multiple apps are allowed to request the same feature tag.
+ private final List<SipDelegateController> mDelegatePriorityQueue = new ArrayList<>();
+ // SipDelegateControllers who have been created and are pending to be added to the priority
+ // queue. Will be added into the queue in the same order as they were added here.
+ private final List<SipDelegateController> mDelegatePendingCreate = new ArrayList<>();
+ // SipDelegateControllers that are pending to be destroyed.
+ private final List<DestroyRequest> mDelegatePendingDestroy = new ArrayList<>();
+
+ // Future scheduled for operations that require the list of SipDelegateControllers to
+ // be evaluated. When the timer expires and triggers the reevaluate method, this controller
+ // will iterate through mDelegatePriorityQueue and assign Feature Tags based on role+priority.
+ private ScheduledFuture<?> mScheduledEvaluateFuture;
+ // mPendingEvaluateFTFuture creates this CompletableFuture, exposed in order to stop other
+ // evaluates from occurring while another is waiting for a result on other threads.
+ private CompletableFuture<Void> mEvaluateCompleteFuture;
+ // Future scheduled that will trigger the ImsService to update the IMS registration for the
+ // SipDelegate configuration. Will be scheduled TRIGGER_UPDATE_REGISTRATION_DELAY_MS
+ // milliseconds after a pending evaluate completes.
+ private ScheduledFuture<?> mPendingUpdateRegistrationFuture;
+ // Subscription id will change as new subscriptions are loaded on the slot.
private int mSubId;
+ // Will go up/down as the ImsService associated with this slotId goes up/down.
private RcsFeatureManager mRcsManager;
+ // Cached package name of the app that is considered the default SMS app.
+ private String mCachedSmsRolePackageName = "";
/**
* Create an instance of SipTransportController.
@@ -64,10 +252,11 @@
* @param subId The subscription ID associated with this controller when it was first created.
*/
public SipTransportController(Context context, int slotId, int subId) {
- mContext = context;
mSlotId = slotId;
mSubId = subId;
+ mRoleManagerAdapter = new RoleManagerAdapterImpl(context);
+ mTimerAdapter = new TimerAdapterImpl();
mExecutorService = Executors.newSingleThreadScheduledExecutor();
}
@@ -76,11 +265,14 @@
*/
@VisibleForTesting
public SipTransportController(Context context, int slotId, int subId,
- ScheduledExecutorService executor) {
- mContext = context;
+ SipDelegateControllerFactory delegateFactory, RoleManagerAdapter roleManagerAdapter,
+ TimerAdapter timerAdapter, ScheduledExecutorService executor) {
mSlotId = slotId;
mSubId = subId;
+ mRoleManagerAdapter = roleManagerAdapter;
+ mTimerAdapter = timerAdapter;
+ mDelegateControllerFactory = delegateFactory;
mExecutorService = executor;
logi("created");
}
@@ -102,8 +294,14 @@
@Override
public void onDestroy() {
- // Can be null in testing.
- mExecutorService.shutdownNow();
+ mExecutorService.submit(()-> {
+ // Ensure new create/destroy requests are denied.
+ mSubId = -1;
+ triggerDeregistrationEvent();
+ scheduleDestroyDelegates(
+ SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_SUBSCRIPTION_TORN_DOWN)
+ .thenRun(mExecutorService::shutdown);
+ });
}
/**
@@ -119,12 +317,24 @@
* @param delegateMessage The {@link DelegateConnectionMessageCallback} Binder Connection
* @throws ImsException if the request to create the {@link SipDelegate} did not complete.
*/
- public void createSipDelegate(int subId, DelegateRequest request,
+ public void createSipDelegate(int subId, DelegateRequest request, String packageName,
ISipDelegateConnectionStateCallback delegateState,
ISipDelegateMessageCallback delegateMessage) throws ImsException {
- // TODO implementation.
- throw new ImsException("createSipDelegate is not supported yet",
- ImsException.CODE_ERROR_UNSUPPORTED_OPERATION);
+ logi("createSipDelegate: request= " + request + ", packageName= " + packageName);
+ CompletableFuture<ImsException> result = new CompletableFuture<>();
+ mExecutorService.submit(() -> createSipDelegateInternal(subId, request, packageName,
+ delegateState,
+ // Capture any ImsExceptions generated during the process.
+ delegateMessage, result::complete));
+ try {
+ ImsException e = result.get();
+ logi("createSipDelegate: request finished");
+ if (e != null) {
+ throw e;
+ }
+ } catch (InterruptedException | ExecutionException e) {
+ logw("createSipDelegate: exception completing future: " + e);
+ }
}
/**
@@ -134,7 +344,7 @@
* @param reason The reason for why the {@link SipDelegate} was destroyed.
*/
public void destroySipDelegate(int subId, ISipDelegate connection, int reason) {
- // TODO implementation
+ mExecutorService.execute(() -> destroySipDelegateInternal(subId, connection, reason));
}
/**
@@ -151,6 +361,104 @@
return result;
}
+ private void createSipDelegateInternal(int subId, DelegateRequest request, String packageName,
+ ISipDelegateConnectionStateCallback delegateState,
+ ISipDelegateMessageCallback delegateMessage,
+ Consumer<ImsException> startedErrorConsumer) {
+ ISipTransport transport;
+ // Send back any errors via Consumer early in creation process if it is clear that the
+ // SipDelegate will never be created.
+ try {
+ checkStateOfController(subId);
+ transport = mRcsManager.getSipTransport();
+ if (transport == null) {
+ logw("createSipDelegateInternal, transport null during request.");
+ startedErrorConsumer.accept(new ImsException("SipTransport not supported",
+ ImsException.CODE_ERROR_UNSUPPORTED_OPERATION));
+ return;
+ } else {
+ // Release the binder thread as there were no issues processing the initial request.
+ startedErrorConsumer.accept(null);
+ }
+ } catch (ImsException e) {
+ logw("createSipDelegateInternal, ImsException during create: " + e);
+ startedErrorConsumer.accept(e);
+ return;
+ }
+
+ SipDelegateController c = mDelegateControllerFactory.create(subId, request, packageName,
+ transport, mExecutorService, delegateState, delegateMessage);
+ logi("createSipDelegateInternal: request= " + request + ", packageName= " + packageName
+ + ", controller created: " + c);
+ addPendingCreateAndEvaluate(c);
+ }
+
+ private void destroySipDelegateInternal(int subId, ISipDelegate connection, int reason) {
+ if (subId != mSubId) {
+ logw("destroySipDelegateInternal: ignoring destroy, as this is about to be destroyed "
+ + "anyway due to subId change, delegate=" + connection);
+ return;
+ }
+ if (connection == null) {
+ logw("destroySipDelegateInternal: ignoring destroy, null connection binder.");
+ return;
+ }
+ SipDelegateController match = null;
+ for (SipDelegateController controller : mDelegatePriorityQueue) {
+ if (Objects.equal(connection.asBinder(),
+ controller.getSipDelegateInterface().asBinder())) {
+ match = controller;
+ break;
+ }
+ }
+ if (match == null) {
+ logw("destroySipDelegateInternal: could not find matching connection=" + connection);
+ return;
+ }
+
+ logi("destroySipDelegateInternal: destroy queued for connection= " + connection);
+ addPendingDestroyAndEvaluate(match, reason);
+ }
+
+ /**
+ * Cancel pending update IMS registration events if they exist and instead send a deregister
+ * event.
+ */
+ private void triggerDeregistrationEvent() {
+ if (mPendingUpdateRegistrationFuture != null
+ && !mPendingUpdateRegistrationFuture.isDone()) {
+ // Cancel pending update and replace with a call to deregister now.
+ mPendingUpdateRegistrationFuture.cancel(false);
+ logi("triggerDeregistrationEvent: cancelling existing reg update event: "
+ + mPendingUpdateRegistrationFuture);
+ }
+ logi("triggerDeregistrationEvent: Sending deregister event to ImsService");
+ //TODO hook up registration apis
+ }
+
+ /**
+ * Schedule an update to the IMS registration. If there is an existing update scheduled, cancel
+ * it and reschedule.
+ */
+ private void scheduleUpdateRegistration() {
+ if (mPendingUpdateRegistrationFuture != null
+ && !mPendingUpdateRegistrationFuture.isDone()) {
+ // Cancel the old pending operation and reschedule again.
+ mPendingUpdateRegistrationFuture.cancel(false);
+ logi("scheduleUpdateRegistration: cancelling existing reg update event: "
+ + mPendingUpdateRegistrationFuture);
+ }
+ ScheduledFuture<?> f = mExecutorService.schedule(this::triggerUpdateRegistrationEvent,
+ mTimerAdapter.getUpdateRegistrationDelayMilliseconds(), TimeUnit.MILLISECONDS);
+ logi("scheduleUpdateRegistration: scheduling new event: " + f);
+ mPendingUpdateRegistrationFuture = f;
+ }
+
+ private void triggerUpdateRegistrationEvent() {
+ logi("triggerUpdateRegistrationEvent: Sending update registration event to ImsService");
+ //TODO hook up registration apis
+ }
+
/**
* Returns whether or not the ImsService implementation associated with the supplied subId
* supports the SipTransport APIs.
@@ -164,6 +472,277 @@
return (mRcsManager.getSipTransport() != null);
}
+ private boolean addPendingDestroy(SipDelegateController c, int reason) {
+ DestroyRequest request = new DestroyRequest(c, reason);
+ if (!mDelegatePendingDestroy.contains(request)) {
+ return mDelegatePendingDestroy.add(request);
+ }
+ return false;
+ }
+
+ /**
+ * The supplied SipDelegateController has been destroyed and associated feature tags have been
+ * released. Trigger the re-evaluation of the priority queue with the new configuration.
+ */
+ private void addPendingDestroyAndEvaluate(SipDelegateController c, int reason) {
+ if (addPendingDestroy(c, reason)) {
+ scheduleThrottledReevaluate();
+ }
+ }
+
+ /**
+ * A new SipDelegateController has been created, add to the back of the priority queue and
+ * trigger the re-evaluation of the priority queue with the new configuration.
+ */
+ private void addPendingCreateAndEvaluate(SipDelegateController c) {
+ mDelegatePendingCreate.add(c);
+ scheduleThrottledReevaluate();
+ }
+
+ /**
+ * The priority queue has changed, which will cause a re-evaluation of the feature tags granted
+ * to each SipDelegate.
+ * <p>
+ * Note: re-evaluations are throttled to happen at a minimum to occur every
+ * REEVALUATE_THROTTLE_MS seconds. We also do not reevaluate while another reevaluate operation
+ * is in progress, so in this case, defer schedule itself.
+ */
+ private void scheduleThrottledReevaluate() {
+ if (isEvaluatePendingAndNotInProgress()) {
+ logi("scheduleThrottledReevaluate: throttling reevaluate, eval already pending: "
+ + mScheduledEvaluateFuture);
+ } else {
+ mScheduledEvaluateFuture = mExecutorService.schedule(this::reevaluateDelegates,
+ mTimerAdapter.getReevaluateThrottleTimerMilliseconds(), TimeUnit.MILLISECONDS);
+ logi("scheduleThrottledReevaluate: new reevaluate scheduled: "
+ + mScheduledEvaluateFuture);
+ }
+ }
+
+ /**
+ * @return true if there is a evaluate pending, false if there is not. If evaluate has already
+ * begun, but we are waiting for it to complete, this will also return false.
+ */
+ private boolean isEvaluatePendingAndNotInProgress() {
+ boolean isEvalScheduled = mScheduledEvaluateFuture != null
+ && !mScheduledEvaluateFuture.isDone();
+ boolean isEvalInProgress = mEvaluateCompleteFuture != null
+ && !mEvaluateCompleteFuture.isDone();
+ return isEvalScheduled && !isEvalInProgress;
+ }
+
+ /**
+ * Cancel any pending re-evaluates and perform it as soon as possible. This is done in the case
+ * where we need to do something like tear down this controller or change subId.
+ */
+ private void scheduleReevaluateNow(CompletableFuture<Void> onDoneFuture) {
+ if (isEvaluatePendingAndNotInProgress()) {
+ mScheduledEvaluateFuture.cancel(false /*interrupt*/);
+ logi("scheduleReevaluateNow: cancelling pending reevaluate: "
+ + mScheduledEvaluateFuture);
+ }
+ // we have tasks that depend on this potentially, so once the last reevaluate is done,
+ // schedule a new one right away.
+ if (mEvaluateCompleteFuture != null && !mEvaluateCompleteFuture.isDone()) {
+ mEvaluateCompleteFuture.thenRunAsync(
+ () -> scheduleReevaluateNow(onDoneFuture), mExecutorService);
+ return;
+ }
+
+ reevaluateDelegates();
+ mEvaluateCompleteFuture.thenAccept(onDoneFuture::complete);
+ }
+
+ /**
+ * Apply all operations that have been pending by collecting pending create/destroy operations
+ * and batch applying them to the mDelegatePriorityQueue.
+ *
+ * First perform the operation of destroying all SipDelegateConnections that have been pending
+ * destroy. Next, add all pending new SipDelegateControllers to the end of
+ * mDelegatePriorityQueue and loop through all in the queue, applying feature tags to the
+ * appropriate SipDelegateController if they pass role checks and have not already been claimed
+ * by another delegate higher in the priority queue.
+ */
+ private void reevaluateDelegates() {
+ if (mEvaluateCompleteFuture != null && !mEvaluateCompleteFuture.isDone()) {
+ logw("reevaluateDelegates: last evaluate not done, deferring new request");
+ // Defer re-evaluate until after the pending re-evaluate is complete.
+ mEvaluateCompleteFuture.thenRunAsync(this::scheduleThrottledReevaluate,
+ mExecutorService);
+ return;
+ }
+
+ // Destroy all pending destroy delegates first. Order doesn't matter.
+ List<CompletableFuture<Void>> pendingDestroyList = mDelegatePendingDestroy.stream()
+ .map(d -> triggerDestroy(d.controller, d.reason)).collect(
+ Collectors.toList());
+ CompletableFuture<Void> pendingDestroy = CompletableFuture.allOf(
+ pendingDestroyList.toArray(new CompletableFuture[mDelegatePendingDestroy.size()]));
+ mDelegatePriorityQueue.removeAll(mDelegatePendingDestroy.stream().map(d -> d.controller)
+ .collect(Collectors.toList()));
+ mDelegatePendingDestroy.clear();
+
+ // Add newly created SipDelegates to end of queue before evaluating associated features.
+ mDelegatePriorityQueue.addAll(mDelegatePendingCreate);
+ for (SipDelegateController c : mDelegatePendingCreate) {
+ logi("reevaluateDelegates: pending create: " + c);
+ }
+ mDelegatePendingCreate.clear();
+
+ // Wait for destroy stages to complete, then loop from oldest to most recent and associate
+ // feature tags that the app has requested to the SipDelegate.
+ // Each feature tag can only be associated with one SipDelegate, so as feature tags are
+ // taken, do not allow other SipDelegates to be associated with those tags as well. Each
+ // stage of the CompletableFuture chain passes the previously claimed feature tags into the
+ // next stage so that those feature tags can be denied if already claimed.
+ // Executor doesn't matter here, just composing here to transform to the next stage.
+ CompletableFuture<Set<String>> pendingChange = pendingDestroy.thenCompose((ignore) -> {
+ logi("reevaluateDelegates: destroy phase complete");
+ return CompletableFuture.completedFuture(new ArraySet<>());
+ });
+ final String cachedSmsRolePackage = mCachedSmsRolePackageName;
+ for (SipDelegateController c : mDelegatePriorityQueue) {
+ logi("reevaluateDelegates: pending reeval: " + c);
+ pendingChange = pendingChange.thenComposeAsync((takenTags) -> {
+ logi("reevaluateDelegates: last stage completed with result:" + takenTags);
+ if (takenTags == null) {
+ // return early, the ImsService is no longer available. This will eventually be
+ // destroyed.
+ return CompletableFuture.completedFuture(null /*failed*/);
+ }
+ return changeSupportedFeatureTags(c, cachedSmsRolePackage, takenTags);
+ }, mExecutorService);
+ }
+
+ // Executor doesn't matter here, just adding an extra stage to print result.
+ mEvaluateCompleteFuture = pendingChange
+ .thenAccept((associatedFeatures) -> logi("reevaluateDelegates: reevaluate complete,"
+ + " feature tags associated: " + associatedFeatures));
+ logi("reevaluateDelegates: future created.");
+ }
+
+ private CompletableFuture<Void> triggerDestroy(SipDelegateController c, int reason) {
+ return c.destroy(isForcedFromReason(reason), reason)
+ // Executor doesn't matter here, just for logging.
+ .thenAccept((delegateReason) -> logi("destroy triggered with " + reason
+ + " and finished with reason= " + delegateReason));
+ }
+
+ private boolean isForcedFromReason(int reason) {
+ switch (reason) {
+ case SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_UNKNOWN:
+ logw("isForcedFromReason, unknown reason");
+ /*intentional fallthrough*/
+ case SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_REQUESTED_BY_APP:
+ /*intentional fallthrough*/
+ case SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_USER_DISABLED_RCS:
+ return false;
+ case SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_SERVICE_DEAD:
+ /*intentional fallthrough*/
+ case SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_SUBSCRIPTION_TORN_DOWN:
+ return true;
+ }
+ logw("isForcedFromReason, unexpected reason: " + reason);
+ return false;
+ }
+
+ /**
+ * Called by RoleManager when a role has changed so that we can query the new role holder.
+ * @param roleName the name of the role whose holders are changed
+ * @param user the user for this role holder change
+ */
+ // Called on mExecutorThread
+ @Override
+ public void onRoleHoldersChanged(@NonNull String roleName, @NonNull UserHandle user) {
+ logi("onRoleHoldersChanged, roleName= " + roleName + ", user= " + user);
+ // Only monitor changes on the system
+ if (!UserHandle.SYSTEM.equals(user)) {
+ return;
+ }
+
+ if (!RoleManager.ROLE_SMS.equals(roleName)) {
+ logi("onRoleHoldersChanged, ignoring non SMS role change");
+ // TODO: only target default sms app for now and add new roles later using
+ // CarrierConfigManager
+ return;
+ }
+ updateRoleCache();
+ // new denied tags will be picked up when reevaluate completes.
+ scheduleThrottledReevaluate();
+ }
+
+
+ private void updateRoleCache() {
+ // Only one app can fulfill the SMS role.
+ String newSmsRolePackageName = mRoleManagerAdapter.getRoleHolders(RoleManager.ROLE_SMS)
+ .stream().findFirst().orElse("");
+
+ if (TextUtils.equals(mCachedSmsRolePackageName, newSmsRolePackageName)) {
+ logi("onRoleHoldersChanged, skipping, role did not change");
+ return;
+ }
+ mCachedSmsRolePackageName = newSmsRolePackageName;
+ }
+
+ /**
+ * Check the requested roles for the specified package name and return the tags that were
+ * applied to that SipDelegateController.
+ * @param controller Controller to attribute feature tags to.
+ * @param alreadyRequestedTags The feature tags that were already granted to other SipDelegates.
+ * @return Once complete, contains the set of feature tags that the SipDelegate now has
+ * associated with it along with the feature tags that previous SipDelegates had.
+ *
+ * // TODO: we currently only track SMS role, extend to support other roles as well.
+ */
+ private CompletableFuture<Set<String>> changeSupportedFeatureTags(
+ SipDelegateController controller, String smsRolePackageName,
+ Set<String> alreadyRequestedTags) {
+ Set<String> requestedFeatureTags = controller.getInitialRequest().getFeatureTags();
+ String packageName = controller.getPackageName();
+ if (!smsRolePackageName.equals(packageName)) {
+ // Deny all tags.
+ Set<FeatureTagState> deniedTags = new ArraySet<>();
+ for (String s : requestedFeatureTags) {
+ deniedTags.add(new FeatureTagState(s,
+ SipDelegateManager.DENIED_REASON_NOT_ALLOWED));
+ }
+ CompletableFuture<Boolean> pendingDeny = controller.changeSupportedFeatureTags(
+ Collections.emptySet(), deniedTags);
+ logi("changeSupportedFeatureTags pendingDeny=" + pendingDeny);
+ // do not worry about executor used here, this stage used to interpret result + add log.
+ return pendingDeny.thenApply((completedSuccessfully) -> {
+ logi("changeSupportedFeatureTags: deny completed: " + completedSuccessfully);
+ if (!completedSuccessfully) return null;
+ // Return back the previous list of requested tags, as we did not add any more.
+ return alreadyRequestedTags;
+ });
+ }
+
+ ArraySet<String> previouslyGrantedTags = new ArraySet<>(alreadyRequestedTags);
+ // deny tags already used by other delegates
+ Set<FeatureTagState> deniedTags = new ArraySet<>();
+ for (String s : requestedFeatureTags) {
+ if (previouslyGrantedTags.contains(s)) {
+ deniedTags.add(new FeatureTagState(s,
+ SipDelegateManager.DENIED_REASON_IN_USE_BY_ANOTHER_DELEGATE));
+ }
+ }
+ Set<String> nonDeniedTags = requestedFeatureTags.stream()
+ .filter(r -> !previouslyGrantedTags.contains(r))
+ .collect(Collectors.toSet());
+ // Add newly granted tags to the already requested tags list.
+ previouslyGrantedTags.addAll(nonDeniedTags);
+ CompletableFuture<Boolean> pendingChange = controller.changeSupportedFeatureTags(
+ nonDeniedTags, deniedTags);
+ logi("changeSupportedFeatureTags pendingChange=" + pendingChange);
+ // do not worry about executor used here, this stage used to interpret result + add log.
+ return pendingChange.thenApply((completedSuccessfully) -> {
+ logi("changeSupportedFeatureTags: change completed: " + completedSuccessfully);
+ if (!completedSuccessfully) return null;
+ return previouslyGrantedTags;
+ });
+ }
+
/**
* Run a Callable on the ExecutorService Thread and wait for the result.
* If an ImsException is thrown, catch it and rethrow it to caller.
@@ -207,7 +786,26 @@
private void onRcsManagerChanged(RcsFeatureManager m) {
logi("manager changed, " + mRcsManager + "->" + m);
+ if (mRcsManager == m) return;
mRcsManager = m;
+ if (mRcsManager == null) {
+ logi("onRcsManagerChanged: lost connection to ImsService, tearing down...");
+ unregisterListeners();
+ scheduleDestroyDelegates(SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_SERVICE_DEAD);
+ } else {
+ registerListeners();
+ updateRoleCache();
+ }
+ }
+
+ private void registerListeners() {
+ mRoleManagerAdapter.addOnRoleHoldersChangedListenerAsUser(mExecutorService, this,
+ UserHandle.SYSTEM);
+ }
+
+ private void unregisterListeners() {
+ mRoleManagerAdapter.removeOnRoleHoldersChangedListenerAsUser(this, UserHandle.SYSTEM);
+ mCachedSmsRolePackageName = "";
}
/**
@@ -216,18 +814,65 @@
*/
private void onSubIdChanged(int newSubId) {
logi("subId changed, " + mSubId + "->" + newSubId);
- mSubId = newSubId;
+ if (mSubId != newSubId) {
+ // Swap subId, any pending create/destroy on old subId will be denied.
+ mSubId = newSubId;
+ scheduleDestroyDelegates(
+ SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_SUBSCRIPTION_TORN_DOWN);
+ return;
+ }
+ // TODO: if subId hasn't changed this means that we should load in any new carrier configs
+ // that we care about and apply.
}
- private void logi(String message) {
- Log.i(LOG_TAG, getPrefix() + ": " + message);
+ /**
+ * Destroy all tracked SipDelegateConnections due to the subscription being torn down.
+ * @return A CompletableFuture that will complete when all SipDelegates have been torn down.
+ */
+ private CompletableFuture<Void> scheduleDestroyDelegates(int reason) {
+ boolean addedDestroy = false;
+ for (SipDelegateController c : mDelegatePriorityQueue) {
+ logi("scheduleDestroyDelegates: Controller pending destroy: " + c);
+ addedDestroy |= addPendingDestroy(c, reason);
+ }
+ if (addedDestroy) {
+ CompletableFuture<Void> pendingDestroy = new CompletableFuture<>();
+ scheduleReevaluateNow(pendingDestroy);
+ return pendingDestroy;
+ } else {
+ return CompletableFuture.completedFuture(null);
+ }
}
- private void logw(String message) {
- Log.w(LOG_TAG, getPrefix() + ": " + message);
+ @Override
+ public void dump(PrintWriter printWriter) {
+ IndentingPrintWriter pw = new IndentingPrintWriter(printWriter, " ");
+ pw.println("SipTransportController" + "[" + mSlotId + "->" + mSubId + "]:");
+ pw.increaseIndent();
+ pw.println("LocalLog:");
+ pw.increaseIndent();
+ mLocalLog.dump(pw);
+ pw.decreaseIndent();
+ pw.println("SipDelegateControllers (in priority order):");
+ pw.increaseIndent();
+ if (mDelegatePriorityQueue.isEmpty()) {
+ pw.println("[NONE]");
+ } else {
+ for (SipDelegateController c : mDelegatePriorityQueue) {
+ c.dump(pw);
+ }
+ }
+ pw.decreaseIndent();
+ pw.decreaseIndent();
}
- private String getPrefix() {
- return "[" + mSlotId + "," + mSubId + "]";
+ private void logi(String log) {
+ Log.w(LOG_TAG, "[" + mSlotId + "->" + mSubId + "] " + log);
+ mLocalLog.log("[I] " + log);
+ }
+
+ private void logw(String log) {
+ Log.w(LOG_TAG, "[" + mSlotId + "->" + mSubId + "] " + log);
+ mLocalLog.log("[W] " + log);
}
}
diff --git a/src/com/android/services/telephony/rcs/UceControllerManager.java b/src/com/android/services/telephony/rcs/UceControllerManager.java
index db8c933..d1f91d1 100644
--- a/src/com/android/services/telephony/rcs/UceControllerManager.java
+++ b/src/com/android/services/telephony/rcs/UceControllerManager.java
@@ -28,7 +28,9 @@
import com.android.ims.RcsFeatureManager;
import com.android.ims.rcs.uce.UceController;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.IndentingPrintWriter;
+import java.io.PrintWriter;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
@@ -231,4 +233,15 @@
}
return true;
}
+
+
+ @Override
+ public void dump(PrintWriter printWriter) {
+ IndentingPrintWriter pw = new IndentingPrintWriter(printWriter, " ");
+ pw.println("UceControllerManager" + "[" + mSlotId + "]:");
+ pw.increaseIndent();
+ pw.println("UceController available = " + mUceController != null);
+ //TODO: Add dump for UceController
+ pw.decreaseIndent();
+ }
}
diff --git a/tests/src/com/android/TelephonyTestBase.java b/tests/src/com/android/TelephonyTestBase.java
index 132d893..502740d 100644
--- a/tests/src/com/android/TelephonyTestBase.java
+++ b/tests/src/com/android/TelephonyTestBase.java
@@ -27,6 +27,7 @@
import org.mockito.MockitoAnnotations;
import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
/**
@@ -58,6 +59,23 @@
PhoneConfigurationManager.unregisterAllMultiSimConfigChangeRegistrants();
}
+ protected final boolean waitForExecutorAction(Executor executor, long timeoutMillis) {
+ final CountDownLatch lock = new CountDownLatch(1);
+ Log.i("BRAD", "waitForExecutorAction");
+ executor.execute(() -> {
+ Log.i("BRAD", "countdown");
+ lock.countDown();
+ });
+ while (lock.getCount() > 0) {
+ try {
+ return lock.await(timeoutMillis, TimeUnit.MILLISECONDS);
+ } catch (InterruptedException e) {
+ // do nothing
+ }
+ }
+ return true;
+ }
+
protected final void waitForHandlerAction(Handler h, long timeoutMillis) {
final CountDownLatch lock = new CountDownLatch(1);
h.post(lock::countDown);
diff --git a/tests/src/com/android/services/telephony/rcs/DelegateStateTrackerTest.java b/tests/src/com/android/services/telephony/rcs/DelegateStateTrackerTest.java
new file mode 100644
index 0000000..4d40702
--- /dev/null
+++ b/tests/src/com/android/services/telephony/rcs/DelegateStateTrackerTest.java
@@ -0,0 +1,293 @@
+/*
+ * Copyright (C) 2020 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.services.telephony.rcs;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+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 android.telephony.ims.DelegateRegistrationState;
+import android.telephony.ims.FeatureTagState;
+import android.telephony.ims.SipDelegateImsConfiguration;
+import android.telephony.ims.SipDelegateManager;
+import android.telephony.ims.aidl.ISipDelegate;
+import android.telephony.ims.aidl.ISipDelegateConnectionStateCallback;
+import android.util.ArraySet;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.TelephonyTestBase;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+@RunWith(AndroidJUnit4.class)
+public class DelegateStateTrackerTest extends TelephonyTestBase {
+ private static final int TEST_SUB_ID = 1;
+
+ @Mock private ISipDelegate mSipDelegate;
+ @Mock private ISipDelegateConnectionStateCallback mAppCallback;
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ /**
+ * When an underlying SipDelegate is created, the app should only receive one onCreated callback
+ * independent of how many times sipDelegateConnected is called. Once created, registration
+ * and IMS configuration events should propagate up to the app as well.
+ */
+ @SmallTest
+ @Test
+ public void testDelegateCreated() throws Exception {
+ DelegateStateTracker stateTracker = new DelegateStateTracker(TEST_SUB_ID, mAppCallback,
+ mSipDelegate);
+ Set<FeatureTagState> deniedTags = getMmTelDeniedTag();
+ stateTracker.sipDelegateConnected(deniedTags);
+ // Calling connected multiple times should not generate multiple onCreated events.
+ stateTracker.sipDelegateConnected(deniedTags);
+ verify(mAppCallback).onCreated(mSipDelegate);
+
+ // Ensure status updates are sent to app as expected.
+ DelegateRegistrationState regState = new DelegateRegistrationState.Builder()
+ .addRegisteredFeatureTag(ImsSignallingUtils.ONE_TO_ONE_CHAT_TAG)
+ .build();
+ SipDelegateImsConfiguration config = new SipDelegateImsConfiguration.Builder(1/*version*/)
+ .build();
+ stateTracker.onRegistrationStateChanged(regState);
+ stateTracker.onImsConfigurationChanged(config);
+ verify(mAppCallback).onFeatureTagStatusChanged(eq(regState),
+ eq(new ArrayList<>(deniedTags)));
+ verify(mAppCallback).onImsConfigurationChanged(config);
+
+ verify(mAppCallback, never()).onDestroyed(anyInt());
+ }
+
+ /**
+ * onDestroyed should be called when sipDelegateDestroyed is called.
+ */
+ @SmallTest
+ @Test
+ public void testDelegateDestroyed() throws Exception {
+ DelegateStateTracker stateTracker = new DelegateStateTracker(TEST_SUB_ID, mAppCallback,
+ mSipDelegate);
+ Set<FeatureTagState> deniedTags = getMmTelDeniedTag();
+ stateTracker.sipDelegateConnected(deniedTags);
+
+ stateTracker.sipDelegateDestroyed(
+ SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_REQUESTED_BY_APP);
+ verify(mAppCallback).onDestroyed(
+ SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_REQUESTED_BY_APP);
+ }
+
+ /**
+ * When a SipDelegate is created and then an event occurs that will destroy->create a new
+ * SipDelegate underneath, we need to move the state of the features that are reporting
+ * registered to DEREGISTERING_REASON_FEATURE_TAGS_CHANGING so that the app can close dialogs on
+ * it. Once the new underlying SipDelegate is created, we must verify that the new registration
+ * is propagated up without any overrides.
+ */
+ @SmallTest
+ @Test
+ public void testDelegateChangingRegisteredTagsOverride() throws Exception {
+ DelegateStateTracker stateTracker = new DelegateStateTracker(TEST_SUB_ID, mAppCallback,
+ mSipDelegate);
+ Set<FeatureTagState> deniedTags = getMmTelDeniedTag();
+ stateTracker.sipDelegateConnected(deniedTags);
+ // SipDelegate created
+ verify(mAppCallback).onCreated(mSipDelegate);
+ DelegateRegistrationState regState = new DelegateRegistrationState.Builder()
+ .addRegisteredFeatureTag(ImsSignallingUtils.ONE_TO_ONE_CHAT_TAG)
+ .addDeregisteringFeatureTag(ImsSignallingUtils.FILE_TRANSFER_HTTP_TAG,
+ DelegateRegistrationState.DEREGISTERING_REASON_PROVISIONING_CHANGE)
+ .addDeregisteredFeatureTag(ImsSignallingUtils.GROUP_CHAT_TAG,
+ DelegateRegistrationState.DEREGISTERED_REASON_NOT_PROVISIONED)
+ .build();
+ stateTracker.onRegistrationStateChanged(regState);
+ // Simulate underlying SipDelegate switch
+ stateTracker.sipDelegateChanging(
+ DelegateRegistrationState.DEREGISTERING_REASON_FEATURE_TAGS_CHANGING);
+ // onFeatureTagStatusChanged should now be called with registered features overridden with
+ // DEREGISTERING_REASON_FEATURE_TAGS_CHANGING
+ DelegateRegistrationState overrideRegState = new DelegateRegistrationState.Builder()
+ .addDeregisteringFeatureTag(ImsSignallingUtils.ONE_TO_ONE_CHAT_TAG,
+ DelegateRegistrationState.DEREGISTERING_REASON_FEATURE_TAGS_CHANGING)
+ // Already Deregistering/Deregistered tags should not be overridden.
+ .addDeregisteringFeatureTag(ImsSignallingUtils.FILE_TRANSFER_HTTP_TAG,
+ DelegateRegistrationState.DEREGISTERING_REASON_PROVISIONING_CHANGE)
+ .addDeregisteredFeatureTag(ImsSignallingUtils.GROUP_CHAT_TAG,
+ DelegateRegistrationState.DEREGISTERED_REASON_NOT_PROVISIONED)
+ .build();
+ // new underlying SipDelegate created
+ stateTracker.sipDelegateConnected(deniedTags);
+ stateTracker.onRegistrationStateChanged(regState);
+
+ // Verify registration state through the process:
+ ArgumentCaptor<DelegateRegistrationState> regCaptor =
+ ArgumentCaptor.forClass(DelegateRegistrationState.class);
+ verify(mAppCallback, times(3)).onFeatureTagStatusChanged(
+ regCaptor.capture(), eq(new ArrayList<>(deniedTags)));
+ List<DelegateRegistrationState> testStates = regCaptor.getAllValues();
+ // feature tags should first be registered
+ assertEquals(regState, testStates.get(0));
+ // registered feature tags should have moved to deregistering
+ assertEquals(overrideRegState, testStates.get(1));
+ // and then moved back to registered after underlying FT change done.
+ assertEquals(regState, testStates.get(2));
+
+ //onCreate should only have been called once and onDestroy should have never been called.
+ verify(mAppCallback).onCreated(mSipDelegate);
+ verify(mAppCallback, never()).onDestroyed(anyInt());
+ }
+
+ /**
+ * Test the case that when the underlying Denied tags change in the SipDelegate, the change is
+ * properly shown in the registration update event.
+ */
+ @SmallTest
+ @Test
+ public void testDelegateChangingDeniedTagsChanged() throws Exception {
+ DelegateStateTracker stateTracker = new DelegateStateTracker(TEST_SUB_ID, mAppCallback,
+ mSipDelegate);
+ Set<FeatureTagState> deniedTags = getMmTelDeniedTag();
+ stateTracker.sipDelegateConnected(deniedTags);
+ // SipDelegate created
+ verify(mAppCallback).onCreated(mSipDelegate);
+ DelegateRegistrationState regState = new DelegateRegistrationState.Builder()
+ .addRegisteredFeatureTag(ImsSignallingUtils.ONE_TO_ONE_CHAT_TAG)
+ .build();
+ stateTracker.onRegistrationStateChanged(regState);
+ // Simulate underlying SipDelegate switch
+ stateTracker.sipDelegateChanging(
+ DelegateRegistrationState.DEREGISTERING_REASON_FEATURE_TAGS_CHANGING);
+ // onFeatureTagStatusChanged should now be called with registered features overridden with
+ // DEREGISTERING_REASON_FEATURE_TAGS_CHANGING
+ DelegateRegistrationState overrideRegState = new DelegateRegistrationState.Builder()
+ .addDeregisteringFeatureTag(ImsSignallingUtils.ONE_TO_ONE_CHAT_TAG,
+ DelegateRegistrationState.DEREGISTERING_REASON_FEATURE_TAGS_CHANGING)
+ .build();
+ // Verify registration state so far.
+ ArgumentCaptor<DelegateRegistrationState> regCaptor =
+ ArgumentCaptor.forClass(DelegateRegistrationState.class);
+ verify(mAppCallback, times(2)).onFeatureTagStatusChanged(
+ regCaptor.capture(), eq(new ArrayList<>(deniedTags)));
+ List<DelegateRegistrationState> testStates = regCaptor.getAllValues();
+ assertEquals(2, testStates.size());
+ // feature tags should first be registered
+ assertEquals(regState, testStates.get(0));
+ // registered feature tags should have moved to deregistering
+ assertEquals(overrideRegState, testStates.get(1));
+
+ // new underlying SipDelegate created, but SipDelegate denied one to one chat
+ deniedTags.add(new FeatureTagState(ImsSignallingUtils.ONE_TO_ONE_CHAT_TAG,
+ SipDelegateManager.DENIED_REASON_NOT_ALLOWED));
+ stateTracker.sipDelegateConnected(deniedTags);
+ DelegateRegistrationState fullyDeniedRegState = new DelegateRegistrationState.Builder()
+ .build();
+ // In this special case, it will be the SipDelegateConnectionBase that will trigger
+ // reg state change.
+ stateTracker.onRegistrationStateChanged(fullyDeniedRegState);
+ verify(mAppCallback).onFeatureTagStatusChanged(regCaptor.capture(),
+ eq(new ArrayList<>(deniedTags)));
+ // now all feature tags denied, so we should see only denied tags.
+ assertEquals(fullyDeniedRegState, regCaptor.getValue());
+
+ //onCreate should only have been called once and onDestroy should have never been called.
+ verify(mAppCallback).onCreated(mSipDelegate);
+ verify(mAppCallback, never()).onDestroyed(anyInt());
+ }
+
+ /**
+ * Test that when we move from changing tags state to the delegate being destroyed, we get the
+ * correct onDestroy event sent to the app.
+ */
+ @SmallTest
+ @Test
+ public void testDelegateChangingDeniedTagsChangingToDestroy() throws Exception {
+ DelegateStateTracker stateTracker = new DelegateStateTracker(TEST_SUB_ID, mAppCallback,
+ mSipDelegate);
+ Set<FeatureTagState> deniedTags = getMmTelDeniedTag();
+ stateTracker.sipDelegateConnected(deniedTags);
+ // SipDelegate created
+ verify(mAppCallback).onCreated(mSipDelegate);
+ DelegateRegistrationState regState = new DelegateRegistrationState.Builder()
+ .addRegisteredFeatureTag(ImsSignallingUtils.ONE_TO_ONE_CHAT_TAG)
+ .addDeregisteredFeatureTag(ImsSignallingUtils.GROUP_CHAT_TAG,
+ DelegateRegistrationState.DEREGISTERED_REASON_NOT_PROVISIONED)
+ .build();
+ stateTracker.onRegistrationStateChanged(regState);
+ verify(mAppCallback).onFeatureTagStatusChanged(any(),
+ eq(new ArrayList<>(deniedTags)));
+ // Simulate underlying SipDelegate switch
+ stateTracker.sipDelegateChanging(
+ DelegateRegistrationState.DEREGISTERING_REASON_DESTROY_PENDING);
+ // Destroy
+ stateTracker.sipDelegateDestroyed(
+ SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_REQUESTED_BY_APP);
+
+ // onFeatureTagStatusChanged should now be called with registered features overridden with
+ // DEREGISTERING_REASON_DESTROY_PENDING
+ DelegateRegistrationState overrideRegState = new DelegateRegistrationState.Builder()
+ .addDeregisteringFeatureTag(ImsSignallingUtils.ONE_TO_ONE_CHAT_TAG,
+ DelegateRegistrationState.DEREGISTERING_REASON_DESTROY_PENDING)
+ // Deregistered should stay the same.
+ .addDeregisteredFeatureTag(ImsSignallingUtils.GROUP_CHAT_TAG,
+ DelegateRegistrationState.DEREGISTERED_REASON_NOT_PROVISIONED)
+ .build();
+ // Verify registration state through process:
+ ArgumentCaptor<DelegateRegistrationState> regCaptor =
+ ArgumentCaptor.forClass(DelegateRegistrationState.class);
+ verify(mAppCallback, times(2)).onFeatureTagStatusChanged(regCaptor.capture(),
+ eq(new ArrayList<>(deniedTags)));
+ List<DelegateRegistrationState> testStates = regCaptor.getAllValues();
+ assertEquals(2, testStates.size());
+ // feature tags should first be registered
+ assertEquals(regState, testStates.get(0));
+ // registered feature tags should have moved to deregistering
+ assertEquals(overrideRegState, testStates.get(1));
+ //onCreate/onDestroy should only be called once.
+ verify(mAppCallback).onCreated(mSipDelegate);
+ verify(mAppCallback).onDestroyed(
+ SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_REQUESTED_BY_APP);
+ }
+
+ private Set<FeatureTagState> getMmTelDeniedTag() {
+ Set<FeatureTagState> deniedTags = new ArraySet<>();
+ deniedTags.add(new FeatureTagState(ImsSignallingUtils.MMTEL_TAG,
+ SipDelegateManager.DENIED_REASON_NOT_ALLOWED));
+ return deniedTags;
+ }
+}
diff --git a/tests/src/com/android/services/telephony/rcs/ImsSignallingUtils.java b/tests/src/com/android/services/telephony/rcs/ImsSignallingUtils.java
new file mode 100644
index 0000000..d607f6d
--- /dev/null
+++ b/tests/src/com/android/services/telephony/rcs/ImsSignallingUtils.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2020 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.services.telephony.rcs;
+
+/**
+ * Various definitions and utilities related to IMS Signalling.
+ */
+public class ImsSignallingUtils {
+ public static final String MMTEL_TAG =
+ "+g.3gpp.iari-ref=\"urn%3Aurn-7%3A3gpp-service.ims.icsi.mmtel\"";
+ public static final String ONE_TO_ONE_CHAT_TAG =
+ "+g.3gpp.icsi-ref=\"urn%3Aurn-7%3A3gppservice.ims.icsi.oma.cpm.msg\"";
+ public static final String GROUP_CHAT_TAG =
+ "+g.3gpp.icsi-ref=\"urn%3Aurn-7%3A3gppservice.ims.icsi.oma.cpm.session\"";
+ public static final String FILE_TRANSFER_HTTP_TAG =
+ "+g.3gpp.iari-ref=\"urn%3Aurn-7%3A3gppapplication.ims.iari.rcs.fthttp\"";
+}
diff --git a/tests/src/com/android/services/telephony/rcs/MessageTransportStateTrackerTest.java b/tests/src/com/android/services/telephony/rcs/MessageTransportStateTrackerTest.java
new file mode 100644
index 0000000..7ba4252
--- /dev/null
+++ b/tests/src/com/android/services/telephony/rcs/MessageTransportStateTrackerTest.java
@@ -0,0 +1,242 @@
+/*
+ * Copyright (C) 2020 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.services.telephony.rcs;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.os.RemoteException;
+import android.telephony.ims.SipDelegateManager;
+import android.telephony.ims.SipMessage;
+import android.telephony.ims.aidl.ISipDelegate;
+import android.telephony.ims.aidl.ISipDelegateMessageCallback;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.TelephonyTestBase;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.concurrent.Executor;
+import java.util.function.Consumer;
+
+@RunWith(AndroidJUnit4.class)
+public class MessageTransportStateTrackerTest extends TelephonyTestBase {
+ private static final int TEST_SUB_ID = 1;
+
+ private static final SipMessage TEST_MESSAGE = new SipMessage(
+ "INVITE sip:callee@ex.domain.com SIP/2.0",
+ "Via: SIP/2.0/UDP ex.place.com;branch=z9hG4bK776asdhds",
+ new byte[0]);
+
+ // Use for finer-grained control of when the Executor executes.
+ private static class PendingExecutor implements Executor {
+ private final ArrayList<Runnable> mPendingRunnables = new ArrayList<>();
+
+ @Override
+ public void execute(Runnable command) {
+ mPendingRunnables.add(command);
+ }
+
+ public void executePending() {
+ for (Runnable r : mPendingRunnables) {
+ r.run();
+ }
+ mPendingRunnables.clear();
+ }
+ }
+
+ @Mock private ISipDelegateMessageCallback mDelegateMessageCallback;
+ @Mock private ISipDelegate mISipDelegate;
+ @Mock private Consumer<Boolean> mMockCloseConsumer;
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ @SmallTest
+ @Test
+ public void testDelegateConnectionSendOutgoingMessage() throws Exception {
+ MessageTransportStateTracker tracker = new MessageTransportStateTracker(TEST_SUB_ID,
+ Runnable::run, mDelegateMessageCallback);
+
+ tracker.openTransport(mISipDelegate, Collections.emptySet());
+ tracker.getDelegateConnection().sendMessage(TEST_MESSAGE, 1 /*version*/);
+ verify(mISipDelegate).sendMessage(TEST_MESSAGE, 1 /*version*/);
+
+ doThrow(new RemoteException()).when(mISipDelegate).sendMessage(any(), anyInt());
+ tracker.getDelegateConnection().sendMessage(TEST_MESSAGE, 1 /*version*/);
+ verify(mDelegateMessageCallback).onMessageSendFailure(any(),
+ eq(SipDelegateManager.MESSAGE_FAILURE_REASON_DELEGATE_DEAD));
+
+ tracker.close(SipDelegateManager.MESSAGE_FAILURE_REASON_DELEGATE_CLOSED);
+ tracker.getDelegateConnection().sendMessage(TEST_MESSAGE, 1 /*version*/);
+ verify(mDelegateMessageCallback).onMessageSendFailure(any(),
+ eq(SipDelegateManager.MESSAGE_FAILURE_REASON_DELEGATE_CLOSED));
+ }
+
+ @SmallTest
+ @Test
+ public void testDelegateConnectionCloseGracefully() throws Exception {
+ PendingExecutor executor = new PendingExecutor();
+ MessageTransportStateTracker tracker = new MessageTransportStateTracker(TEST_SUB_ID,
+ executor, mDelegateMessageCallback);
+
+ tracker.openTransport(mISipDelegate, Collections.emptySet());
+ tracker.getDelegateConnection().sendMessage(TEST_MESSAGE, 1 /*version*/);
+ executor.executePending();
+ verify(mISipDelegate).sendMessage(TEST_MESSAGE, 1 /*version*/);
+ verify(mDelegateMessageCallback, never()).onMessageSendFailure(any(), anyInt());
+
+ // Use PendingExecutor a little weird here, we need to queue sendMessage first, even though
+ // closeGracefully will complete partly synchronously to test that the pending message will
+ // return MESSAGE_FAILURE_REASON_INTERNAL_DELEGATE_STATE_TRANSITION before the scheduled
+ // graceful close operation completes.
+ tracker.getDelegateConnection().sendMessage(TEST_MESSAGE, 1 /*version*/);
+ tracker.closeGracefully(
+ SipDelegateManager.MESSAGE_FAILURE_REASON_INTERNAL_DELEGATE_STATE_TRANSITION,
+ SipDelegateManager.MESSAGE_FAILURE_REASON_DELEGATE_CLOSED,
+ mMockCloseConsumer);
+ verify(mMockCloseConsumer, never()).accept(any());
+ // resolve pending close operation
+ executor.executePending();
+ verify(mDelegateMessageCallback).onMessageSendFailure(any(),
+ eq(SipDelegateManager.MESSAGE_FAILURE_REASON_INTERNAL_DELEGATE_STATE_TRANSITION));
+ // Still should only report one call of sendMessage from before
+ verify(mISipDelegate).sendMessage(TEST_MESSAGE, 1 /*version*/);
+ verify(mMockCloseConsumer).accept(true);
+
+ // ensure that after close operation completes, we get the correct
+ // MESSAGE_FAILURE_REASON_DELEGATE_CLOSED message.
+ tracker.getDelegateConnection().sendMessage(TEST_MESSAGE, 1 /*version*/);
+ executor.executePending();
+ verify(mDelegateMessageCallback).onMessageSendFailure(any(),
+ eq(SipDelegateManager.MESSAGE_FAILURE_REASON_DELEGATE_CLOSED));
+ // Still should only report one call of sendMessage from before
+ verify(mISipDelegate).sendMessage(TEST_MESSAGE, 1 /*version*/);
+ }
+
+ @SmallTest
+ @Test
+ public void testDelegateConnectionNotifyMessageReceived() throws Exception {
+ MessageTransportStateTracker tracker = new MessageTransportStateTracker(TEST_SUB_ID,
+ Runnable::run, mDelegateMessageCallback);
+ tracker.openTransport(mISipDelegate, Collections.emptySet());
+ tracker.getDelegateConnection().notifyMessageReceived("z9hG4bK776asdhds");
+ verify(mISipDelegate).notifyMessageReceived("z9hG4bK776asdhds");
+ }
+
+ @SmallTest
+ @Test
+ public void testDelegateConnectionNotifyMessageReceiveError() throws Exception {
+ MessageTransportStateTracker tracker = new MessageTransportStateTracker(TEST_SUB_ID,
+ Runnable::run, mDelegateMessageCallback);
+ tracker.openTransport(mISipDelegate, Collections.emptySet());
+ tracker.getDelegateConnection().notifyMessageReceiveError("z9hG4bK776asdhds",
+ SipDelegateManager.MESSAGE_FAILURE_REASON_NETWORK_NOT_AVAILABLE);
+ verify(mISipDelegate).notifyMessageReceiveError("z9hG4bK776asdhds",
+ SipDelegateManager.MESSAGE_FAILURE_REASON_NETWORK_NOT_AVAILABLE);
+ }
+
+ @SmallTest
+ @Test
+ public void testDelegateConnectionCloseDialog() throws Exception {
+ MessageTransportStateTracker tracker = new MessageTransportStateTracker(TEST_SUB_ID,
+ Runnable::run, mDelegateMessageCallback);
+ tracker.openTransport(mISipDelegate, Collections.emptySet());
+ tracker.getDelegateConnection().closeDialog("testCallId");
+ verify(mISipDelegate).closeDialog("testCallId");
+ }
+
+ @SmallTest
+ @Test
+ public void testDelegateOnMessageReceived() throws Exception {
+ MessageTransportStateTracker tracker = new MessageTransportStateTracker(TEST_SUB_ID,
+ Runnable::run, mDelegateMessageCallback);
+ tracker.openTransport(mISipDelegate, Collections.emptySet());
+
+ tracker.getMessageCallback().onMessageReceived(TEST_MESSAGE);
+ verify(mDelegateMessageCallback).onMessageReceived(TEST_MESSAGE);
+
+ doThrow(new RemoteException()).when(mDelegateMessageCallback).onMessageReceived(any());
+ tracker.getMessageCallback().onMessageReceived(TEST_MESSAGE);
+ verify(mISipDelegate).notifyMessageReceiveError(any(),
+ eq(SipDelegateManager.MESSAGE_FAILURE_REASON_DELEGATE_DEAD));
+ }
+
+ @SmallTest
+ @Test
+ public void testDelegateOnMessageReceivedClosedGracefully() throws Exception {
+ PendingExecutor executor = new PendingExecutor();
+ MessageTransportStateTracker tracker = new MessageTransportStateTracker(TEST_SUB_ID,
+ executor, mDelegateMessageCallback);
+ tracker.openTransport(mISipDelegate, Collections.emptySet());
+
+ tracker.getMessageCallback().onMessageReceived(TEST_MESSAGE);
+ executor.executePending();
+ verify(mDelegateMessageCallback).onMessageReceived(TEST_MESSAGE);
+
+ tracker.getMessageCallback().onMessageReceived(TEST_MESSAGE);
+ tracker.closeGracefully(
+ SipDelegateManager.MESSAGE_FAILURE_REASON_INTERNAL_DELEGATE_STATE_TRANSITION,
+ SipDelegateManager.MESSAGE_FAILURE_REASON_DELEGATE_CLOSED,
+ mMockCloseConsumer);
+ executor.executePending();
+ // Incoming SIP message should not be blocked by closeGracefully
+ verify(mDelegateMessageCallback, times(2)).onMessageReceived(TEST_MESSAGE);
+ }
+
+ @SmallTest
+ @Test
+ public void testDelegateOnMessageSent() throws Exception {
+ MessageTransportStateTracker tracker = new MessageTransportStateTracker(TEST_SUB_ID,
+ Runnable::run, mDelegateMessageCallback);
+ tracker.openTransport(mISipDelegate, Collections.emptySet());
+ tracker.getMessageCallback().onMessageSent("z9hG4bK776asdhds");
+ verify(mDelegateMessageCallback).onMessageSent("z9hG4bK776asdhds");
+ }
+
+ @SmallTest
+ @Test
+ public void testDelegateonMessageSendFailure() throws Exception {
+ MessageTransportStateTracker tracker = new MessageTransportStateTracker(TEST_SUB_ID,
+ Runnable::run, mDelegateMessageCallback);
+ tracker.openTransport(mISipDelegate, Collections.emptySet());
+ tracker.getMessageCallback().onMessageSendFailure("z9hG4bK776asdhds",
+ SipDelegateManager.MESSAGE_FAILURE_REASON_NETWORK_NOT_AVAILABLE);
+ verify(mDelegateMessageCallback).onMessageSendFailure("z9hG4bK776asdhds",
+ SipDelegateManager.MESSAGE_FAILURE_REASON_NETWORK_NOT_AVAILABLE);
+ }
+}
diff --git a/tests/src/com/android/services/telephony/rcs/SipDelegateBinderConnectionTest.java b/tests/src/com/android/services/telephony/rcs/SipDelegateBinderConnectionTest.java
new file mode 100644
index 0000000..b95ae90
--- /dev/null
+++ b/tests/src/com/android/services/telephony/rcs/SipDelegateBinderConnectionTest.java
@@ -0,0 +1,226 @@
+/*
+ * Copyright (C) 2020 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.services.telephony.rcs;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.verify;
+
+import android.os.RemoteException;
+import android.telephony.ims.DelegateRegistrationState;
+import android.telephony.ims.DelegateRequest;
+import android.telephony.ims.FeatureTagState;
+import android.telephony.ims.SipDelegateImsConfiguration;
+import android.telephony.ims.SipDelegateManager;
+import android.telephony.ims.aidl.ISipDelegate;
+import android.telephony.ims.aidl.ISipDelegateMessageCallback;
+import android.telephony.ims.aidl.ISipDelegateStateCallback;
+import android.telephony.ims.aidl.ISipTransport;
+import android.util.ArraySet;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.TelephonyTestBase;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+
+import java.util.ArrayList;
+import java.util.Set;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+
+@RunWith(AndroidJUnit4.class)
+public class SipDelegateBinderConnectionTest extends TelephonyTestBase {
+ private static final int TEST_SUB_ID = 1;
+
+ @Mock private ISipDelegate mMockDelegate;
+ @Mock private ISipTransport mMockTransport;
+ @Mock private ISipDelegateMessageCallback mMessageCallback;
+ @Mock private DelegateBinderStateManager.StateCallback mMockStateCallback;
+ @Mock private BiConsumer<ISipDelegate, Set<FeatureTagState>> mMockCreatedCallback;
+ @Mock private Consumer<Integer> mMockDestroyedCallback;
+
+ private ArrayList<SipDelegateBinderConnection.StateCallback> mStateCallbackList;
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ mStateCallbackList = new ArrayList<>(1);
+ mStateCallbackList.add(mMockStateCallback);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ @SmallTest
+ @Test
+ public void testBaseImpl() throws Exception {
+ DelegateBinderStateManager baseConnection = new SipDelegateBinderConnectionStub(
+ getMmTelDeniedTag(), Runnable::run, mStateCallbackList);
+
+ baseConnection.create(null /*message cb*/, mMockCreatedCallback);
+ // Verify the stub simulates onCreated + on registration state callback.
+ verify(mMockCreatedCallback).accept(any(), eq(getMmTelDeniedTag()));
+ verify(mMockStateCallback).onRegistrationStateChanged(
+ new DelegateRegistrationState.Builder().build());
+
+ // Verify onDestroyed is called correctly.
+ baseConnection.destroy(SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_REQUESTED_BY_APP,
+ mMockDestroyedCallback);
+ verify(mMockDestroyedCallback).accept(
+ SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_REQUESTED_BY_APP);
+ }
+
+ @SmallTest
+ @Test
+ public void testCreateConnection() throws Exception {
+ DelegateRequest request = getDelegateRequest();
+ ArraySet<FeatureTagState> deniedTags = getMmTelDeniedTag();
+ SipDelegateBinderConnection connection = new SipDelegateBinderConnection(TEST_SUB_ID,
+ mMockTransport, request, deniedTags, Runnable::run, mStateCallbackList);
+ ISipDelegateStateCallback cb = createDelegateCaptureStateCallback(request, connection);
+
+ // Send onCreated callback from SipDelegate
+ ArrayList<FeatureTagState> delegateDeniedTags = new ArrayList<>(1);
+ delegateDeniedTags.add(new FeatureTagState(ImsSignallingUtils.GROUP_CHAT_TAG,
+ SipDelegateManager.DENIED_REASON_IN_USE_BY_ANOTHER_DELEGATE));
+ assertNotNull(cb);
+ cb.onCreated(mMockDelegate, delegateDeniedTags);
+
+ ArraySet<FeatureTagState> totalDeniedTags = new ArraySet<>(deniedTags);
+ // Add the tags denied by the controller as well.
+ totalDeniedTags.addAll(delegateDeniedTags);
+ // The callback should contain the controller and delegate denied tags in the callback.
+ verify(mMockCreatedCallback).accept(mMockDelegate, totalDeniedTags);
+ }
+
+ @SmallTest
+ @Test
+ public void testCreateConnectionServiceDead() throws Exception {
+ DelegateRequest request = getDelegateRequest();
+ ArraySet<FeatureTagState> deniedTags = getMmTelDeniedTag();
+ SipDelegateBinderConnection connection = new SipDelegateBinderConnection(TEST_SUB_ID,
+ mMockTransport, request, deniedTags, Runnable::run, mStateCallbackList);
+ doThrow(new RemoteException()).when(mMockTransport).createSipDelegate(eq(TEST_SUB_ID),
+ any(), any(), any());
+ ISipDelegateStateCallback cb = createDelegateCaptureStateCallback(request, connection);
+ assertNull(cb);
+ }
+
+ @SmallTest
+ @Test
+ public void testDestroyConnection() throws Exception {
+ DelegateRequest request = getDelegateRequest();
+ ArraySet<FeatureTagState> deniedTags = getMmTelDeniedTag();
+ SipDelegateBinderConnection connection = new SipDelegateBinderConnection(TEST_SUB_ID,
+ mMockTransport, request, deniedTags, Runnable::run, mStateCallbackList);
+ ISipDelegateStateCallback cb = createDelegateCaptureStateCallback(request, connection);
+ assertNotNull(cb);
+ cb.onCreated(mMockDelegate, null /*denied*/);
+ verify(mMockCreatedCallback).accept(mMockDelegate, deniedTags);
+
+ // call Destroy on the SipDelegate
+ destroy(connection, SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_REQUESTED_BY_APP);
+ cb.onDestroyed(SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_REQUESTED_BY_APP);
+ verify(mMockDestroyedCallback).accept(
+ SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_REQUESTED_BY_APP);
+ }
+
+ @SmallTest
+ @Test
+ public void testDestroyConnectionDead() throws Exception {
+ DelegateRequest request = getDelegateRequest();
+ ArraySet<FeatureTagState> deniedTags = getMmTelDeniedTag();
+ SipDelegateBinderConnection connection = new SipDelegateBinderConnection(TEST_SUB_ID,
+ mMockTransport, request, deniedTags, Runnable::run, mStateCallbackList);
+ ISipDelegateStateCallback cb = createDelegateCaptureStateCallback(request, connection);
+ assertNotNull(cb);
+ cb.onCreated(mMockDelegate, null /*denied*/);
+ verify(mMockCreatedCallback).accept(mMockDelegate, deniedTags);
+
+ // try to destroy when dead and ensure callback is still called.
+ doThrow(new RemoteException()).when(mMockTransport).destroySipDelegate(any(), anyInt());
+ destroy(connection, SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_REQUESTED_BY_APP);
+ verify(mMockDestroyedCallback).accept(
+ SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_REQUESTED_BY_APP);
+ }
+
+ @SmallTest
+ @Test
+ public void testStateCallback() throws Exception {
+ DelegateRequest request = getDelegateRequest();
+ ArraySet<FeatureTagState> deniedTags = getMmTelDeniedTag();
+ SipDelegateBinderConnection connection = new SipDelegateBinderConnection(TEST_SUB_ID,
+ mMockTransport, request, deniedTags, Runnable::run, mStateCallbackList);
+ ISipDelegateStateCallback cb = createDelegateCaptureStateCallback(request, connection);
+ assertNotNull(cb);
+ cb.onCreated(mMockDelegate, new ArrayList<>(deniedTags));
+ verify(mMockCreatedCallback).accept(mMockDelegate, deniedTags);
+
+ SipDelegateImsConfiguration config = new SipDelegateImsConfiguration.Builder(1).build();
+ cb.onImsConfigurationChanged(config);
+ verify(mMockStateCallback).onImsConfigurationChanged(config);
+
+ DelegateRegistrationState regState = new DelegateRegistrationState.Builder()
+ .addRegisteredFeatureTags(request.getFeatureTags()).build();
+ cb.onFeatureTagRegistrationChanged(regState);
+ verify(mMockStateCallback).onRegistrationStateChanged(regState);
+ }
+
+ private ISipDelegateStateCallback createDelegateCaptureStateCallback(
+ DelegateRequest r, SipDelegateBinderConnection c) throws Exception {
+ boolean isCreating = c.create(mMessageCallback, mMockCreatedCallback);
+ if (!isCreating) return null;
+ ArgumentCaptor<ISipDelegateStateCallback> stateCaptor =
+ ArgumentCaptor.forClass(ISipDelegateStateCallback.class);
+ verify(mMockTransport).createSipDelegate(eq(TEST_SUB_ID), eq(r), stateCaptor.capture(),
+ eq(mMessageCallback));
+ assertNotNull(stateCaptor.getValue());
+ return stateCaptor.getValue();
+ }
+
+ private void destroy(SipDelegateBinderConnection c, int reason) throws Exception {
+ c.destroy(reason, mMockDestroyedCallback);
+ verify(mMockTransport).destroySipDelegate(mMockDelegate, reason);
+ }
+
+ private DelegateRequest getDelegateRequest() {
+ ArraySet<String> featureTags = new ArraySet<>(2);
+ featureTags.add(ImsSignallingUtils.ONE_TO_ONE_CHAT_TAG);
+ featureTags.add(ImsSignallingUtils.GROUP_CHAT_TAG);
+ return new DelegateRequest(featureTags);
+ }
+
+ private ArraySet<FeatureTagState> getMmTelDeniedTag() {
+ ArraySet<FeatureTagState> deniedTags = new ArraySet<>();
+ deniedTags.add(new FeatureTagState(ImsSignallingUtils.MMTEL_TAG,
+ SipDelegateManager.DENIED_REASON_NOT_ALLOWED));
+ return deniedTags;
+ }
+}
diff --git a/tests/src/com/android/services/telephony/rcs/SipDelegateControllerTest.java b/tests/src/com/android/services/telephony/rcs/SipDelegateControllerTest.java
new file mode 100644
index 0000000..47b4808
--- /dev/null
+++ b/tests/src/com/android/services/telephony/rcs/SipDelegateControllerTest.java
@@ -0,0 +1,270 @@
+/*
+ * Copyright (C) 2020 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.services.telephony.rcs;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.telephony.ims.DelegateRegistrationState;
+import android.telephony.ims.DelegateRequest;
+import android.telephony.ims.FeatureTagState;
+import android.telephony.ims.SipDelegateManager;
+import android.telephony.ims.aidl.ISipDelegate;
+import android.telephony.ims.aidl.ISipDelegateMessageCallback;
+import android.telephony.ims.aidl.ISipTransport;
+import android.util.ArraySet;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.TelephonyTestBase;
+import com.android.TestExecutorService;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+
+import java.util.Collections;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+
+@RunWith(AndroidJUnit4.class)
+public class SipDelegateControllerTest extends TelephonyTestBase {
+ private static final int TEST_SUB_ID = 1;
+
+ @Mock private ISipDelegate mMockSipDelegate;
+ @Mock private ISipTransport mMockSipTransport;
+ @Mock private MessageTransportStateTracker mMockMessageTracker;
+ @Mock private ISipDelegateMessageCallback mMockMessageCallback;
+ @Mock private DelegateStateTracker mMockDelegateStateTracker;
+ @Mock private DelegateBinderStateManager mMockBinderConnection;
+ @Captor private ArgumentCaptor<BiConsumer<ISipDelegate, Set<FeatureTagState>>> mCreatedCaptor;
+ @Captor private ArgumentCaptor<Consumer<Boolean>> mBooleanConsumerCaptor;
+ @Captor private ArgumentCaptor<Consumer<Integer>> mIntegerConsumerCaptor;
+
+ private ScheduledExecutorService mExecutorService;
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ when(mMockMessageTracker.getMessageCallback()).thenReturn(mMockMessageCallback);
+ mExecutorService = new TestExecutorService();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ mExecutorService.shutdownNow();
+ super.tearDown();
+ }
+
+ @SmallTest
+ @Test
+ public void testCreateDelegate() throws Exception {
+ DelegateRequest request = getBaseDelegateRequest();
+ SipDelegateController controller = getTestDelegateController(request,
+ Collections.emptySet());
+
+ doReturn(true).when(mMockBinderConnection).create(eq(mMockMessageCallback), any());
+ CompletableFuture<Boolean> future = controller.create(request.getFeatureTags(),
+ Collections.emptySet() /*denied tags*/);
+ BiConsumer<ISipDelegate, Set<FeatureTagState>> consumer =
+ verifyConnectionCreated(1);
+ assertNotNull(consumer);
+
+ assertFalse(future.isDone());
+ consumer.accept(mMockSipDelegate, Collections.emptySet());
+ assertTrue(future.get());
+ verify(mMockMessageTracker).openTransport(mMockSipDelegate, Collections.emptySet());
+ verify(mMockDelegateStateTracker).sipDelegateConnected(Collections.emptySet());
+ }
+
+ @SmallTest
+ @Test
+ public void testCreateDelegateTransportDied() throws Exception {
+ DelegateRequest request = getBaseDelegateRequest();
+ SipDelegateController controller = getTestDelegateController(request,
+ Collections.emptySet());
+
+ //Create operation fails
+ doReturn(false).when(mMockBinderConnection).create(eq(mMockMessageCallback), any());
+ CompletableFuture<Boolean> future = controller.create(request.getFeatureTags(),
+ Collections.emptySet() /*denied tags*/);
+
+ assertFalse(future.get());
+ }
+
+ @SmallTest
+ @Test
+ public void testDestroyDelegate() throws Exception {
+ DelegateRequest request = getBaseDelegateRequest();
+ SipDelegateController controller = getTestDelegateController(request,
+ Collections.emptySet());
+ createSipDelegate(request, controller);
+
+ CompletableFuture<Integer> pendingDestroy = controller.destroy(false /*force*/,
+ SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_REQUESTED_BY_APP);
+ assertFalse(pendingDestroy.isDone());
+ Consumer<Boolean> pendingClosedConsumer = verifyMessageTrackerCloseGracefully();
+ verify(mMockDelegateStateTracker).sipDelegateChanging(
+ DelegateRegistrationState.DEREGISTERING_REASON_DESTROY_PENDING);
+
+ // verify we do not call destroy on the delegate until the message tracker releases the
+ // transport.
+ verify(mMockBinderConnection, never()).destroy(anyInt(), any());
+ pendingClosedConsumer.accept(true);
+ Consumer<Integer> pendingDestroyedConsumer = verifyBinderConnectionDestroy();
+ pendingDestroyedConsumer.accept(
+ SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_REQUESTED_BY_APP);
+ verify(mMockDelegateStateTracker).sipDelegateDestroyed(
+ SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_REQUESTED_BY_APP);
+ assertTrue(pendingDestroy.isDone());
+ assertEquals(SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_REQUESTED_BY_APP,
+ pendingDestroy.get().intValue());
+ }
+
+ @SmallTest
+ @Test
+ public void testDestroyDelegateForce() throws Exception {
+ DelegateRequest request = getBaseDelegateRequest();
+ SipDelegateController controller = getTestDelegateController(request,
+ Collections.emptySet());
+ createSipDelegate(request, controller);
+
+ CompletableFuture<Integer> pendingDestroy = controller.destroy(true /*force*/,
+ SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_REQUESTED_BY_APP);
+ assertFalse(pendingDestroy.isDone());
+ // Do not wait for message transport close in this case.
+ verify(mMockMessageTracker).close(
+ SipDelegateManager.MESSAGE_FAILURE_REASON_DELEGATE_CLOSED);
+ verify(mMockDelegateStateTracker, never()).sipDelegateChanging(
+ DelegateRegistrationState.DEREGISTERING_REASON_DESTROY_PENDING);
+
+ //verify destroy is called
+ Consumer<Integer> pendingDestroyedConsumer = verifyBinderConnectionDestroy();
+ pendingDestroyedConsumer.accept(
+ SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_REQUESTED_BY_APP);
+ verify(mMockDelegateStateTracker).sipDelegateDestroyed(
+ SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_REQUESTED_BY_APP);
+ assertTrue(pendingDestroy.isDone());
+ assertEquals(SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_REQUESTED_BY_APP,
+ pendingDestroy.get().intValue());
+ }
+
+ @SmallTest
+ @Test
+ public void testChangeSupportedFeatures() throws Exception {
+ DelegateRequest request = getBaseDelegateRequest();
+ SipDelegateController controller = getTestDelegateController(request,
+ Collections.emptySet());
+ createSipDelegate(request, controller);
+
+ Set<String> newFts = getBaseFTSet();
+ newFts.add(ImsSignallingUtils.GROUP_CHAT_TAG);
+ CompletableFuture<Boolean> pendingChange = controller.changeSupportedFeatureTags(
+ newFts, Collections.emptySet());
+ assertFalse(pendingChange.isDone());
+ // message tracker should close gracefully.
+ Consumer<Boolean> pendingClosedConsumer = verifyMessageTrackerCloseGracefully();
+ verify(mMockDelegateStateTracker).sipDelegateChanging(
+ DelegateRegistrationState.DEREGISTERING_REASON_FEATURE_TAGS_CHANGING);
+ verify(mMockBinderConnection, never()).destroy(anyInt(), any());
+ pendingClosedConsumer.accept(true);
+ Consumer<Integer> pendingDestroyedConsumer = verifyBinderConnectionDestroy();
+ pendingDestroyedConsumer.accept(
+ SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_REQUESTED_BY_APP);
+ verify(mMockDelegateStateTracker, never()).sipDelegateDestroyed(anyInt());
+
+ // This will cause any exceptions to be printed if something completed exceptionally.
+ assertNull(pendingChange.getNow(null));
+ BiConsumer<ISipDelegate, Set<FeatureTagState>> consumer =
+ verifyConnectionCreated(2);
+ assertNotNull(consumer);
+ consumer.accept(mMockSipDelegate, Collections.emptySet());
+ assertTrue(pendingChange.get());
+
+ verify(mMockMessageTracker, times(2)).openTransport(mMockSipDelegate,
+ Collections.emptySet());
+ verify(mMockDelegateStateTracker, times(2)).sipDelegateConnected(Collections.emptySet());
+ }
+
+ private void createSipDelegate(DelegateRequest request, SipDelegateController controller)
+ throws Exception {
+ doReturn(true).when(mMockBinderConnection).create(eq(mMockMessageCallback), any());
+ CompletableFuture<Boolean> future = controller.create(request.getFeatureTags(),
+ Collections.emptySet() /*denied tags*/);
+ BiConsumer<ISipDelegate, Set<FeatureTagState>> consumer =
+ verifyConnectionCreated(1);
+ assertNotNull(consumer);
+ consumer.accept(mMockSipDelegate, Collections.emptySet());
+ assertTrue(future.get());
+ }
+
+ private ArraySet<String> getBaseFTSet() {
+ ArraySet<String> request = new ArraySet<>();
+ request.add(ImsSignallingUtils.ONE_TO_ONE_CHAT_TAG);
+ return request;
+ }
+
+ private DelegateRequest getBaseDelegateRequest() {
+ return new DelegateRequest(getBaseFTSet());
+ }
+
+ private SipDelegateController getTestDelegateController(DelegateRequest request,
+ Set<FeatureTagState> deniedSet) {
+ return new SipDelegateController(TEST_SUB_ID, request, "", mMockSipTransport,
+ mExecutorService, mMockMessageTracker, mMockDelegateStateTracker,
+ (a, b, c, deniedFeatureSet, e, f) -> {
+ assertEquals(deniedSet, deniedFeatureSet);
+ return mMockBinderConnection;
+ });
+ }
+
+ private BiConsumer<ISipDelegate, Set<FeatureTagState>> verifyConnectionCreated(int numTimes) {
+ verify(mMockBinderConnection, times(numTimes)).create(eq(mMockMessageCallback),
+ mCreatedCaptor.capture());
+ return mCreatedCaptor.getValue();
+ }
+
+ private Consumer<Boolean> verifyMessageTrackerCloseGracefully() {
+ verify(mMockMessageTracker).closeGracefully(anyInt(), anyInt(),
+ mBooleanConsumerCaptor.capture());
+ return mBooleanConsumerCaptor.getValue();
+ }
+ private Consumer<Integer> verifyBinderConnectionDestroy() {
+ verify(mMockBinderConnection).destroy(anyInt(), mIntegerConsumerCaptor.capture());
+ return mIntegerConsumerCaptor.getValue();
+ }
+
+}
diff --git a/tests/src/com/android/services/telephony/rcs/SipTransportControllerTest.java b/tests/src/com/android/services/telephony/rcs/SipTransportControllerTest.java
index 65a95cd..8e10757 100644
--- a/tests/src/com/android/services/telephony/rcs/SipTransportControllerTest.java
+++ b/tests/src/com/android/services/telephony/rcs/SipTransportControllerTest.java
@@ -18,13 +18,34 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
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.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
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.verify;
+import android.app.role.RoleManager;
+import android.os.IBinder;
+import android.os.UserHandle;
+import android.telephony.ims.DelegateRequest;
+import android.telephony.ims.FeatureTagState;
import android.telephony.ims.ImsException;
+import android.telephony.ims.SipDelegateManager;
+import android.telephony.ims.aidl.ISipDelegate;
+import android.telephony.ims.aidl.ISipDelegateConnectionStateCallback;
+import android.telephony.ims.aidl.ISipDelegateMessageCallback;
import android.telephony.ims.aidl.ISipTransport;
+import android.util.ArraySet;
+import android.util.Pair;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
@@ -39,30 +60,89 @@
import org.junit.runner.RunWith;
import org.mockito.Mock;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
@RunWith(AndroidJUnit4.class)
public class SipTransportControllerTest extends TelephonyTestBase {
+ private static final int TEST_SUB_ID = 1;
+ private static final String TEST_PACKAGE_NAME = "com.test_pkg";
+ private static final String TEST_PACKAGE_NAME_2 = "com.test_pkg2";
+ private static final int TIMEOUT_MS = 200;
+ private static final int THROTTLE_MS = 50;
+
+ private class SipDelegateControllerContainer {
+ public final int subId;
+ public final String packageName;
+ public final DelegateRequest delegateRequest;
+ public final SipDelegateController delegateController;
+ public final ISipDelegate mMockDelegate;
+ public final IBinder mMockDelegateBinder;
+
+ SipDelegateControllerContainer(int id, String name, DelegateRequest request) {
+ delegateController = mock(SipDelegateController.class);
+ mMockDelegate = mock(ISipDelegate.class);
+ mMockDelegateBinder = mock(IBinder.class);
+ doReturn(mMockDelegateBinder).when(mMockDelegate).asBinder();
+ doReturn(name).when(delegateController).getPackageName();
+ doReturn(request).when(delegateController).getInitialRequest();
+ doReturn(mMockDelegate).when(delegateController).getSipDelegateInterface();
+ subId = id;
+ packageName = name;
+ delegateRequest = request;
+ }
+ }
@Mock private RcsFeatureManager mRcsManager;
@Mock private ISipTransport mSipTransport;
+ @Mock private ISipDelegateConnectionStateCallback mMockStateCallback;
+ @Mock private ISipDelegateMessageCallback mMockMessageCallback;
+ @Mock private SipTransportController.SipDelegateControllerFactory
+ mMockDelegateControllerFactory;
+ @Mock private SipTransportController.RoleManagerAdapter mMockRoleManager;
- private final TestExecutorService mExecutorService = new TestExecutorService();
+ private ScheduledExecutorService mExecutorService = null;
+ private final ArrayList<SipDelegateControllerContainer> mMockControllers = new ArrayList<>();
+ private final ArrayList<String> mSmsPackageName = new ArrayList<>(1);
@Before
public void setUp() throws Exception {
super.setUp();
+ doReturn(mSmsPackageName).when(mMockRoleManager).getRoleHolders(RoleManager.ROLE_SMS);
+ mSmsPackageName.add(TEST_PACKAGE_NAME);
+ doAnswer(invocation -> {
+ Integer subId = invocation.getArgument(0);
+ String packageName = invocation.getArgument(2);
+ DelegateRequest request = invocation.getArgument(1);
+ SipDelegateController c = getMockDelegateController(subId, packageName, request);
+ assertNotNull("create called with no corresponding controller set up", c);
+ return c;
+ }).when(mMockDelegateControllerFactory).create(anyInt(), any(), anyString(), any(),
+ any(), any(), any());
}
@After
public void tearDown() throws Exception {
super.tearDown();
+ boolean isShutdown = mExecutorService == null || mExecutorService.isShutdown();
+ if (!isShutdown) {
+ mExecutorService.shutdownNow();
+ }
}
@SmallTest
@Test
public void isSupportedRcsNotConnected() {
- SipTransportController controller = createController(0 /*slotId*/, 0 /*subId*/);
+ SipTransportController controller = createController(new TestExecutorService());
try {
- controller.isSupported(0 /*subId*/);
+ controller.isSupported(TEST_SUB_ID);
fail();
} catch (ImsException e) {
assertEquals(ImsException.CODE_ERROR_SERVICE_UNAVAILABLE, e.getCode());
@@ -72,9 +152,9 @@
@SmallTest
@Test
public void isSupportedInvalidSubId() {
- SipTransportController controller = createController(0 /*slotId*/, 0 /*subId*/);
+ SipTransportController controller = createController(new TestExecutorService());
try {
- controller.isSupported(1 /*subId*/);
+ controller.isSupported(TEST_SUB_ID + 1);
fail();
} catch (ImsException e) {
assertEquals(ImsException.CODE_ERROR_INVALID_SUBSCRIPTION, e.getCode());
@@ -84,10 +164,10 @@
@SmallTest
@Test
public void isSupportedSubIdChanged() {
- SipTransportController controller = createController(0 /*slotId*/, 0 /*subId*/);
- controller.onAssociatedSubscriptionUpdated(1 /*subId*/);
+ SipTransportController controller = createController(new TestExecutorService());
+ controller.onAssociatedSubscriptionUpdated(TEST_SUB_ID + 1);
try {
- controller.isSupported(0 /*subId*/);
+ controller.isSupported(TEST_SUB_ID);
fail();
} catch (ImsException e) {
assertEquals(ImsException.CODE_ERROR_INVALID_SUBSCRIPTION, e.getCode());
@@ -97,11 +177,11 @@
@SmallTest
@Test
public void isSupportedSipTransportAvailableRcsConnected() throws Exception {
- SipTransportController controller = createController(0 /*slotId*/, 0 /*subId*/);
+ SipTransportController controller = createController(new TestExecutorService());
doReturn(mSipTransport).when(mRcsManager).getSipTransport();
controller.onRcsConnected(mRcsManager);
try {
- assertTrue(controller.isSupported(0 /*subId*/));
+ assertTrue(controller.isSupported(TEST_SUB_ID));
} catch (ImsException e) {
fail();
}
@@ -110,12 +190,12 @@
@SmallTest
@Test
public void isSupportedSipTransportNotAvailableRcsDisconnected() throws Exception {
- SipTransportController controller = createController(0 /*slotId*/, 0 /*subId*/);
+ SipTransportController controller = createController(new TestExecutorService());
doReturn(mSipTransport).when(mRcsManager).getSipTransport();
controller.onRcsConnected(mRcsManager);
controller.onRcsDisconnected();
try {
- controller.isSupported(0 /*subId*/);
+ controller.isSupported(TEST_SUB_ID);
fail();
} catch (ImsException e) {
assertEquals(ImsException.CODE_ERROR_SERVICE_UNAVAILABLE, e.getCode());
@@ -125,11 +205,11 @@
@SmallTest
@Test
public void isSupportedSipTransportNotAvailableRcsConnected() throws Exception {
- SipTransportController controller = createController(0 /*slotId*/, 0 /*subId*/);
+ SipTransportController controller = createController(new TestExecutorService());
doReturn(null).when(mRcsManager).getSipTransport();
controller.onRcsConnected(mRcsManager);
try {
- assertFalse(controller.isSupported(0 /*subId*/));
+ assertFalse(controller.isSupported(TEST_SUB_ID));
} catch (ImsException e) {
fail();
}
@@ -138,19 +218,627 @@
@SmallTest
@Test
public void isSupportedImsServiceNotAvailableRcsConnected() throws Exception {
- SipTransportController controller = createController(0 /*slotId*/, 0 /*subId*/);
+ SipTransportController controller = createController(new TestExecutorService());
doThrow(new ImsException("", ImsException.CODE_ERROR_SERVICE_UNAVAILABLE))
.when(mRcsManager).getSipTransport();
controller.onRcsConnected(mRcsManager);
try {
- controller.isSupported(0 /*subId*/);
+ controller.isSupported(TEST_SUB_ID);
fail();
} catch (ImsException e) {
assertEquals(ImsException.CODE_ERROR_SERVICE_UNAVAILABLE, e.getCode());
}
}
- private SipTransportController createController(int slotId, int subId) {
- return new SipTransportController(mContext, slotId, subId, mExecutorService);
+ @SmallTest
+ @Test
+ public void createImsServiceAvailableSubIdIncorrect() throws Exception {
+ SipTransportController controller = createController(new TestExecutorService());
+ doReturn(mSipTransport).when(mRcsManager).getSipTransport();
+ controller.onRcsConnected(mRcsManager);
+ try {
+ controller.createSipDelegate(TEST_SUB_ID + 1,
+ new DelegateRequest(Collections.emptySet()), TEST_PACKAGE_NAME,
+ mMockStateCallback, mMockMessageCallback);
+ fail();
+ } catch (ImsException e) {
+ assertEquals(ImsException.CODE_ERROR_INVALID_SUBSCRIPTION, e.getCode());
+ }
+ }
+
+ @SmallTest
+ @Test
+ public void createImsServiceDoesntSupportTransport() throws Exception {
+ SipTransportController controller = createController(new TestExecutorService());
+ doReturn(null).when(mRcsManager).getSipTransport();
+ controller.onRcsConnected(mRcsManager);
+ try {
+ controller.createSipDelegate(TEST_SUB_ID,
+ new DelegateRequest(Collections.emptySet()), TEST_PACKAGE_NAME,
+ mMockStateCallback, mMockMessageCallback);
+ fail();
+ } catch (ImsException e) {
+ assertEquals(ImsException.CODE_ERROR_UNSUPPORTED_OPERATION, e.getCode());
+ }
+ }
+
+ @SmallTest
+ @Test
+ public void createImsServiceNotAvailable() throws Exception {
+ SipTransportController controller = createController(new TestExecutorService());
+ doThrow(new ImsException("", ImsException.CODE_ERROR_SERVICE_UNAVAILABLE))
+ .when(mRcsManager).getSipTransport();
+ // No RCS connected message
+ try {
+ controller.createSipDelegate(TEST_SUB_ID,
+ new DelegateRequest(Collections.emptySet()), TEST_PACKAGE_NAME,
+ mMockStateCallback, mMockMessageCallback);
+ fail();
+ } catch (ImsException e) {
+ assertEquals(ImsException.CODE_ERROR_SERVICE_UNAVAILABLE, e.getCode());
+ }
+ }
+
+ @SmallTest
+ @Test
+ public void basicCreate() throws Exception {
+ SipTransportController controller = setupLiveTransportController();
+
+ DelegateRequest r = getBaseDelegateRequest();
+
+ SipDelegateController c = injectMockDelegateController(TEST_PACKAGE_NAME, r);
+ createDelegateAndVerify(controller, c, r, r.getFeatureTags(), Collections.emptySet(),
+ TEST_PACKAGE_NAME);
+ }
+
+ @SmallTest
+ @Test
+ public void basicCreateDestroy() throws Exception {
+ SipTransportController controller = setupLiveTransportController();
+
+ DelegateRequest r = getBaseDelegateRequest();
+ SipDelegateController c = injectMockDelegateController(TEST_PACKAGE_NAME, r);
+ createDelegateAndVerify(controller, c, r, r.getFeatureTags(), Collections.emptySet(),
+ TEST_PACKAGE_NAME);
+
+ destroyDelegateAndVerify(controller, c, false,
+ SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_REQUESTED_BY_APP);
+ }
+
+ @SmallTest
+ @Test
+ public void testCreateButNotInRole() throws Exception {
+ SipTransportController controller = setupLiveTransportController();
+
+ DelegateRequest r = getBaseDelegateRequest();
+ Set<FeatureTagState> getDeniedTags = getDeniedTagsForReason(r.getFeatureTags(),
+ SipDelegateManager.DENIED_REASON_NOT_ALLOWED);
+
+ // Try to create a SipDelegate for a package that is not the default sms role.
+ SipDelegateController c = injectMockDelegateController(TEST_PACKAGE_NAME_2, r);
+ createDelegateAndVerify(controller, c, r, Collections.emptySet(), getDeniedTags,
+ TEST_PACKAGE_NAME_2);
+ }
+
+ @SmallTest
+ @Test
+ public void createTwoAndDenyOverlappingTags() throws Exception {
+ SipTransportController controller = setupLiveTransportController();
+
+ // First delegate requests RCS message + File transfer
+ ArraySet<String> firstDelegate = new ArraySet<>(getBaseDelegateRequest().getFeatureTags());
+ firstDelegate.remove(ImsSignallingUtils.GROUP_CHAT_TAG);
+ DelegateRequest firstDelegateRequest = new DelegateRequest(firstDelegate);
+ SipDelegateController c1 = injectMockDelegateController(TEST_PACKAGE_NAME,
+ firstDelegateRequest);
+ createDelegateAndVerify(controller, c1, firstDelegateRequest, firstDelegate,
+ Collections.emptySet(), TEST_PACKAGE_NAME);
+
+ // First delegate requests RCS message + Group RCS message. For this delegate, single RCS
+ // message should be denied.
+ ArraySet<String> secondDelegate = new ArraySet<>(getBaseDelegateRequest().getFeatureTags());
+ secondDelegate.remove(ImsSignallingUtils.FILE_TRANSFER_HTTP_TAG);
+ DelegateRequest secondDelegateRequest = new DelegateRequest(secondDelegate);
+ Pair<Set<String>, Set<FeatureTagState>> grantedAndDenied = getAllowedAndDeniedTagsForConfig(
+ secondDelegateRequest, SipDelegateManager.DENIED_REASON_IN_USE_BY_ANOTHER_DELEGATE,
+ firstDelegate);
+ SipDelegateController c2 = injectMockDelegateController(TEST_PACKAGE_NAME,
+ secondDelegateRequest);
+ createDelegateAndVerify(controller, c2, secondDelegateRequest, grantedAndDenied.first,
+ grantedAndDenied.second, TEST_PACKAGE_NAME, 1);
+ }
+
+ @SmallTest
+ @Test
+ public void createTwoAndTriggerRoleChange() throws Exception {
+ SipTransportController controller = setupLiveTransportController();
+
+ DelegateRequest firstDelegateRequest = getBaseDelegateRequest();
+ Set<FeatureTagState> firstDeniedTags = getDeniedTagsForReason(
+ firstDelegateRequest.getFeatureTags(),
+ SipDelegateManager.DENIED_REASON_NOT_ALLOWED);
+ SipDelegateController c1 = injectMockDelegateController(TEST_PACKAGE_NAME,
+ firstDelegateRequest);
+ createDelegateAndVerify(controller, c1, firstDelegateRequest,
+ firstDelegateRequest.getFeatureTags(), Collections.emptySet(), TEST_PACKAGE_NAME);
+
+ DelegateRequest secondDelegateRequest = getBaseDelegateRequest();
+ Set<FeatureTagState> secondDeniedTags = getDeniedTagsForReason(
+ secondDelegateRequest.getFeatureTags(),
+ SipDelegateManager.DENIED_REASON_NOT_ALLOWED);
+ // Try to create a SipDelegate for a package that is not the default sms role.
+ SipDelegateController c2 = injectMockDelegateController(TEST_PACKAGE_NAME_2,
+ secondDelegateRequest);
+ createDelegateAndVerify(controller, c2, secondDelegateRequest, Collections.emptySet(),
+ secondDeniedTags, TEST_PACKAGE_NAME_2, 1);
+
+ // now swap the SMS role.
+ CompletableFuture<Boolean> pendingC1Change = setChangeSupportedFeatureTagsFuture(c1,
+ Collections.emptySet(), firstDeniedTags);
+ CompletableFuture<Boolean> pendingC2Change = setChangeSupportedFeatureTagsFuture(c2,
+ secondDelegateRequest.getFeatureTags(), Collections.emptySet());
+ setSmsRoleAndEvaluate(controller, TEST_PACKAGE_NAME_2);
+ // trigger completion stage to run
+ waitForExecutorAction(mExecutorService, TIMEOUT_MS);
+ verify(c1).changeSupportedFeatureTags(Collections.emptySet(), firstDeniedTags);
+ // we should not get a change for c2 until pendingC1Change completes.
+ verify(c2, never()).changeSupportedFeatureTags(secondDelegateRequest.getFeatureTags(),
+ Collections.emptySet());
+ // ensure we are not blocking executor here
+ waitForExecutorAction(mExecutorService, TIMEOUT_MS);
+ completePendingChange(pendingC1Change, true);
+ // trigger completion stage to run
+ waitForExecutorAction(mExecutorService, TIMEOUT_MS);
+ verify(c2).changeSupportedFeatureTags(secondDelegateRequest.getFeatureTags(),
+ Collections.emptySet());
+ // ensure we are not blocking executor here
+ waitForExecutorAction(mExecutorService, TIMEOUT_MS);
+ completePendingChange(pendingC2Change, true);
+ }
+
+ @SmallTest
+ @Test
+ public void createTwoAndDestroyOlder() throws Exception {
+ SipTransportController controller = setupLiveTransportController();
+
+ // First delegate requests RCS message + File transfer
+ ArraySet<String> firstDelegate = new ArraySet<>(getBaseDelegateRequest().getFeatureTags());
+ firstDelegate.remove(ImsSignallingUtils.GROUP_CHAT_TAG);
+ DelegateRequest firstDelegateRequest = new DelegateRequest(firstDelegate);
+ SipDelegateController c1 = injectMockDelegateController(TEST_PACKAGE_NAME,
+ firstDelegateRequest);
+ createDelegateAndVerify(controller, c1, firstDelegateRequest, firstDelegate,
+ Collections.emptySet(), TEST_PACKAGE_NAME);
+
+ // First delegate requests RCS message + Group RCS message. For this delegate, single RCS
+ // message should be denied.
+ ArraySet<String> secondDelegate = new ArraySet<>(getBaseDelegateRequest().getFeatureTags());
+ secondDelegate.remove(ImsSignallingUtils.FILE_TRANSFER_HTTP_TAG);
+ DelegateRequest secondDelegateRequest = new DelegateRequest(secondDelegate);
+ Pair<Set<String>, Set<FeatureTagState>> grantedAndDenied = getAllowedAndDeniedTagsForConfig(
+ secondDelegateRequest, SipDelegateManager.DENIED_REASON_IN_USE_BY_ANOTHER_DELEGATE,
+ firstDelegate);
+ SipDelegateController c2 = injectMockDelegateController(TEST_PACKAGE_NAME,
+ secondDelegateRequest);
+ createDelegateAndVerify(controller, c2, secondDelegateRequest, grantedAndDenied.first,
+ grantedAndDenied.second, TEST_PACKAGE_NAME, 1);
+
+ // Destroy the firstDelegate, which should now cause all previously denied tags to be
+ // granted to the new delegate.
+ CompletableFuture<Boolean> pendingC2Change = setChangeSupportedFeatureTagsFuture(c2,
+ secondDelegate, Collections.emptySet());
+ destroyDelegateAndVerify(controller, c1, false /*force*/,
+ SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_REQUESTED_BY_APP);
+ // wait for create to be processed.
+ assertTrue(waitForExecutorAction(mExecutorService, TIMEOUT_MS));
+ verify(c2).changeSupportedFeatureTags(secondDelegate, Collections.emptySet());
+ completePendingChange(pendingC2Change, true);
+ }
+
+ @SmallTest
+ @Test
+ public void testThrottling() throws Exception {
+ SipTransportController controller = setupLiveTransportController(THROTTLE_MS);
+
+ // First delegate requests RCS message + File transfer
+ ArraySet<String> firstDelegate = new ArraySet<>(getBaseDelegateRequest().getFeatureTags());
+ firstDelegate.remove(ImsSignallingUtils.GROUP_CHAT_TAG);
+ DelegateRequest firstDelegateRequest = new DelegateRequest(firstDelegate);
+ SipDelegateController c1 = injectMockDelegateController(TEST_PACKAGE_NAME,
+ firstDelegateRequest);
+ CompletableFuture<Boolean> pendingC1Change = createDelegate(controller, c1,
+ firstDelegateRequest, firstDelegate, Collections.emptySet(), TEST_PACKAGE_NAME);
+
+ // Request RCS message + group RCS Message. For this delegate, single RCS message should be
+ // denied.
+ ArraySet<String> secondDelegate = new ArraySet<>(getBaseDelegateRequest().getFeatureTags());
+ secondDelegate.remove(ImsSignallingUtils.FILE_TRANSFER_HTTP_TAG);
+ DelegateRequest secondDelegateRequest = new DelegateRequest(secondDelegate);
+ Pair<Set<String>, Set<FeatureTagState>> grantedAndDeniedC2 =
+ getAllowedAndDeniedTagsForConfig(secondDelegateRequest,
+ SipDelegateManager.DENIED_REASON_IN_USE_BY_ANOTHER_DELEGATE, firstDelegate);
+ SipDelegateController c2 = injectMockDelegateController(TEST_PACKAGE_NAME,
+ secondDelegateRequest);
+ CompletableFuture<Boolean> pendingC2Change = createDelegate(controller, c2,
+ secondDelegateRequest, grantedAndDeniedC2.first, grantedAndDeniedC2.second,
+ TEST_PACKAGE_NAME);
+
+ // Request group RCS message + file transfer. All should be denied at first
+ ArraySet<String> thirdDelegate = new ArraySet<>(getBaseDelegateRequest().getFeatureTags());
+ thirdDelegate.remove(ImsSignallingUtils.ONE_TO_ONE_CHAT_TAG);
+ DelegateRequest thirdDelegateRequest = new DelegateRequest(thirdDelegate);
+ Pair<Set<String>, Set<FeatureTagState>> grantedAndDeniedC3 =
+ getAllowedAndDeniedTagsForConfig(thirdDelegateRequest,
+ SipDelegateManager.DENIED_REASON_IN_USE_BY_ANOTHER_DELEGATE, firstDelegate,
+ grantedAndDeniedC2.first);
+ SipDelegateController c3 = injectMockDelegateController(TEST_PACKAGE_NAME,
+ thirdDelegateRequest);
+ CompletableFuture<Boolean> pendingC3Change = createDelegate(controller, c3,
+ thirdDelegateRequest, grantedAndDeniedC3.first, grantedAndDeniedC3.second,
+ TEST_PACKAGE_NAME);
+
+ assertTrue(scheduleDelayedWait(2 * THROTTLE_MS));
+ verifyDelegateChanged(c1, pendingC1Change, firstDelegate, Collections.emptySet(), 0);
+ verifyDelegateChanged(c2, pendingC2Change, grantedAndDeniedC2.first,
+ grantedAndDeniedC2.second, 0);
+ verifyDelegateChanged(c3, pendingC3Change, grantedAndDeniedC3.first,
+ grantedAndDeniedC3.second, 0);
+
+ // Destroy the first and second controller in quick succession, this should only generate
+ // one reevaluate for the third controller.
+ CompletableFuture<Boolean> pendingChangeC3 = setChangeSupportedFeatureTagsFuture(
+ c3, thirdDelegate, Collections.emptySet());
+ CompletableFuture<Integer> pendingDestroyC1 = destroyDelegate(controller, c1,
+ false /*force*/,
+ SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_REQUESTED_BY_APP);
+ CompletableFuture<Integer> pendingDestroyC2 = destroyDelegate(controller, c2,
+ false /*force*/,
+ SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_REQUESTED_BY_APP);
+ assertTrue(scheduleDelayedWait(2 * THROTTLE_MS));
+ verifyDestroyDelegate(controller, c1, pendingDestroyC1, false /*force*/,
+ SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_REQUESTED_BY_APP);
+ verifyDestroyDelegate(controller, c2, pendingDestroyC2, false /*force*/,
+ SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_REQUESTED_BY_APP);
+
+ // All requested features should now be granted
+ completePendingChange(pendingChangeC3, true);
+ verify(c3).changeSupportedFeatureTags(thirdDelegate, Collections.emptySet());
+ // In total reeval should have only been called twice.
+ verify(c3, times(2)).changeSupportedFeatureTags(any(), any());
+ }
+
+ @SmallTest
+ @Test
+ public void testSubIdChangeDestroyTriggered() throws Exception {
+ SipTransportController controller = setupLiveTransportController();
+
+ ArraySet<String> firstDelegate = new ArraySet<>(getBaseDelegateRequest().getFeatureTags());
+ DelegateRequest firstDelegateRequest = new DelegateRequest(firstDelegate);
+ SipDelegateController c1 = injectMockDelegateController(TEST_PACKAGE_NAME,
+ firstDelegateRequest);
+ createDelegateAndVerify(controller, c1, firstDelegateRequest, firstDelegate,
+ Collections.emptySet(), TEST_PACKAGE_NAME);
+
+ CompletableFuture<Integer> pendingDestroy = setDestroyFuture(c1, true,
+ SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_SUBSCRIPTION_TORN_DOWN);
+ controller.onAssociatedSubscriptionUpdated(TEST_SUB_ID + 1);
+ waitForExecutorAction(mExecutorService, TIMEOUT_MS);
+ verifyDestroyDelegate(controller, c1, pendingDestroy, true /*force*/,
+ SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_SUBSCRIPTION_TORN_DOWN);
+ }
+
+ @SmallTest
+ @Test
+ public void testRcsManagerGoneDestroyTriggered() throws Exception {
+ SipTransportController controller = setupLiveTransportController();
+
+ ArraySet<String> firstDelegate = new ArraySet<>(getBaseDelegateRequest().getFeatureTags());
+ DelegateRequest firstDelegateRequest = new DelegateRequest(firstDelegate);
+ SipDelegateController c1 = injectMockDelegateController(TEST_PACKAGE_NAME,
+ firstDelegateRequest);
+ createDelegateAndVerify(controller, c1, firstDelegateRequest, firstDelegate,
+ Collections.emptySet(), TEST_PACKAGE_NAME);
+
+ CompletableFuture<Integer> pendingDestroy = setDestroyFuture(c1, true,
+ SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_SERVICE_DEAD);
+ controller.onRcsDisconnected();
+ waitForExecutorAction(mExecutorService, TIMEOUT_MS);
+ verifyDestroyDelegate(controller, c1, pendingDestroy, true /*force*/,
+ SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_SERVICE_DEAD);
+ }
+
+ @SmallTest
+ @Test
+ public void testDestroyTriggered() throws Exception {
+ SipTransportController controller = setupLiveTransportController();
+
+ ArraySet<String> firstDelegate = new ArraySet<>(getBaseDelegateRequest().getFeatureTags());
+ DelegateRequest firstDelegateRequest = new DelegateRequest(firstDelegate);
+ SipDelegateController c1 = injectMockDelegateController(TEST_PACKAGE_NAME,
+ firstDelegateRequest);
+ createDelegateAndVerify(controller, c1, firstDelegateRequest, firstDelegate,
+ Collections.emptySet(), TEST_PACKAGE_NAME);
+
+ CompletableFuture<Integer> pendingDestroy = setDestroyFuture(c1, true,
+ SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_SUBSCRIPTION_TORN_DOWN);
+ controller.onDestroy();
+ waitForExecutorAction(mExecutorService, TIMEOUT_MS);
+ // verify change was called.
+ verify(c1).destroy(true /*force*/,
+ SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_SUBSCRIPTION_TORN_DOWN);
+ // ensure thread is not blocked while waiting for pending complete.
+ waitForExecutorAction(mExecutorService, TIMEOUT_MS);
+ completePendingDestroy(pendingDestroy,
+ SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_SUBSCRIPTION_TORN_DOWN);
+ }
+
+ @SmallTest
+ @Test
+ public void testTimingSubIdChangedAndCreateNewSubId() throws Exception {
+ SipTransportController controller = setupLiveTransportController(THROTTLE_MS);
+
+ ArraySet<String> firstDelegate = new ArraySet<>(getBaseDelegateRequest().getFeatureTags());
+ DelegateRequest firstDelegateRequest = new DelegateRequest(firstDelegate);
+ SipDelegateController c1 = injectMockDelegateController(TEST_PACKAGE_NAME,
+ firstDelegateRequest);
+ CompletableFuture<Boolean> pendingC1Change = createDelegate(controller, c1,
+ firstDelegateRequest, firstDelegate, Collections.emptySet(), TEST_PACKAGE_NAME);
+ assertTrue(scheduleDelayedWait(2 * THROTTLE_MS));
+ verifyDelegateChanged(c1, pendingC1Change, firstDelegate, Collections.emptySet(), 0);
+
+
+ CompletableFuture<Integer> pendingDestroy = setDestroyFuture(c1, true,
+ SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_SUBSCRIPTION_TORN_DOWN);
+ // triggers reeval now.
+ controller.onAssociatedSubscriptionUpdated(TEST_SUB_ID + 1);
+ waitForExecutorAction(mExecutorService, TIMEOUT_MS);
+
+ // mock a second delegate with the new subId associated with the slot.
+ ArraySet<String> secondDelegate = new ArraySet<>();
+ secondDelegate.add(ImsSignallingUtils.ONE_TO_ONE_CHAT_TAG);
+ secondDelegate.add(ImsSignallingUtils.FILE_TRANSFER_HTTP_TAG);
+ DelegateRequest secondDelegateRequest = new DelegateRequest(secondDelegate);
+ SipDelegateController c2 = injectMockDelegateController(TEST_SUB_ID + 1,
+ TEST_PACKAGE_NAME, secondDelegateRequest);
+ CompletableFuture<Boolean> pendingC2Change = createDelegate(controller, c2,
+ TEST_SUB_ID + 1, secondDelegateRequest, secondDelegate,
+ Collections.emptySet(), TEST_PACKAGE_NAME);
+ assertTrue(scheduleDelayedWait(THROTTLE_MS));
+
+ //trigger destroyed event
+ verifyDestroyDelegate(controller, c1, pendingDestroy, true /*force*/,
+ SipDelegateManager.SIP_DELEGATE_DESTROY_REASON_SUBSCRIPTION_TORN_DOWN);
+ assertTrue(scheduleDelayedWait(2 * THROTTLE_MS));
+ verifyDelegateChanged(c2, pendingC2Change, secondDelegate, Collections.emptySet(), 0);
+ }
+
+ @SafeVarargs
+ private final Pair<Set<String>, Set<FeatureTagState>> getAllowedAndDeniedTagsForConfig(
+ DelegateRequest r, int denyReason, Set<String>... previousRequestedTagSets) {
+ ArraySet<String> rejectedTags = new ArraySet<>(r.getFeatureTags());
+ ArraySet<String> grantedTags = new ArraySet<>(r.getFeatureTags());
+ Set<String> previousRequestedTags = new ArraySet<>();
+ for (Set<String> s : previousRequestedTagSets) {
+ previousRequestedTags.addAll(s);
+ }
+ rejectedTags.retainAll(previousRequestedTags);
+ grantedTags.removeAll(previousRequestedTags);
+ Set<FeatureTagState> deniedTags = getDeniedTagsForReason(rejectedTags, denyReason);
+ return new Pair<>(grantedTags, deniedTags);
+ }
+
+ private void completePendingChange(CompletableFuture<Boolean> change, boolean result) {
+ mExecutorService.execute(() -> change.complete(result));
+ waitForExecutorAction(mExecutorService, TIMEOUT_MS);
+ }
+
+ private void completePendingDestroy(CompletableFuture<Integer> destroy, int result) {
+ mExecutorService.execute(() -> destroy.complete(result));
+ waitForExecutorAction(mExecutorService, TIMEOUT_MS);
+ }
+
+ private SipTransportController setupLiveTransportController() throws Exception {
+ return setupLiveTransportController(0 /*throttleMs*/);
+ }
+
+ private SipTransportController setupLiveTransportController(int throttleMs) throws Exception {
+ mExecutorService = Executors.newSingleThreadScheduledExecutor();
+ SipTransportController controller = createControllerAndThrottle(mExecutorService,
+ throttleMs);
+ doReturn(mSipTransport).when(mRcsManager).getSipTransport();
+ controller.onAssociatedSubscriptionUpdated(TEST_SUB_ID);
+ controller.onRcsConnected(mRcsManager);
+ waitForExecutorAction(mExecutorService, TIMEOUT_MS);
+ return controller;
+ }
+
+ private void createDelegateAndVerify(SipTransportController controller,
+ SipDelegateController delegateController, DelegateRequest r, Set<String> allowedTags,
+ Set<FeatureTagState> deniedTags, String packageName,
+ int numPreviousChanges) throws ImsException {
+
+ CompletableFuture<Boolean> pendingChange = createDelegate(controller, delegateController, r,
+ allowedTags, deniedTags, packageName);
+ verifyDelegateChanged(delegateController, pendingChange, allowedTags, deniedTags,
+ numPreviousChanges);
+ }
+
+ private void createDelegateAndVerify(SipTransportController controller,
+ SipDelegateController delegateController, DelegateRequest r, Set<String> allowedTags,
+ Set<FeatureTagState> deniedTags, String packageName) throws ImsException {
+ createDelegateAndVerify(controller, delegateController, r, allowedTags, deniedTags,
+ packageName, 0);
+ }
+
+ private CompletableFuture<Boolean> createDelegate(SipTransportController controller,
+ SipDelegateController delegateController, int subId, DelegateRequest r,
+ Set<String> allowedTags, Set<FeatureTagState> deniedTags, String packageName) {
+ CompletableFuture<Boolean> pendingChange = setChangeSupportedFeatureTagsFuture(
+ delegateController, allowedTags, deniedTags);
+ try {
+ controller.createSipDelegate(subId, r, packageName, mMockStateCallback,
+ mMockMessageCallback);
+ } catch (ImsException e) {
+ fail("ImsException thrown:" + e);
+ }
+ // move to internal & schedule eval
+ waitForExecutorAction(mExecutorService, TIMEOUT_MS);
+ // reeval
+ waitForExecutorAction(mExecutorService, TIMEOUT_MS);
+ return pendingChange;
+ }
+
+ private CompletableFuture<Boolean> createDelegate(SipTransportController controller,
+ SipDelegateController delegateController, DelegateRequest r, Set<String> allowedTags,
+ Set<FeatureTagState> deniedTags, String packageName) throws ImsException {
+ return createDelegate(controller, delegateController, TEST_SUB_ID, r, allowedTags,
+ deniedTags, packageName);
+ }
+
+ private void verifyDelegateChanged(SipDelegateController delegateController,
+ CompletableFuture<Boolean> pendingChange, Set<String> allowedTags,
+ Set<FeatureTagState> deniedTags, int numPreviousChangeStages) {
+ // empty the queue of pending changeSupportedFeatureTags before running the one we are
+ // interested in, since the reevaluate waits for one stage to complete before moving to the
+ // next.
+ for (int i = 0; i < numPreviousChangeStages + 1; i++) {
+ assertTrue(waitForExecutorAction(mExecutorService, TIMEOUT_MS));
+ }
+ // verify change was called.
+ verify(delegateController).changeSupportedFeatureTags(allowedTags, deniedTags);
+ // ensure thread is not blocked while waiting for pending complete.
+ assertTrue(waitForExecutorAction(mExecutorService, TIMEOUT_MS));
+ completePendingChange(pendingChange, true);
+ // process pending change.
+ assertTrue(waitForExecutorAction(mExecutorService, TIMEOUT_MS));
+ }
+
+ private void destroyDelegateAndVerify(SipTransportController controller,
+ SipDelegateController delegateController, boolean force, int reason) {
+ CompletableFuture<Integer> pendingDestroy = destroyDelegate(controller, delegateController,
+ force, reason);
+ verifyDestroyDelegate(controller, delegateController, pendingDestroy, force, reason);
+ }
+
+ private CompletableFuture<Integer> destroyDelegate(SipTransportController controller,
+ SipDelegateController delegateController, boolean force, int reason) {
+ CompletableFuture<Integer> pendingDestroy = setDestroyFuture(delegateController, force,
+ reason);
+ controller.destroySipDelegate(TEST_SUB_ID, delegateController.getSipDelegateInterface(),
+ reason);
+ // move to internal & schedule eval
+ waitForExecutorAction(mExecutorService, TIMEOUT_MS);
+ // reeval
+ waitForExecutorAction(mExecutorService, TIMEOUT_MS);
+ return pendingDestroy;
+ }
+
+ private void verifyDestroyDelegate(SipTransportController controller,
+ SipDelegateController delegateController, CompletableFuture<Integer> pendingDestroy,
+ boolean force, int reason) {
+ // verify destroy was called.
+ verify(delegateController).destroy(force, reason);
+ // ensure thread is not blocked while waiting for pending complete.
+ waitForExecutorAction(mExecutorService, TIMEOUT_MS);
+ completePendingDestroy(pendingDestroy, reason);
+ }
+
+ private DelegateRequest getBaseDelegateRequest() {
+ Set<String> featureTags = new ArraySet<>();
+ featureTags.add(ImsSignallingUtils.ONE_TO_ONE_CHAT_TAG);
+ featureTags.add(ImsSignallingUtils.GROUP_CHAT_TAG);
+ featureTags.add(ImsSignallingUtils.FILE_TRANSFER_HTTP_TAG);
+ return new DelegateRequest(featureTags);
+ }
+
+ private Set<FeatureTagState> getBaseDeniedSet() {
+ Set<FeatureTagState> deniedTags = new ArraySet<>();
+ deniedTags.add(new FeatureTagState(ImsSignallingUtils.MMTEL_TAG,
+ SipDelegateManager.DENIED_REASON_IN_USE_BY_ANOTHER_DELEGATE));
+ return deniedTags;
+ }
+
+ private Set<FeatureTagState> getDeniedTagsForReason(Set<String> deniedTags, int reason) {
+ return deniedTags.stream().map(t -> new FeatureTagState(t, reason))
+ .collect(Collectors.toSet());
+ }
+
+ private SipDelegateController injectMockDelegateController(String packageName,
+ DelegateRequest r) {
+ return injectMockDelegateController(TEST_SUB_ID, packageName, r);
+ }
+
+ private SipDelegateController injectMockDelegateController(int subId, String packageName,
+ DelegateRequest r) {
+ SipDelegateControllerContainer c = new SipDelegateControllerContainer(subId,
+ packageName, r);
+ mMockControllers.add(c);
+ return c.delegateController;
+ }
+
+ private SipDelegateController getMockDelegateController(int subId, String packageName,
+ DelegateRequest r) {
+ return mMockControllers.stream()
+ .filter(c -> c.subId == subId && c.packageName.equals(packageName)
+ && c.delegateRequest.equals(r))
+ .map(c -> c.delegateController).findFirst().orElse(null);
+ }
+
+ private CompletableFuture<Boolean> setChangeSupportedFeatureTagsFuture(SipDelegateController c,
+ Set<String> supportedSet, Set<FeatureTagState> deniedSet) {
+ CompletableFuture<Boolean> result = new CompletableFuture<>();
+ doReturn(result).when(c).changeSupportedFeatureTags(eq(supportedSet), eq(deniedSet));
+ return result;
+ }
+
+ private CompletableFuture<Integer> setDestroyFuture(SipDelegateController c, boolean force,
+ int destroyReason) {
+ CompletableFuture<Integer> result = new CompletableFuture<>();
+ doReturn(result).when(c).destroy(force, destroyReason);
+ return result;
+ }
+
+ private void setSmsRoleAndEvaluate(SipTransportController c, String packageName) {
+ verify(mMockRoleManager).addOnRoleHoldersChangedListenerAsUser(any(), any(), any());
+ mSmsPackageName.clear();
+ mSmsPackageName.add(packageName);
+ c.onRoleHoldersChanged(RoleManager.ROLE_SMS, UserHandle.SYSTEM);
+ // finish internal throttled re-evaluate
+ waitForExecutorAction(mExecutorService, TIMEOUT_MS);
+ }
+
+ private SipTransportController createController(ScheduledExecutorService e) {
+ return createControllerAndThrottle(e, 0 /*throttleMs*/);
+ }
+
+ private SipTransportController createControllerAndThrottle(ScheduledExecutorService e,
+ int throttleMs) {
+ return new SipTransportController(mContext, 0 /*slotId*/, TEST_SUB_ID,
+ mMockDelegateControllerFactory, mMockRoleManager,
+ // Remove delays for testing.
+ new SipTransportController.TimerAdapter() {
+ @Override
+ public int getReevaluateThrottleTimerMilliseconds() {
+ return throttleMs;
+ }
+
+ @Override
+ public int getUpdateRegistrationDelayMilliseconds() {
+ return 0;
+ }
+ }, e);
+ }
+
+ private boolean scheduleDelayedWait(long timeMs) {
+ CountDownLatch l = new CountDownLatch(1);
+ mExecutorService.schedule(l::countDown, timeMs, TimeUnit.MILLISECONDS);
+ while (l.getCount() > 0) {
+ try {
+ return l.await(TIMEOUT_MS, TimeUnit.MILLISECONDS);
+ } catch (InterruptedException e) {
+ // try again
+ }
+ }
+ return true;
}
}