aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Android.bp28
-rw-r--r--OWNERS2
-rw-r--r--TEST_MAPPING2
-rw-r--r--src/java/com/android/ims/FeatureConnection.java184
-rw-r--r--src/java/com/android/ims/FeatureConnector.java371
-rw-r--r--src/java/com/android/ims/FeatureUpdates.java68
-rwxr-xr-xsrc/java/com/android/ims/ImsCall.java163
-rw-r--r--src/java/com/android/ims/ImsEcbm.java21
-rw-r--r--src/java/com/android/ims/ImsFeatureBinderRepository.java438
-rw-r--r--src/java/com/android/ims/ImsManager.java1941
-rw-r--r--src/java/com/android/ims/ImsMultiEndpoint.java13
-rw-r--r--src/java/com/android/ims/MmTelFeatureConnection.java318
-rw-r--r--src/java/com/android/ims/RcsFeatureConnection.java164
-rw-r--r--src/java/com/android/ims/RcsFeatureManager.java396
-rw-r--r--src/java/com/android/ims/internal/ConferenceParticipant.java1
-rw-r--r--src/java/com/android/ims/internal/ImsVideoCallProviderWrapper.java3
-rw-r--r--src/java/com/android/ims/rcs/uce/ControllerBase.java44
-rw-r--r--src/java/com/android/ims/rcs/uce/OWNERS3
-rw-r--r--src/java/com/android/ims/rcs/uce/UceController.java833
-rw-r--r--src/java/com/android/ims/rcs/uce/UceDeviceState.java410
-rw-r--r--src/java/com/android/ims/rcs/uce/eab/EabBulkCapabilityUpdater.java482
-rw-r--r--src/java/com/android/ims/rcs/uce/eab/EabCapabilityResult.java100
-rw-r--r--src/java/com/android/ims/rcs/uce/eab/EabContactSyncController.java357
-rw-r--r--src/java/com/android/ims/rcs/uce/eab/EabController.java51
-rw-r--r--src/java/com/android/ims/rcs/uce/eab/EabControllerImpl.java793
-rw-r--r--src/java/com/android/ims/rcs/uce/eab/EabProvider.java667
-rw-r--r--src/java/com/android/ims/rcs/uce/eab/EabUtil.java157
-rw-r--r--src/java/com/android/ims/rcs/uce/eab/RcsUceCapabilityBuilderWrapper.java55
-rw-r--r--src/java/com/android/ims/rcs/uce/options/OptionsController.java40
-rw-r--r--src/java/com/android/ims/rcs/uce/options/OptionsControllerImpl.java88
-rw-r--r--src/java/com/android/ims/rcs/uce/presence/pidfparser/ElementBase.java84
-rw-r--r--src/java/com/android/ims/rcs/uce/presence/pidfparser/PidfParser.java274
-rw-r--r--src/java/com/android/ims/rcs/uce/presence/pidfparser/PidfParserConstant.java39
-rw-r--r--src/java/com/android/ims/rcs/uce/presence/pidfparser/PidfParserUtils.java312
-rw-r--r--src/java/com/android/ims/rcs/uce/presence/pidfparser/capabilities/Audio.java91
-rw-r--r--src/java/com/android/ims/rcs/uce/presence/pidfparser/capabilities/CapsConstant.java (renamed from src/java/com/android/ims/IFeatureConnector.java)13
-rw-r--r--src/java/com/android/ims/rcs/uce/presence/pidfparser/capabilities/Duplex.java182
-rw-r--r--src/java/com/android/ims/rcs/uce/presence/pidfparser/capabilities/ServiceCaps.java117
-rw-r--r--src/java/com/android/ims/rcs/uce/presence/pidfparser/capabilities/Video.java92
-rw-r--r--src/java/com/android/ims/rcs/uce/presence/pidfparser/omapres/Description.java94
-rw-r--r--src/java/com/android/ims/rcs/uce/presence/pidfparser/omapres/OmaPresConstant.java21
-rw-r--r--src/java/com/android/ims/rcs/uce/presence/pidfparser/omapres/ServiceDescription.java137
-rw-r--r--src/java/com/android/ims/rcs/uce/presence/pidfparser/omapres/ServiceId.java94
-rw-r--r--src/java/com/android/ims/rcs/uce/presence/pidfparser/omapres/Version.java105
-rw-r--r--src/java/com/android/ims/rcs/uce/presence/pidfparser/pidf/Basic.java118
-rw-r--r--src/java/com/android/ims/rcs/uce/presence/pidfparser/pidf/Contact.java114
-rw-r--r--src/java/com/android/ims/rcs/uce/presence/pidfparser/pidf/Note.java95
-rw-r--r--src/java/com/android/ims/rcs/uce/presence/pidfparser/pidf/PidfConstant.java21
-rw-r--r--src/java/com/android/ims/rcs/uce/presence/pidfparser/pidf/Presence.java176
-rw-r--r--src/java/com/android/ims/rcs/uce/presence/pidfparser/pidf/Status.java99
-rw-r--r--src/java/com/android/ims/rcs/uce/presence/pidfparser/pidf/Timestamp.java91
-rw-r--r--src/java/com/android/ims/rcs/uce/presence/pidfparser/pidf/Tuple.java226
-rw-r--r--src/java/com/android/ims/rcs/uce/presence/publish/DeviceCapabilityInfo.java648
-rw-r--r--src/java/com/android/ims/rcs/uce/presence/publish/DeviceCapabilityListener.java736
-rw-r--r--src/java/com/android/ims/rcs/uce/presence/publish/PublishController.java232
-rw-r--r--src/java/com/android/ims/rcs/uce/presence/publish/PublishControllerImpl.java1080
-rw-r--r--src/java/com/android/ims/rcs/uce/presence/publish/PublishProcessor.java492
-rw-r--r--src/java/com/android/ims/rcs/uce/presence/publish/PublishProcessorState.java399
-rw-r--r--src/java/com/android/ims/rcs/uce/presence/publish/PublishRequestResponse.java342
-rw-r--r--src/java/com/android/ims/rcs/uce/presence/publish/PublishServiceDescTracker.java320
-rw-r--r--src/java/com/android/ims/rcs/uce/presence/publish/PublishUtils.java151
-rw-r--r--src/java/com/android/ims/rcs/uce/presence/publish/ServiceDescription.java197
-rw-r--r--src/java/com/android/ims/rcs/uce/presence/subscribe/SubscribeController.java38
-rw-r--r--src/java/com/android/ims/rcs/uce/presence/subscribe/SubscribeControllerImpl.java85
-rw-r--r--src/java/com/android/ims/rcs/uce/request/CapabilityRequest.java261
-rw-r--r--src/java/com/android/ims/rcs/uce/request/CapabilityRequestResponse.java485
-rw-r--r--src/java/com/android/ims/rcs/uce/request/OptionsRequest.java189
-rw-r--r--src/java/com/android/ims/rcs/uce/request/OptionsRequestCoordinator.java360
-rw-r--r--src/java/com/android/ims/rcs/uce/request/RemoteOptionsCoordinator.java187
-rw-r--r--src/java/com/android/ims/rcs/uce/request/RemoteOptionsRequest.java215
-rw-r--r--src/java/com/android/ims/rcs/uce/request/SubscribeRequest.java234
-rw-r--r--src/java/com/android/ims/rcs/uce/request/SubscribeRequestCoordinator.java523
-rw-r--r--src/java/com/android/ims/rcs/uce/request/SubscriptionTerminatedHelper.java182
-rw-r--r--src/java/com/android/ims/rcs/uce/request/UceRequest.java73
-rw-r--r--src/java/com/android/ims/rcs/uce/request/UceRequestCoordinator.java293
-rw-r--r--src/java/com/android/ims/rcs/uce/request/UceRequestDispatcher.java233
-rw-r--r--src/java/com/android/ims/rcs/uce/request/UceRequestManager.java829
-rw-r--r--src/java/com/android/ims/rcs/uce/request/UceRequestRepository.java92
-rw-r--r--src/java/com/android/ims/rcs/uce/util/FeatureTags.java136
-rw-r--r--src/java/com/android/ims/rcs/uce/util/NetworkSipCode.java113
-rw-r--r--src/java/com/android/ims/rcs/uce/util/UceUtils.java402
-rw-r--r--tests/Android.bp10
-rw-r--r--tests/AndroidManifest.xml5
-rw-r--r--tests/AndroidTest.xml29
-rw-r--r--tests/src/com/android/ims/ContextFixture.java168
-rw-r--r--tests/src/com/android/ims/FeatureConnectionTest.java177
-rw-r--r--tests/src/com/android/ims/FeatureConnectorTest.java422
-rw-r--r--tests/src/com/android/ims/ImsConfigTest.java4
-rw-r--r--tests/src/com/android/ims/ImsFeatureBinderRepositoryTest.java363
-rw-r--r--tests/src/com/android/ims/ImsFeatureContainerTest.java87
-rw-r--r--tests/src/com/android/ims/ImsManagerTest.java907
-rw-r--r--tests/src/com/android/ims/ImsTestBase.java16
-rw-r--r--tests/src/com/android/ims/ImsUtTest.java149
-rw-r--r--tests/src/com/android/ims/MmTelFeatureConnectionTest.java293
-rw-r--r--tests/src/com/android/ims/rcs/uce/OWNERS3
-rw-r--r--tests/src/com/android/ims/rcs/uce/UceControllerTest.java283
-rw-r--r--tests/src/com/android/ims/rcs/uce/eab/EabBulkCapabilityUpdaterTest.java200
-rw-r--r--tests/src/com/android/ims/rcs/uce/eab/EabContactSyncControllerTest.java277
-rw-r--r--tests/src/com/android/ims/rcs/uce/eab/EabControllerTest.java346
-rw-r--r--tests/src/com/android/ims/rcs/uce/eab/EabProviderTest.java338
-rw-r--r--tests/src/com/android/ims/rcs/uce/eab/EabProviderTestable.java105
-rw-r--r--tests/src/com/android/ims/rcs/uce/presence/pidfparser/PidfParserTest.java500
-rw-r--r--tests/src/com/android/ims/rcs/uce/presence/pidfparser/capabilities/AudioTest.java135
-rw-r--r--tests/src/com/android/ims/rcs/uce/presence/pidfparser/capabilities/DuplexTest.java147
-rw-r--r--tests/src/com/android/ims/rcs/uce/presence/pidfparser/capabilities/ServiceCapsTest.java181
-rw-r--r--tests/src/com/android/ims/rcs/uce/presence/pidfparser/capabilities/VideoTest.java135
-rw-r--r--tests/src/com/android/ims/rcs/uce/presence/pidfparser/omapres/DescriptionTest.java139
-rw-r--r--tests/src/com/android/ims/rcs/uce/presence/pidfparser/omapres/ServiceDescriptionTest.java150
-rw-r--r--tests/src/com/android/ims/rcs/uce/presence/pidfparser/omapres/ServiceIdTest.java139
-rw-r--r--tests/src/com/android/ims/rcs/uce/presence/pidfparser/omapres/VersionTest.java140
-rw-r--r--tests/src/com/android/ims/rcs/uce/presence/pidfparser/pidf/BasicTest.java145
-rw-r--r--tests/src/com/android/ims/rcs/uce/presence/pidfparser/pidf/ContactTest.java134
-rw-r--r--tests/src/com/android/ims/rcs/uce/presence/pidfparser/pidf/NoteTest.java132
-rw-r--r--tests/src/com/android/ims/rcs/uce/presence/pidfparser/pidf/PresenceTest.java209
-rw-r--r--tests/src/com/android/ims/rcs/uce/presence/pidfparser/pidf/StatusTest.java130
-rw-r--r--tests/src/com/android/ims/rcs/uce/presence/pidfparser/pidf/TimestampTest.java134
-rw-r--r--tests/src/com/android/ims/rcs/uce/presence/pidfparser/pidf/TupleTest.java238
-rw-r--r--tests/src/com/android/ims/rcs/uce/presence/publish/DeviceCapabilityListenerTest.java246
-rw-r--r--tests/src/com/android/ims/rcs/uce/presence/publish/PublishControllerImplTest.java357
-rw-r--r--tests/src/com/android/ims/rcs/uce/presence/publish/PublishProcessorTest.java246
-rw-r--r--tests/src/com/android/ims/rcs/uce/presence/publish/PublishServiceDescTrackerTest.java260
-rw-r--r--tests/src/com/android/ims/rcs/uce/request/CapabilityRequestTest.java195
-rw-r--r--tests/src/com/android/ims/rcs/uce/request/OptionsCoordinatorTest.java149
-rw-r--r--tests/src/com/android/ims/rcs/uce/request/OptionsRequestTest.java163
-rw-r--r--tests/src/com/android/ims/rcs/uce/request/RemoteOptionsCoordinatorTest.java109
-rw-r--r--tests/src/com/android/ims/rcs/uce/request/RemoteOptionsRequestTest.java154
-rw-r--r--tests/src/com/android/ims/rcs/uce/request/SubscribeCoordinatorTest.java249
-rw-r--r--tests/src/com/android/ims/rcs/uce/request/SubscribeRequestTest.java198
-rw-r--r--tests/src/com/android/ims/rcs/uce/request/UceRequestManagerTest.java270
129 files changed, 28430 insertions, 1451 deletions
diff --git a/Android.bp b/Android.bp
index 860c7403..6b78ebbe 100644
--- a/Android.bp
+++ b/Android.bp
@@ -12,6 +12,34 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+package {
+ default_applicable_licenses: ["frameworks_opt_net_ims_license"],
+}
+
+// Added automatically by a large-scale-change that took the approach of
+// 'apply every license found to every target'. While this makes sure we respect
+// every license restriction, it may not be entirely correct.
+//
+// e.g. GPL in an MIT project might only apply to the contrib/ directory.
+//
+// Please consider splitting the single license below into multiple licenses,
+// taking care not to lose any license_kind information, and overriding the
+// default license using the 'licenses: [...]' property on targets as needed.
+//
+// For unused files, consider creating a 'fileGroup' with "//visibility:private"
+// to attach the license to, and including a comment whether the files may be
+// used in the current project.
+// See: http://go/android-license-faq
+license {
+ name: "frameworks_opt_net_ims_license",
+ visibility: [":__subpackages__"],
+ license_kinds: [
+ "SPDX-license-identifier-Apache-2.0",
+ "SPDX-license-identifier-BSD",
+ ],
+ // large-scale-change unable to identify any license_text files
+}
+
java_library {
name: "ims-common",
installable: true,
diff --git a/OWNERS b/OWNERS
index 94409ef1..8a0d4eda 100644
--- a/OWNERS
+++ b/OWNERS
@@ -1,4 +1,4 @@
breadley@google.com
hallliu@google.com
tgunn@google.com
-paulye@google.com
+dbright@google.com
diff --git a/TEST_MAPPING b/TEST_MAPPING
index e75dcb02..4b2fe34b 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -1,7 +1,7 @@
{
"presubmit": [
{
- "name": "TeleServiceTests",
+ "name": "ImsCommonTests",
"options": [
{
"exclude-annotation": "androidx.test.filters.FlakyTest"
diff --git a/src/java/com/android/ims/FeatureConnection.java b/src/java/com/android/ims/FeatureConnection.java
index f6668b53..748ae577 100644
--- a/src/java/com/android/ims/FeatureConnection.java
+++ b/src/java/com/android/ims/FeatureConnection.java
@@ -18,21 +18,21 @@ package com.android.ims;
import android.annotation.Nullable;
import android.content.Context;
-import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.RemoteException;
import android.telephony.TelephonyManager;
+import android.telephony.ims.ImsService;
+import android.telephony.ims.aidl.IImsConfig;
import android.telephony.ims.aidl.IImsRegistration;
+import android.telephony.ims.aidl.ISipTransport;
import android.telephony.ims.feature.ImsFeature;
import android.telephony.ims.stub.ImsRegistrationImplBase;
import android.util.Log;
-import com.android.ims.internal.IImsServiceFeatureCallback;
import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.telephony.util.HandlerExecutor;
-import java.util.concurrent.Executor;
+import java.util.NoSuchElementException;
/**
* Base class of MmTelFeatureConnection and RcsFeatureConnection.
@@ -40,50 +40,29 @@ import java.util.concurrent.Executor;
public abstract class FeatureConnection {
protected static final String TAG = "FeatureConnection";
- public interface IFeatureUpdate {
- /**
- * Called when the ImsFeature has changed its state. Use
- * {@link ImsFeature#getFeatureState()} to get the new state.
- */
- void notifyStateChanged();
-
- /**
- * Called when the ImsFeature has become unavailable due to the binder switching or app
- * crashing. A new ImsServiceProxy should be requested for that feature.
- */
- void notifyUnavailable();
- }
-
protected static boolean sImsSupportedOnDevice = true;
protected final int mSlotId;
protected Context mContext;
protected IBinder mBinder;
- @VisibleForTesting
- public Executor mExecutor;
// We are assuming the feature is available when started.
protected volatile boolean mIsAvailable = true;
// ImsFeature Status from the ImsService. Cached.
protected Integer mFeatureStateCached = null;
- protected IFeatureUpdate mStatusCallback;
- protected IImsRegistration mRegistrationBinder;
+ protected long mFeatureCapabilities;
+ private final IImsRegistration mRegistrationBinder;
+ private final IImsConfig mConfigBinder;
+ private final ISipTransport mSipTransportBinder;
protected final Object mLock = new Object();
- public FeatureConnection(Context context, int slotId) {
+ public FeatureConnection(Context context, int slotId, IImsConfig c, IImsRegistration r,
+ ISipTransport s) {
mSlotId = slotId;
mContext = context;
-
- // Callbacks should be scheduled on the main thread.
- if (context.getMainLooper() != null) {
- mExecutor = context.getMainExecutor();
- } else {
- // Fallback to the current thread.
- if (Looper.myLooper() == null) {
- Looper.prepare();
- }
- mExecutor = new HandlerExecutor(new Handler(Looper.myLooper()));
- }
+ mRegistrationBinder = r;
+ mConfigBinder = c;
+ mSipTransportBinder = s;
}
protected TelephonyManager getTelephonyManager() {
@@ -102,7 +81,8 @@ public abstract class FeatureConnection {
mBinder.linkToDeath(mDeathRecipient, 0);
}
} catch (RemoteException e) {
- // No need to do anything if the binder is already dead.
+ Log.w(TAG, "setBinder: linkToDeath on already dead Binder, setting null");
+ mBinder = null;
}
}
}
@@ -126,58 +106,17 @@ public abstract class FeatureConnection {
synchronized (mLock) {
if (mIsAvailable) {
mIsAvailable = false;
- mRegistrationBinder = null;
- if (mBinder != null) {
- mBinder.unlinkToDeath(mDeathRecipient, 0);
- }
- if (mStatusCallback != null) {
- Log.d(TAG, "onRemovedOrDied: notifyUnavailable");
- mStatusCallback.notifyUnavailable();
- // Unlink because this FeatureConnection should no longer send callbacks.
- mStatusCallback = null;
+ try {
+ if (mBinder != null) {
+ mBinder.unlinkToDeath(mDeathRecipient, 0);
+ }
+ } catch (NoSuchElementException e) {
+ Log.w(TAG, "onRemovedOrDied: unlinkToDeath called on unlinked Binder.");
}
}
}
}
- /**
- * The listener for ImsManger and RcsFeatureManager to receive IMS feature status changed.
- * @param callback Callback that will fire when the feature status has changed.
- */
- public void setStatusCallback(IFeatureUpdate callback) {
- mStatusCallback = callback;
- }
-
- @VisibleForTesting
- public IImsServiceFeatureCallback getListener() {
- return mListenerBinder;
- }
-
- /**
- * The callback to receive ImsFeature status changed.
- */
- private final IImsServiceFeatureCallback mListenerBinder =
- new IImsServiceFeatureCallback.Stub() {
- @Override
- public void imsFeatureCreated(int slotId, int feature) {
- mExecutor.execute(() -> {
- handleImsFeatureCreatedCallback(slotId, feature);
- });
- }
- @Override
- public void imsFeatureRemoved(int slotId, int feature) {
- mExecutor.execute(() -> {
- handleImsFeatureRemovedCallback(slotId, feature);
- });
- }
- @Override
- public void imsStatusChanged(int slotId, int feature, int status) {
- mExecutor.execute(() -> {
- handleImsStatusChangedCallback(slotId, feature, status);
- });
- }
- };
-
public @ImsRegistrationImplBase.ImsRegistrationTech int getRegistrationTech()
throws RemoteException {
IImsRegistration registration = getRegistration();
@@ -190,24 +129,17 @@ public abstract class FeatureConnection {
}
public @Nullable IImsRegistration getRegistration() {
- synchronized (mLock) {
- // null if cache is invalid;
- if (mRegistrationBinder != null) {
- return mRegistrationBinder;
- }
- }
- // We don't want to synchronize on a binder call to another process.
- IImsRegistration regBinder = getRegistrationBinder();
- synchronized (mLock) {
- // mRegistrationBinder may have changed while we tried to get the registration
- // interface.
- if (mRegistrationBinder == null) {
- mRegistrationBinder = regBinder;
- }
- }
return mRegistrationBinder;
}
+ public @Nullable IImsConfig getConfig() {
+ return mConfigBinder;
+ }
+
+ public @Nullable ISipTransport getSipTransport() {
+ return mSipTransportBinder;
+ }
+
@VisibleForTesting
public void checkServiceIsReady() throws RemoteException {
if (!sImsSupportedOnDevice) {
@@ -238,6 +170,35 @@ public abstract class FeatureConnection {
return mIsAvailable && mBinder != null && mBinder.isBinderAlive();
}
+ public void updateFeatureState(int state) {
+ synchronized (mLock) {
+ mFeatureStateCached = state;
+ }
+ }
+
+ public long getFeatureCapabilties() {
+ synchronized (mLock) {
+ return mFeatureCapabilities;
+ }
+ }
+
+ public void updateFeatureCapabilities(long caps) {
+ synchronized (mLock) {
+ if (mFeatureCapabilities != caps) {
+ mFeatureCapabilities = caps;
+ onFeatureCapabilitiesUpdated(caps);
+ }
+ }
+ }
+
+ public boolean isCapable(@ImsService.ImsServiceCapability long capabilities)
+ throws RemoteException {
+ if (!isBinderAlive()) {
+ throw new RemoteException("isCapable: ImsService is not alive");
+ }
+ return (getFeatureCapabilties() & capabilities) > 0;
+ }
+
/**
* @return an integer describing the current Feature Status, defined in
* {@link ImsFeature.ImsState}.
@@ -263,36 +224,9 @@ public abstract class FeatureConnection {
}
/**
- * An ImsFeature has been created for this FeatureConnection for the associated
- * {@link ImsFeature.FeatureType}.
- * @param slotId The slot ID associated with the event.
- * @param feature The {@link ImsFeature.FeatureType} associated with the event.
- */
- protected abstract void handleImsFeatureCreatedCallback(int slotId, int feature);
-
- /**
- * An ImsFeature has been removed for this FeatureConnection for the associated
- * {@link ImsFeature.FeatureType}.
- * @param slotId The slot ID associated with the event.
- * @param feature The {@link ImsFeature.FeatureType} associated with the event.
- */
- protected abstract void handleImsFeatureRemovedCallback(int slotId, int feature);
-
- /**
- * The status of an ImsFeature has changed for the associated {@link ImsFeature.FeatureType}.
- * @param slotId The slot ID associated with the event.
- * @param feature The {@link ImsFeature.FeatureType} associated with the event.
- * @param status The new {@link ImsFeature.ImsState} associated with the ImsFeature
- */
- protected abstract void handleImsStatusChangedCallback(int slotId, int feature, int status);
-
- /**
* Internal method used to retrieve the feature status from the corresponding ImsService.
*/
protected abstract Integer retrieveFeatureState();
- /**
- * @return The ImsRegistration instance associated with the FeatureConnection.
- */
- protected abstract IImsRegistration getRegistrationBinder();
+ protected abstract void onFeatureCapabilitiesUpdated(long capabilities);
}
diff --git a/src/java/com/android/ims/FeatureConnector.java b/src/java/com/android/ims/FeatureConnector.java
index e7c1c74a..19e21511 100644
--- a/src/java/com/android/ims/FeatureConnector.java
+++ b/src/java/com/android/ims/FeatureConnector.java
@@ -16,114 +16,227 @@
package com.android.ims;
+import android.annotation.IntDef;
import android.content.Context;
import android.content.pm.PackageManager;
-import android.os.Handler;
-import android.os.Looper;
+import android.os.RemoteException;
import android.telephony.ims.ImsReasonInfo;
+import android.telephony.ims.ImsService;
import android.telephony.ims.feature.ImsFeature;
+import com.android.ims.internal.IImsServiceFeatureCallback;
import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.telephony.util.HandlerExecutor;
import com.android.telephony.Rlog;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
import java.util.concurrent.Executor;
/**
* Helper class for managing a connection to the ImsFeature manager.
*/
-public class FeatureConnector<T extends IFeatureConnector> extends Handler {
+public class FeatureConnector<U extends FeatureUpdates> {
private static final String TAG = "FeatureConnector";
private static final boolean DBG = false;
- // Initial condition for ims connection retry.
- private static final int IMS_RETRY_STARTING_TIMEOUT_MS = 500; // ms
+ /**
+ * This Connection has become unavailable due to the ImsService being disconnected due to
+ * an event such as SIM Swap, carrier configuration change, etc...
+ *
+ * {@link Listener#connectionReady} will be called when a new Manager is available.
+ */
+ public static final int UNAVAILABLE_REASON_DISCONNECTED = 0;
- // Ceiling bitshift amount for service query timeout, calculated as:
- // 2^mImsServiceRetryCount * IMS_RETRY_STARTING_TIMEOUT_MS, where
- // mImsServiceRetryCount ∊ [0, CEILING_SERVICE_RETRY_COUNT].
- private static final int CEILING_SERVICE_RETRY_COUNT = 6;
+ /**
+ * This Connection has become unavailable due to the ImsService moving to the NOT_READY state.
+ *
+ * {@link Listener#connectionReady} will be called when the manager moves back to ready.
+ */
+ public static final int UNAVAILABLE_REASON_NOT_READY = 1;
- public interface Listener<T> {
+ /**
+ * IMS is not supported on this device. This should be considered a permanent error and
+ * a Manager will never become available.
+ */
+ public static final int UNAVAILABLE_REASON_IMS_UNSUPPORTED = 2;
+
+ /**
+ * The server of this information has crashed or otherwise generated an error that will require
+ * a retry to connect. This is rare, however in this case, {@link #disconnect()} and
+ * {@link #connect()} will need to be called again to recreate the connection with the server.
+ * <p>
+ * Only applicable if this is used outside of the server's own process.
+ */
+ public static final int UNAVAILABLE_REASON_SERVER_UNAVAILABLE = 3;
+
+ /**
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = "UNAVAILABLE_REASON_", value = {
+ UNAVAILABLE_REASON_DISCONNECTED,
+ UNAVAILABLE_REASON_NOT_READY,
+ UNAVAILABLE_REASON_IMS_UNSUPPORTED,
+ UNAVAILABLE_REASON_SERVER_UNAVAILABLE
+ })
+ public @interface UnavailableReason {}
+
+ /**
+ * Factory used to create a new instance of the manager that this FeatureConnector is waiting
+ * to connect the FeatureConnection to.
+ * @param <U> The Manager that this FeatureConnector has been created for.
+ */
+ public interface ManagerFactory<U extends FeatureUpdates> {
/**
- * Get ImsFeature manager instance
+ * Create a manager instance, which will connect to the FeatureConnection.
*/
- T getFeatureManager();
+ U createManager(Context context, int phoneId);
+ }
+ /**
+ * Listener interface used by Listeners of FeatureConnector that are waiting for a Manager
+ * interface for a specific ImsFeature.
+ * @param <U> The Manager that the listener is listening for.
+ */
+ public interface Listener<U extends FeatureUpdates> {
/**
* ImsFeature manager is connected to the underlying IMS implementation.
*/
- void connectionReady(T manager) throws ImsException;
+ void connectionReady(U manager) throws ImsException;
/**
* The underlying IMS implementation is unavailable and can not be used to communicate.
*/
- void connectionUnavailable();
- }
-
- public interface RetryTimeout {
- int get();
+ void connectionUnavailable(@UnavailableReason int reason);
}
- protected final int mPhoneId;
- protected final Context mContext;
- protected final Executor mExecutor;
- protected final Object mLock = new Object();
- protected final String mLogPrefix;
+ private final IImsServiceFeatureCallback mCallback = new IImsServiceFeatureCallback.Stub() {
- @VisibleForTesting
- public Listener<T> mListener;
+ @Override
+ public void imsFeatureCreated(ImsFeatureContainer c) {
+ log("imsFeatureCreated: " + c);
+ synchronized (mLock) {
+ mManager.associate(c);
+ mManager.updateFeatureCapabilities(c.getCapabilities());
+ mDisconnectedReason = null;
+ }
+ // Notifies executor, so notify outside of lock
+ imsStatusChanged(c.getState());
+ }
- // The IMS feature manager which interacts with ImsService
- @VisibleForTesting
- public T mManager;
+ @Override
+ public void imsFeatureRemoved(@UnavailableReason int reason) {
+ log("imsFeatureRemoved: reason=" + reason);
+ synchronized (mLock) {
+ // only generate new events if the disconnect event isn't the same as before, except
+ // for UNAVAILABLE_REASON_SERVER_UNAVAILABLE, which indicates a local issue and
+ // each event is actionable.
+ if (mDisconnectedReason != null
+ && (mDisconnectedReason == reason
+ && mDisconnectedReason != UNAVAILABLE_REASON_SERVER_UNAVAILABLE)) {
+ log("imsFeatureRemoved: ignore");
+ return;
+ }
+ mDisconnectedReason = reason;
+ // Ensure that we set ready state back to false so that we do not miss setting ready
+ // later if the initial state when recreated is READY.
+ mLastReadyState = false;
+ }
+ // Allow the listener to do cleanup while the connection still potentially valid (unless
+ // the process crashed).
+ mExecutor.execute(() -> mListener.connectionUnavailable(reason));
+ mManager.invalidate();
+ }
- protected int mRetryCount = 0;
+ @Override
+ public void imsStatusChanged(int status) {
+ log("imsStatusChanged: status=" + ImsFeature.STATE_LOG_MAP.get(status));
+ final U manager;
+ final boolean isReady;
+ synchronized (mLock) {
+ if (mDisconnectedReason != null) {
+ log("imsStatusChanged: ignore");
+ return;
+ }
+ mManager.updateFeatureState(status);
+ manager = mManager;
+ isReady = mReadyFilter.contains(status);
+ boolean didReadyChange = isReady ^ mLastReadyState;
+ mLastReadyState = isReady;
+ if (!didReadyChange) {
+ log("imsStatusChanged: ready didn't change, ignore");
+ return;
+ }
+ }
+ mExecutor.execute(() -> {
+ try {
+ if (isReady) {
+ notifyReady(manager);
+ } else {
+ notifyNotReady();
+ }
+ } catch (ImsException e) {
+ if (e.getCode()
+ == ImsReasonInfo.CODE_LOCAL_IMS_NOT_SUPPORTED_ON_DEVICE) {
+ mListener.connectionUnavailable(UNAVAILABLE_REASON_IMS_UNSUPPORTED);
+ } else {
+ notifyNotReady();
+ }
+ }
+ });
+ }
- @VisibleForTesting
- public RetryTimeout mRetryTimeout = () -> {
- synchronized (mLock) {
- int timeout = (1 << mRetryCount) * IMS_RETRY_STARTING_TIMEOUT_MS;
- if (mRetryCount <= CEILING_SERVICE_RETRY_COUNT) {
- mRetryCount++;
+ @Override
+ public void updateCapabilities(long caps) {
+ log("updateCapabilities: capabilities=" + ImsService.getCapabilitiesString(caps));
+ synchronized (mLock) {
+ if (mDisconnectedReason != null) {
+ log("updateCapabilities: ignore");
+ return;
+ }
+ mManager.updateFeatureCapabilities(caps);
}
- return timeout;
}
};
- public FeatureConnector(Context context, int phoneId, Listener<T> listener,
- String logPrefix) {
- mContext = context;
- mPhoneId = phoneId;
- mListener = listener;
- mExecutor = new HandlerExecutor(this);
- mLogPrefix = logPrefix;
- }
+ private final int mPhoneId;
+ private final Context mContext;
+ private final ManagerFactory<U> mFactory;
+ private final Listener<U> mListener;
+ private final Executor mExecutor;
+ private final Object mLock = new Object();
+ private final String mLogPrefix;
+ // A List of integers, each corresponding to an ImsFeature.ImsState, that the FeatureConnector
+ // will use to call Listener#connectionReady when the ImsFeature that this connector is waiting
+ // for changes into one of the states in this list.
+ private final List<Integer> mReadyFilter = new ArrayList<>();
+
+ private U mManager;
+ // Start in disconnected state;
+ private Integer mDisconnectedReason = UNAVAILABLE_REASON_DISCONNECTED;
+ // Stop redundant connectionAvailable if the ready filter contains multiple states.
+ // Also, do not send the first unavailable until after we have moved to available once.
+ private boolean mLastReadyState = false;
+
- @VisibleForTesting
- public FeatureConnector(Context context, int phoneId, Listener<T> listener,
- Executor executor, String logPrefix) {
- mContext = context;
- mPhoneId = phoneId;
- mListener= listener;
- mExecutor = executor;
- mLogPrefix = logPrefix;
- }
@VisibleForTesting
- public FeatureConnector(Context context, int phoneId, Listener<T> listener,
- Executor executor, Looper looper) {
- super(looper);
+ public FeatureConnector(Context context, int phoneId, ManagerFactory<U> factory,
+ String logPrefix, List<Integer> readyFilter, Listener<U> listener, Executor executor) {
mContext = context;
mPhoneId = phoneId;
- mListener= listener;
+ mFactory = factory;
+ mLogPrefix = logPrefix;
+ mReadyFilter.addAll(readyFilter);
+ mListener = listener;
mExecutor = executor;
- mLogPrefix = "?";
}
/**
* Start the creation of a connection to the underlying ImsService implementation. When the
- * service is connected, {@link FeatureConnector.Listener#connectionReady(Object)} will be
+ * service is connected, {@link FeatureConnector.Listener#connectionReady} will be
* called with an active instance.
*
* If this device does not support an ImsStack (i.e. doesn't support
@@ -132,133 +245,44 @@ public class FeatureConnector<T extends IFeatureConnector> extends Handler {
public void connect() {
if (DBG) log("connect");
if (!isSupported()) {
+ mExecutor.execute(() -> mListener.connectionUnavailable(
+ UNAVAILABLE_REASON_IMS_UNSUPPORTED));
logw("connect: not supported.");
return;
}
- mRetryCount = 0;
-
- // Send a message to connect to the Ims Service and open a connection through
- // getImsService().
- post(mGetServiceRunnable);
+ synchronized (mLock) {
+ if (mManager == null) {
+ mManager = mFactory.createManager(mContext, mPhoneId);
+ }
+ }
+ mManager.registerFeatureCallback(mPhoneId, mCallback);
}
// Check if this ImsFeature is supported or not.
private boolean isSupported() {
- return ImsManager.isImsSupportedOnDevice(mContext);
+ return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY_IMS);
}
/**
* Disconnect from the ImsService Implementation and clean up. When this is complete,
- * {@link FeatureConnector.Listener#connectionUnavailable()} will be called one last time.
+ * {@link FeatureConnector.Listener#connectionUnavailable(int)} will be called one last time.
*/
public void disconnect() {
if (DBG) log("disconnect");
- removeCallbacks(mGetServiceRunnable);
+ final U manager;
synchronized (mLock) {
- if (mManager != null) {
- mManager.removeNotifyStatusChangedCallback(mNotifyStatusChangedCallback);
- }
+ manager = mManager;
}
- notifyNotReady();
- }
+ if (manager == null) return;
- private final Runnable mGetServiceRunnable = () -> {
+ manager.unregisterFeatureCallback(mCallback);
try {
- createImsService();
- } catch (android.telephony.ims.ImsException e) {
- int errorCode = e.getCode();
- if (DBG) logw("Create IMS service error: " + errorCode);
- if (android.telephony.ims.ImsException.CODE_ERROR_UNSUPPORTED_OPERATION != errorCode) {
- // Retry when error is not CODE_ERROR_UNSUPPORTED_OPERATION
- retryGetImsService();
- }
- }
- };
-
- @VisibleForTesting
- public void createImsService() throws android.telephony.ims.ImsException {
- synchronized (mLock) {
- if (DBG) log("createImsService");
- mManager = mListener.getFeatureManager();
- // Adding to set, will be safe adding multiple times. If the ImsService is not
- // active yet, this method will throw an ImsException.
- mManager.addNotifyStatusChangedCallbackIfAvailable(mNotifyStatusChangedCallback);
- }
- // Wait for ImsService.STATE_READY to start listening for calls.
- // Call the callback right away for compatibility with older devices that do not use
- // states.
- mNotifyStatusChangedCallback.notifyStateChanged();
- }
-
- /**
- * Remove callback and re-running mGetServiceRunnable
- */
- public void retryGetImsService() {
- if (mManager != null) {
- // remove callback so we do not receive updates from old ImsServiceProxy when
- // switching between ImsServices.
- mManager.removeNotifyStatusChangedCallback(mNotifyStatusChangedCallback);
- //Leave mImsManager as null, then CallStateException will be thrown when dialing
- mManager = null;
- }
-
- // Exponential backoff during retry, limited to 32 seconds.
- removeCallbacks(mGetServiceRunnable);
- int timeout = mRetryTimeout.get();
- postDelayed(mGetServiceRunnable, timeout);
- if (DBG) log("retryGetImsService: unavailable, retrying in " + timeout + " ms");
+ mCallback.imsFeatureRemoved(UNAVAILABLE_REASON_DISCONNECTED);
+ } catch (RemoteException ignore) {} // local call
}
- // Callback fires when IMS Feature changes state
- public FeatureConnection.IFeatureUpdate mNotifyStatusChangedCallback =
- new FeatureConnection.IFeatureUpdate() {
- @Override
- public void notifyStateChanged() {
- mExecutor.execute(() -> {
- try {
- int status = ImsFeature.STATE_UNAVAILABLE;
- synchronized (mLock) {
- if (mManager != null) {
- status = mManager.getImsServiceState();
- }
- }
- switch (status) {
- case ImsFeature.STATE_READY: {
- notifyReady();
- break;
- }
- case ImsFeature.STATE_INITIALIZING:
- // fall through
- case ImsFeature.STATE_UNAVAILABLE: {
- notifyNotReady();
- break;
- }
- default: {
- logw("Unexpected State! " + status);
- }
- }
- } catch (ImsException e) {
- // Could not get the ImsService, retry!
- notifyNotReady();
- retryGetImsService();
- }
- });
- }
-
- @Override
- public void notifyUnavailable() {
- mExecutor.execute(() -> {
- notifyNotReady();
- retryGetImsService();
- });
- }
- };
-
- private void notifyReady() throws ImsException {
- T manager;
- synchronized (mLock) {
- manager = mManager;
- }
+ // Should be called on executor
+ private void notifyReady(U manager) throws ImsException {
try {
if (DBG) log("notifyReady");
mListener.connectionReady(manager);
@@ -267,22 +291,19 @@ public class FeatureConnector<T extends IFeatureConnector> extends Handler {
if(DBG) log("notifyReady exception: " + e.getMessage());
throw e;
}
- // Only reset retry count if connectionReady does not generate an ImsException/
- synchronized (mLock) {
- mRetryCount = 0;
- }
}
- protected void notifyNotReady() {
+ // Should be called on executor.
+ private void notifyNotReady() {
if (DBG) log("notifyNotReady");
- mListener.connectionUnavailable();
+ mListener.connectionUnavailable(UNAVAILABLE_REASON_NOT_READY);
}
- private final void log(String message) {
+ private void log(String message) {
Rlog.d(TAG, "[" + mLogPrefix + ", " + mPhoneId + "] " + message);
}
- private final void logw(String message) {
+ private void logw(String message) {
Rlog.w(TAG, "[" + mLogPrefix + ", " + mPhoneId + "] " + message);
}
}
diff --git a/src/java/com/android/ims/FeatureUpdates.java b/src/java/com/android/ims/FeatureUpdates.java
new file mode 100644
index 00000000..446a78b9
--- /dev/null
+++ b/src/java/com/android/ims/FeatureUpdates.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (c) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ims;
+
+import android.telephony.ims.ImsService;
+import android.telephony.ims.feature.ImsFeature;
+
+import com.android.ims.internal.IImsServiceFeatureCallback;
+
+/**
+ * Interface used by Manager interfaces that will use a {@link FeatureConnector} to connect to
+ * remote ImsFeature Binder interfaces.
+ */
+public interface FeatureUpdates {
+ /**
+ * Register a callback for the slot specified so that the FeatureConnector can notify its
+ * listener of changes.
+ * @param slotId The slot the callback is registered for.
+ * @param cb The callback that the FeatureConnector will use to update its state and notify
+ * its callback of changes.
+ */
+ void registerFeatureCallback(int slotId, IImsServiceFeatureCallback cb);
+
+ /**
+ * Unregister a previously registered callback due to the FeatureConnector disconnecting.
+ * <p>
+ * This does not need to be called if the callback was previously registered for a one
+ * shot result.
+ * @param cb The callback to unregister.
+ */
+ void unregisterFeatureCallback(IImsServiceFeatureCallback cb);
+
+ /**
+ * Associate this Manager instance with the IMS Binder interfaces specified. This is usually
+ * done by creating a FeatureConnection instance with these interfaces.
+ * @param container Contains all of the related interfaces attached to a specific ImsFeature.
+ */
+ void associate(ImsFeatureContainer container);
+
+ /**
+ * Invalidate the previously associated Binder interfaces set in {@link #associate}.
+ */
+ void invalidate();
+
+ /**
+ * Update the state of the remote ImsFeature associated with this Manager instance.
+ */
+ void updateFeatureState(@ImsFeature.ImsState int state);
+
+ /**
+ * Update the capabilities of the remove ImsFeature associated with this Manager instance.
+ */
+ void updateFeatureCapabilities(@ImsService.ImsServiceCapability long capabilities);
+} \ No newline at end of file
diff --git a/src/java/com/android/ims/ImsCall.java b/src/java/com/android/ims/ImsCall.java
index a31971d2..7af0b71c 100755
--- a/src/java/com/android/ims/ImsCall.java
+++ b/src/java/com/android/ims/ImsCall.java
@@ -16,9 +16,11 @@
package com.android.ims;
+import android.annotation.NonNull;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.Context;
import android.net.Uri;
+import android.os.Build;
import android.os.Bundle;
import android.os.Message;
import android.os.Parcel;
@@ -30,10 +32,12 @@ import android.telephony.ServiceState;
import android.telephony.TelephonyManager;
import android.telephony.ims.ImsCallProfile;
import android.telephony.ims.ImsCallSession;
+import android.telephony.ims.ImsCallSessionListener;
import android.telephony.ims.ImsConferenceState;
import android.telephony.ims.ImsReasonInfo;
import android.telephony.ims.ImsStreamMediaProfile;
import android.telephony.ims.ImsSuppServiceNotification;
+import android.telephony.ims.RtpHeaderExtension;
import android.text.TextUtils;
import android.util.Log;
@@ -46,9 +50,7 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
-import java.util.Map;
import java.util.Map.Entry;
-import java.util.Objects;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
@@ -93,11 +95,21 @@ public class ImsCall implements ICall {
*/
public static class Listener {
/**
- * Called when a request is sent out to initiate a new call
- * and 1xx response is received from the network.
+ * Called after the network first begins to establish the call session and is now connecting
+ * to the remote party.
* The default implementation calls {@link #onCallStateChanged}.
- *
- * @param call the call object that carries out the IMS call
+ * <p/>
+ * see: {@link ImsCallSessionListener#callSessionInitiating}
+ */
+ public void onCallInitiating(ImsCall call) {
+ onCallStateChanged(call);
+ }
+
+ /**
+ * Called after the network has contacted the remote party.
+ * The default implementation calls {@link #onCallStateChanged}.
+ * <p/>
+ * see: {@link ImsCallSessionListener#callSessionProgressing}
*/
public void onCallProgressing(ImsCall call) {
onCallStateChanged(call);
@@ -501,6 +513,14 @@ public class ImsCall implements ICall {
}
/**
+ * Reports a DTMF tone received from the network.
+ * @param imsCall The IMS call the tone was received from.
+ * @param digit The digit received.
+ */
+ public void onCallSessionDtmfReceived(ImsCall imsCall, char digit) {
+ }
+
+ /**
* Called when the call quality has changed.
*
* @param imsCall ImsCall object
@@ -508,6 +528,15 @@ public class ImsCall implements ICall {
*/
public void onCallQualityChanged(ImsCall imsCall, CallQuality callQuality) {
}
+
+ /**
+ * Called when RTP header extension data is received from the network.
+ * @param imsCall The ImsCall the data was received on.
+ * @param rtpHeaderExtensionData The RTP extension data received.
+ */
+ public void onCallSessionRtpHeaderExtensionsReceived(ImsCall imsCall,
+ @NonNull Set<RtpHeaderExtension> rtpHeaderExtensionData) {
+ }
}
// List of update operation for IMS call control
@@ -952,7 +981,7 @@ public class ImsCall implements ICall {
*
* @return {@code True} if the call is a multiparty call.
*/
- @UnsupportedAppUsage
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
public boolean isMultiparty() {
synchronized(mLockObj) {
if (mSession == null) {
@@ -1217,7 +1246,7 @@ public class ImsCall implements ICall {
* @param number number to be deflected to.
* @throws ImsException if the IMS service fails to deflect the call
*/
- @UnsupportedAppUsage
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
public void deflect(String number) throws ImsException {
logi("deflect :: session=" + mSession + ", number=" + Rlog.pii(TAG, number));
@@ -1243,7 +1272,7 @@ public class ImsCall implements ICall {
* @see Listener#onCallStartFailed
* @throws ImsException if the IMS service fails to reject the call
*/
- @UnsupportedAppUsage
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
public void reject(int reason) throws ImsException {
logi("reject :: reason=" + reason);
@@ -1328,7 +1357,7 @@ public class ImsCall implements ICall {
*
* @param reason reason code to terminate a call
*/
- @UnsupportedAppUsage
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
public void terminate(int reason) {
logi("terminate :: reason=" + reason);
@@ -1795,6 +1824,30 @@ public class ImsCall implements ICall {
}
}
+ /**
+ * Requests that RTP header extensions are added to the next RTP packet sent by the IMS stack.
+ * <p>
+ * The {@link RtpHeaderExtension#getLocalIdentifier()} local identifiers specified here must match
+ * agreed upon identifiers as indicated in
+ * {@link ImsCallProfile#getAcceptedRtpHeaderExtensionTypes()} for the current
+ * {@link #getCallProfile()}.
+ * <p>
+ * By specification, the RTP header extension is an unacknowledged transmission and there is no
+ * guarantee that the header extension will be delivered by the network to the other end of the
+ * call.
+ * @param rtpHeaderExtensions The RTP header extension(s) to be included in the next RTP
+ * packet.
+ */
+ public void sendRtpHeaderExtensions(@NonNull Set<RtpHeaderExtension> rtpHeaderExtensions) {
+ logi("sendRtpHeaderExtensions; extensionsSent=" + rtpHeaderExtensions.size());
+ synchronized(mLockObj) {
+ if (mSession == null) {
+ loge("sendRtpHeaderExtensions::no session");
+ }
+ mSession.sendRtpHeaderExtensions(rtpHeaderExtensions);
+ }
+ }
+
public void setAnswerWithRtt() {
mAnswerWithRtt = true;
}
@@ -1952,7 +2005,7 @@ public class ImsCall implements ICall {
", status=" + status +
", user=" + Rlog.pii(TAG, user) +
", displayName= " + Rlog.pii(TAG, displayName) +
- ", endpoint=" + endpoint);
+ ", endpoint=" + Rlog.pii(TAG, endpoint));
}
Uri handle = Uri.parse(user);
@@ -2394,6 +2447,32 @@ public class ImsCall implements ICall {
@VisibleForTesting
public class ImsCallSessionListenerProxy extends ImsCallSession.Listener {
@Override
+ public void callSessionInitiating(ImsCallSession session, ImsCallProfile profile) {
+ logi("callSessionInitiating :: session=" + session + " profile=" + profile);
+ if (isTransientConferenceSession(session)) {
+ // If it is a transient (conference) session, there is no action for this signal.
+ logi("callSessionInitiating :: not supported for transient conference session=" +
+ session);
+ return;
+ }
+
+ ImsCall.Listener listener;
+
+ synchronized(ImsCall.this) {
+ listener = mListener;
+ setCallProfile(profile);
+ }
+
+ if (listener != null) {
+ try {
+ listener.onCallInitiating(ImsCall.this);
+ } catch (Throwable t) {
+ loge("callSessionInitiating :: ", t);
+ }
+ }
+ }
+
+ @Override
public void callSessionProgressing(ImsCallSession session, ImsStreamMediaProfile profile) {
logi("callSessionProgressing :: session=" + session + " profile=" + profile);
@@ -2406,8 +2485,14 @@ public class ImsCall implements ICall {
ImsCall.Listener listener;
+ ImsCallProfile updatedProfile = session.getCallProfile();
synchronized(ImsCall.this) {
listener = mListener;
+ // The ImsCallProfile may have updated here (for example call state change). Query
+ // the potentially updated call profile to pick up these changes.
+ setCallProfile(updatedProfile);
+ // Apply the new mediaProfile on top of the Call Profile so it is not ignored in
+ // case the ImsService has not had a chance to update it yet.
mCallProfile.mMediaProfile.copyFrom(profile);
}
@@ -3341,6 +3426,23 @@ public class ImsCall implements ICall {
}
@Override
+ public void callSessionDtmfReceived(char digit) {
+ ImsCall.Listener listener;
+
+ synchronized(ImsCall.this) {
+ listener = mListener;
+ }
+
+ if (listener != null) {
+ try {
+ listener.onCallSessionDtmfReceived(ImsCall.this, digit);
+ } catch (Throwable t) {
+ loge("callSessionDtmfReceived:: ", t);
+ }
+ }
+ }
+
+ @Override
public void callQualityChanged(CallQuality callQuality) {
ImsCall.Listener listener;
@@ -3356,6 +3458,24 @@ public class ImsCall implements ICall {
}
}
}
+
+ @Override
+ public void callSessionRtpHeaderExtensionsReceived(
+ @NonNull Set<RtpHeaderExtension> extensions) {
+ ImsCall.Listener listener;
+
+ synchronized (ImsCall.this) {
+ listener = mListener;
+ }
+
+ if (listener != null) {
+ try {
+ listener.onCallSessionRtpHeaderExtensionsReceived(ImsCall.this, extensions);
+ } catch (Throwable t) {
+ loge("callSessionRtpHeaderExtensionsReceived:: ", t);
+ }
+ }
+ }
}
/**
@@ -3672,8 +3792,7 @@ public class ImsCall implements ICall {
* @param profile The current {@link ImsCallProfile} for the call.
*/
private void trackVideoStateHistory(ImsCallProfile profile) {
- mWasVideoCall = mWasVideoCall
- || profile != null ? profile.isVideoCall() : false;
+ mWasVideoCall = mWasVideoCall || ( profile != null && profile.isVideoCall());
}
/**
@@ -3741,6 +3860,24 @@ public class ImsCall implements ICall {
}
/**
+ * Determines if the current call is a cross sim call
+ * Note: This depends on the RIL exposing the
+ * {@link ImsCallProfile#EXTRA_IS_CROSS_SIM_CALL} extra.
+ *
+ * @return {@code true} if the call is Cross SIM, {@code false} otherwise.
+ */
+ public boolean isCrossSimCall() {
+ synchronized(mLockObj) {
+ if (mCallProfile == null) {
+ return false;
+ }
+ return mCallProfile.getCallExtraBoolean(
+ ImsCallProfile.EXTRA_IS_CROSS_SIM_CALL,
+ false);
+ }
+ }
+
+ /**
* Log a string to the radio buffer at the info level.
* @param s The message to log
*/
diff --git a/src/java/com/android/ims/ImsEcbm.java b/src/java/com/android/ims/ImsEcbm.java
index 13a59256..e0624f2f 100644
--- a/src/java/com/android/ims/ImsEcbm.java
+++ b/src/java/com/android/ims/ImsEcbm.java
@@ -30,6 +30,7 @@
package com.android.ims;
import android.compat.annotation.UnsupportedAppUsage;
+import android.os.Build;
import android.os.RemoteException;
import android.telephony.ims.ImsReasonInfo;
@@ -38,9 +39,7 @@ import com.android.ims.internal.IImsEcbmListener;
import com.android.telephony.Rlog;
/**
- * Provides APIs for the supplementary service settings using IMS (Ut interface).
- * It is created from 3GPP TS 24.623 (XCAP(XML Configuration Access Protocol)
- * over the Ut interface for manipulating supplementary services).
+ * Provides APIs for the modem to communicate the CDMA Emergency Callback Mode status for IMS.
*
* @hide
*/
@@ -55,16 +54,12 @@ public class ImsEcbm {
miEcbm = iEcbm;
}
- public void setEcbmStateListener(ImsEcbmStateListener ecbmListener) throws ImsException {
- try {
- miEcbm.setListener(new ImsEcbmListenerProxy(ecbmListener));
- } catch (RemoteException e) {
- throw new ImsException("setEcbmStateListener()", e,
- ImsReasonInfo.CODE_LOCAL_IMS_SERVICE_DOWN);
- }
+ public void setEcbmStateListener(ImsEcbmStateListener ecbmListener) throws RemoteException {
+ miEcbm.setListener(ecbmListener != null ?
+ new ImsEcbmListenerProxy(ecbmListener) : null);
}
- @UnsupportedAppUsage
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
public void exitEmergencyCallbackMode() throws ImsException {
try {
miEcbm.exitEmergencyCallbackMode();
@@ -81,8 +76,8 @@ public class ImsEcbm {
/**
* Adapter class for {@link IImsEcbmListener}.
*/
- private class ImsEcbmListenerProxy extends IImsEcbmListener.Stub {
- private ImsEcbmStateListener mListener;
+ private static class ImsEcbmListenerProxy extends IImsEcbmListener.Stub {
+ private final ImsEcbmStateListener mListener;
public ImsEcbmListenerProxy(ImsEcbmStateListener listener) {
mListener = listener;
diff --git a/src/java/com/android/ims/ImsFeatureBinderRepository.java b/src/java/com/android/ims/ImsFeatureBinderRepository.java
new file mode 100644
index 00000000..538e5cf1
--- /dev/null
+++ b/src/java/com/android/ims/ImsFeatureBinderRepository.java
@@ -0,0 +1,438 @@
+/*
+ * 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.ims;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.RemoteException;
+import android.telephony.ims.ImsService;
+import android.telephony.ims.feature.ImsFeature;
+import android.util.LocalLog;
+import android.util.Log;
+
+import com.android.ims.internal.IImsServiceFeatureCallback;
+import com.android.internal.annotations.GuardedBy;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.concurrent.Executor;
+import java.util.stream.Collectors;
+
+/**
+ * A repository of ImsFeature connections made available by an ImsService once it has been
+ * successfully bound.
+ *
+ * Provides the ability for listeners to register callbacks and the repository notify registered
+ * listeners when a connection has been created/removed for a specific connection type.
+ */
+public class ImsFeatureBinderRepository {
+
+ private static final String TAG = "ImsFeatureBinderRepo";
+
+ /**
+ * Internal class representing a listener that is listening for changes to specific
+ * ImsFeature instances.
+ */
+ private static class ListenerContainer {
+ private final IImsServiceFeatureCallback mCallback;
+ private final Executor mExecutor;
+
+ public ListenerContainer(@NonNull IImsServiceFeatureCallback c, @NonNull Executor e) {
+ mCallback = c;
+ mExecutor = e;
+ }
+
+ public void notifyFeatureCreatedOrRemoved(ImsFeatureContainer connector) {
+ if (connector == null) {
+ mExecutor.execute(() -> {
+ try {
+ mCallback.imsFeatureRemoved(
+ FeatureConnector.UNAVAILABLE_REASON_DISCONNECTED);
+ } catch (RemoteException e) {
+ // This listener will eventually be caught and removed during stale checks.
+ }
+ });
+ }
+ else {
+ mExecutor.execute(() -> {
+ try {
+ mCallback.imsFeatureCreated(connector);
+ } catch (RemoteException e) {
+ // This listener will eventually be caught and removed during stale checks.
+ }
+ });
+ }
+ }
+
+ public void notifyStateChanged(int state) {
+ mExecutor.execute(() -> {
+ try {
+ mCallback.imsStatusChanged(state);
+ } catch (RemoteException e) {
+ // This listener will eventually be caught and removed during stale checks.
+ }
+ });
+ }
+
+ public void notifyUpdateCapabilties(long caps) {
+ mExecutor.execute(() -> {
+ try {
+ mCallback.updateCapabilities(caps);
+ } catch (RemoteException e) {
+ // This listener will eventually be caught and removed during stale checks.
+ }
+ });
+ }
+
+ public boolean isStale() {
+ return !mCallback.asBinder().isBinderAlive();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ ListenerContainer that = (ListenerContainer) o;
+ // Do not count executor for equality.
+ return mCallback.equals(that.mCallback);
+ }
+
+ @Override
+ public int hashCode() {
+ // Do not use executor for hash.
+ return Objects.hash(mCallback);
+ }
+
+ @Override
+ public String toString() {
+ return "ListenerContainer{" + "cb=" + mCallback + '}';
+ }
+ }
+
+ /**
+ * Contains the mapping from ImsFeature type (MMTEL/RCS) to List of listeners listening for
+ * updates to the ImsFeature instance contained in the ImsFeatureContainer.
+ */
+ private static final class UpdateMapper {
+ public final int phoneId;
+ public final @ImsFeature.FeatureType int imsFeatureType;
+ private final List<ListenerContainer> mListeners = new ArrayList<>();
+ private ImsFeatureContainer mFeatureContainer;
+ private final Object mLock = new Object();
+
+
+ public UpdateMapper(int pId, @ImsFeature.FeatureType int t) {
+ phoneId = pId;
+ imsFeatureType = t;
+ }
+
+ public void addFeatureContainer(ImsFeatureContainer c) {
+ List<ListenerContainer> listeners;
+ synchronized (mLock) {
+ if (Objects.equals(c, mFeatureContainer)) return;
+ mFeatureContainer = c;
+ listeners = copyListenerList(mListeners);
+ }
+ listeners.forEach(l -> l.notifyFeatureCreatedOrRemoved(mFeatureContainer));
+ }
+
+ public ImsFeatureContainer removeFeatureContainer() {
+ ImsFeatureContainer oldContainer;
+ List<ListenerContainer> listeners;
+ synchronized (mLock) {
+ if (mFeatureContainer == null) return null;
+ oldContainer = mFeatureContainer;
+ mFeatureContainer = null;
+ listeners = copyListenerList(mListeners);
+ }
+ listeners.forEach(l -> l.notifyFeatureCreatedOrRemoved(mFeatureContainer));
+ return oldContainer;
+ }
+
+ public ImsFeatureContainer getFeatureContainer() {
+ synchronized(mLock) {
+ return mFeatureContainer;
+ }
+ }
+
+ public void addListener(ListenerContainer c) {
+ ImsFeatureContainer featureContainer;
+ synchronized (mLock) {
+ removeStaleListeners();
+ if (mListeners.contains(c)) {
+ return;
+ }
+ featureContainer = mFeatureContainer;
+ mListeners.add(c);
+ }
+ // Do not call back until the feature container has been set.
+ if (featureContainer != null) {
+ c.notifyFeatureCreatedOrRemoved(featureContainer);
+ }
+ }
+
+ public void removeListener(IImsServiceFeatureCallback callback) {
+ synchronized (mLock) {
+ removeStaleListeners();
+ List<ListenerContainer> oldListeners = mListeners.stream()
+ .filter((c) -> Objects.equals(c.mCallback, callback))
+ .collect(Collectors.toList());
+ mListeners.removeAll(oldListeners);
+ }
+ }
+
+ public void notifyStateUpdated(int newState) {
+ ImsFeatureContainer featureContainer;
+ List<ListenerContainer> listeners;
+ synchronized (mLock) {
+ removeStaleListeners();
+ featureContainer = mFeatureContainer;
+ listeners = copyListenerList(mListeners);
+ if (mFeatureContainer != null) {
+ if (mFeatureContainer.getState() != newState) {
+ mFeatureContainer.setState(newState);
+ }
+ }
+ }
+ // Only update if the feature container is set.
+ if (featureContainer != null) {
+ listeners.forEach(l -> l.notifyStateChanged(newState));
+ }
+ }
+
+ public void notifyUpdateCapabilities(long caps) {
+ ImsFeatureContainer featureContainer;
+ List<ListenerContainer> listeners;
+ synchronized (mLock) {
+ removeStaleListeners();
+ featureContainer = mFeatureContainer;
+ listeners = copyListenerList(mListeners);
+ if (mFeatureContainer != null) {
+ if (mFeatureContainer.getCapabilities() != caps) {
+ mFeatureContainer.setCapabilities(caps);
+ }
+ }
+ }
+ // Only update if the feature container is set.
+ if (featureContainer != null) {
+ listeners.forEach(l -> l.notifyUpdateCapabilties(caps));
+ }
+ }
+
+ @GuardedBy("mLock")
+ private void removeStaleListeners() {
+ List<ListenerContainer> staleListeners = mListeners.stream().filter(
+ ListenerContainer::isStale)
+ .collect(Collectors.toList());
+ mListeners.removeAll(staleListeners);
+ }
+
+ @Override
+ public String toString() {
+ synchronized (mLock) {
+ return "UpdateMapper{" + "phoneId=" + phoneId + ", type="
+ + ImsFeature.FEATURE_LOG_MAP.get(imsFeatureType) + ", container="
+ + mFeatureContainer + '}';
+ }
+ }
+
+
+ private List<ListenerContainer> copyListenerList(List<ListenerContainer> listeners) {
+ return new ArrayList<>(listeners);
+ }
+ }
+
+ private final List<UpdateMapper> mFeatures = new ArrayList<>();
+ private final LocalLog mLocalLog = new LocalLog(50 /*lines*/);
+
+ public ImsFeatureBinderRepository() {
+ logInfoLineLocked(-1, "FeatureConnectionRepository - created");
+ }
+
+ /**
+ * Get the Container for a specific ImsFeature now if it exists.
+ *
+ * @param phoneId The phone ID that the connection is related to.
+ * @param type The ImsFeature type to get the cotnainr for (MMTEL/RCS).
+ * @return The Container containing the requested ImsFeature if it exists.
+ */
+ public Optional<ImsFeatureContainer> getIfExists(
+ int phoneId, @ImsFeature.FeatureType int type) {
+ if (type < 0 || type >= ImsFeature.FEATURE_MAX) {
+ throw new IllegalArgumentException("Incorrect feature type");
+ }
+ UpdateMapper m;
+ m = getUpdateMapper(phoneId, type);
+ ImsFeatureContainer c = m.getFeatureContainer();
+ logVerboseLineLocked(phoneId, "getIfExists, type= " + ImsFeature.FEATURE_LOG_MAP.get(type)
+ + ", result= " + c);
+ return Optional.ofNullable(c);
+ }
+
+ /**
+ * Register a callback that will receive updates when the requested ImsFeature type becomes
+ * available or unavailable for the specified phone ID.
+ * <p>
+ * This callback will not be called the first time until there is a valid ImsFeature.
+ * @param phoneId The phone ID that the connection will be related to.
+ * @param type The ImsFeature type to get (MMTEL/RCS).
+ * @param callback The callback that will be used to notify when the callback is
+ * available/unavailable.
+ * @param executor The executor that the callback will be run on.
+ */
+ public void registerForConnectionUpdates(int phoneId,
+ @ImsFeature.FeatureType int type, @NonNull IImsServiceFeatureCallback callback,
+ @NonNull Executor executor) {
+ if (type < 0 || type >= ImsFeature.FEATURE_MAX || callback == null || executor == null) {
+ throw new IllegalArgumentException("One or more invalid arguments have been passed in");
+ }
+ ListenerContainer container = new ListenerContainer(callback, executor);
+ logInfoLineLocked(phoneId, "registerForConnectionUpdates, type= "
+ + ImsFeature.FEATURE_LOG_MAP.get(type) +", conn= " + container);
+ UpdateMapper m = getUpdateMapper(phoneId, type);
+ m.addListener(container);
+ }
+
+ /**
+ * Unregister for updates on a previously registered callback.
+ *
+ * @param callback The callback to unregister.
+ */
+ public void unregisterForConnectionUpdates(@NonNull IImsServiceFeatureCallback callback) {
+ if (callback == null) {
+ throw new IllegalArgumentException("this method does not accept null arguments");
+ }
+ logInfoLineLocked(-1, "unregisterForConnectionUpdates, callback= " + callback);
+ synchronized (mFeatures) {
+ for (UpdateMapper m : mFeatures) {
+ // warning: no callbacks should be called while holding locks
+ m.removeListener(callback);
+ }
+ }
+ }
+
+ /**
+ * Add a Container containing the IBinder interfaces associated with a specific ImsFeature type
+ * (MMTEL/RCS). If one already exists, it will be replaced. This will notify listeners of the
+ * change.
+ * @param phoneId The phone ID associated with this Container.
+ * @param type The ImsFeature type to get (MMTEL/RCS).
+ * @param newConnection A Container containing the IBinder interface connections associated with
+ * the ImsFeature type.
+ */
+ public void addConnection(int phoneId, @ImsFeature.FeatureType int type,
+ @Nullable ImsFeatureContainer newConnection) {
+ if (type < 0 || type >= ImsFeature.FEATURE_MAX) {
+ throw new IllegalArgumentException("The type must valid");
+ }
+ logInfoLineLocked(phoneId, "addConnection, type=" + ImsFeature.FEATURE_LOG_MAP.get(type)
+ + ", conn=" + newConnection);
+ UpdateMapper m = getUpdateMapper(phoneId, type);
+ m.addFeatureContainer(newConnection);
+ }
+
+ /**
+ * Remove the IBinder Container associated with a specific ImsService type. Listeners will be
+ * notified of this change.
+ * @param phoneId The phone ID associated with this connection.
+ * @param type The ImsFeature type to get (MMTEL/RCS).
+ */
+ public ImsFeatureContainer removeConnection(int phoneId, @ImsFeature.FeatureType int type) {
+ if (type < 0 || type >= ImsFeature.FEATURE_MAX) {
+ throw new IllegalArgumentException("The type must valid");
+ }
+ logInfoLineLocked(phoneId, "removeConnection, type="
+ + ImsFeature.FEATURE_LOG_MAP.get(type));
+ UpdateMapper m = getUpdateMapper(phoneId, type);
+ return m.removeFeatureContainer();
+ }
+
+ /**
+ * Notify listeners that the state of a specific ImsFeature that this repository is
+ * tracking has changed. Listeners will be notified of the change in the ImsFeature's state.
+ * @param phoneId The phoneId of the feature that has changed state.
+ * @param type The ImsFeature type to get (MMTEL/RCS).
+ * @param state The new state of the ImsFeature
+ */
+ public void notifyFeatureStateChanged(int phoneId, @ImsFeature.FeatureType int type,
+ @ImsFeature.ImsState int state) {
+ logInfoLineLocked(phoneId, "notifyFeatureStateChanged, type="
+ + ImsFeature.FEATURE_LOG_MAP.get(type) + ", state="
+ + ImsFeature.STATE_LOG_MAP.get(state));
+ UpdateMapper m = getUpdateMapper(phoneId, type);
+ m.notifyStateUpdated(state);
+ }
+
+ /**
+ * Notify listeners that the capabilities of a specific ImsFeature that this repository is
+ * tracking has changed. Listeners will be notified of the change in the ImsFeature's
+ * capabilities.
+ * @param phoneId The phoneId of the feature that has changed capabilities.
+ * @param type The ImsFeature type to get (MMTEL/RCS).
+ * @param capabilities The new capabilities of the ImsFeature
+ */
+ public void notifyFeatureCapabilitiesChanged(int phoneId, @ImsFeature.FeatureType int type,
+ @ImsService.ImsServiceCapability long capabilities) {
+ logInfoLineLocked(phoneId, "notifyFeatureCapabilitiesChanged, type="
+ + ImsFeature.FEATURE_LOG_MAP.get(type) + ", caps="
+ + ImsService.getCapabilitiesString(capabilities));
+ UpdateMapper m = getUpdateMapper(phoneId, type);
+ m.notifyUpdateCapabilities(capabilities);
+ }
+
+ /**
+ * Prints the dump of log events that have occurred on this repository.
+ */
+ public void dump(PrintWriter printWriter) {
+ synchronized (mLocalLog) {
+ mLocalLog.dump(printWriter);
+ }
+ }
+
+ private UpdateMapper getUpdateMapper(int phoneId, int type) {
+ synchronized (mFeatures) {
+ UpdateMapper mapper = mFeatures.stream()
+ .filter((c) -> ((c.phoneId == phoneId) && (c.imsFeatureType == type)))
+ .findFirst().orElse(null);
+ if (mapper == null) {
+ mapper = new UpdateMapper(phoneId, type);
+ mFeatures.add(mapper);
+ }
+ return mapper;
+ }
+ }
+
+ private void logVerboseLineLocked(int phoneId, String log) {
+ if (!Log.isLoggable(TAG, Log.VERBOSE)) return;
+ final String phoneIdPrefix = "[" + phoneId + "] ";
+ Log.v(TAG, phoneIdPrefix + log);
+ synchronized (mLocalLog) {
+ mLocalLog.log(phoneIdPrefix + log);
+ }
+ }
+
+ private void logInfoLineLocked(int phoneId, String log) {
+ final String phoneIdPrefix = "[" + phoneId + "] ";
+ Log.i(TAG, phoneIdPrefix + log);
+ synchronized (mLocalLog) {
+ mLocalLog.log(phoneIdPrefix + log);
+ }
+ }
+}
diff --git a/src/java/com/android/ims/ImsManager.java b/src/java/com/android/ims/ImsManager.java
index 9a436c30..76c98b09 100644
--- a/src/java/com/android/ims/ImsManager.java
+++ b/src/java/com/android/ims/ImsManager.java
@@ -16,22 +16,24 @@
package com.android.ims;
-import android.annotation.Nullable;
+import static android.telephony.ims.ProvisioningManager.KEY_VOIMS_OPT_IN_STATUS;
+
+import android.annotation.NonNull;
import android.app.PendingIntent;
import android.compat.annotation.UnsupportedAppUsage;
+import android.content.ContentResolver;
import android.content.Context;
import android.content.pm.PackageManager;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.Looper;
+import android.os.Build;
import android.os.Message;
-import android.os.Parcel;
import android.os.PersistableBundle;
import android.os.RemoteException;
+import android.os.ServiceSpecificException;
import android.os.SystemProperties;
import android.provider.Settings;
import android.telecom.TelecomManager;
import android.telephony.AccessNetworkConstants;
+import android.telephony.BinderCacheManager;
import android.telephony.CarrierConfigManager;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyFrameworkInitializer;
@@ -43,48 +45,52 @@ import android.telephony.ims.ImsReasonInfo;
import android.telephony.ims.ImsService;
import android.telephony.ims.ProvisioningManager;
import android.telephony.ims.RegistrationManager;
+import android.telephony.ims.RtpHeaderExtensionType;
import android.telephony.ims.aidl.IImsCapabilityCallback;
import android.telephony.ims.aidl.IImsConfig;
import android.telephony.ims.aidl.IImsConfigCallback;
+import android.telephony.ims.aidl.IImsMmTelFeature;
+import android.telephony.ims.aidl.IImsRegistration;
import android.telephony.ims.aidl.IImsRegistrationCallback;
import android.telephony.ims.aidl.IImsSmsListener;
+import android.telephony.ims.aidl.ISipTransport;
import android.telephony.ims.feature.CapabilityChangeRequest;
import android.telephony.ims.feature.ImsFeature;
import android.telephony.ims.feature.MmTelFeature;
import android.telephony.ims.stub.ImsCallSessionImplBase;
import android.telephony.ims.stub.ImsConfigImplBase;
import android.telephony.ims.stub.ImsRegistrationImplBase;
+import android.util.SparseArray;
import com.android.ims.internal.IImsCallSession;
-import com.android.ims.internal.IImsEcbm;
-import com.android.ims.internal.IImsMultiEndpoint;
-import com.android.ims.internal.IImsUt;
+import com.android.ims.internal.IImsServiceFeatureCallback;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.telephony.ITelephony;
-import com.android.internal.telephony.util.HandlerExecutor;
import com.android.telephony.Rlog;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayList;
-import java.util.HashMap;
+import java.util.Arrays;
import java.util.Set;
import java.util.concurrent.BlockingQueue;
-import java.util.concurrent.ConcurrentLinkedDeque;
-import java.util.concurrent.CopyOnWriteArraySet;
+import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
/**
- * Provides APIs for IMS services, such as initiating IMS calls, and provides access to
- * the operator's IMS network. This class is the starting point for any IMS actions.
- * You can acquire an instance of it with {@link #getInstance getInstance()}.</p>
+ * Provides APIs for MMTEL IMS services, such as initiating IMS calls, and provides access to
+ * the operator's IMS network. This class is the starting point for any IMS MMTEL actions.
+ * You can acquire an instance of it with {@link #getInstance getInstance()}.
+ * {Use {@link RcsFeatureManager} for RCS services}.
* For internal use ONLY! Use {@link ImsMmTelManager} instead.
* @hide
*/
-public class ImsManager implements IFeatureConnector {
+public class ImsManager implements FeatureUpdates {
/*
* Debug flag to override configuration flag
@@ -174,7 +180,7 @@ public class ImsManager implements IFeatureConnector {
* The value "true" indicates that the incoming call is for USSD.
* Internal use only.
* @deprecated Keeping around to not break old vendor components. Use
- * {@link MmTelFeature#EXTRA_USSD} instead.
+ * {@link MmTelFeature#EXTRA_IS_USSD} instead.
* @hide
*/
public static final String EXTRA_USSD = "android:ussd";
@@ -205,68 +211,248 @@ public class ImsManager implements IFeatureConnector {
private static final int RESPONSE_WAIT_TIME_MS = 3000;
+ private static final int[] LOCAL_IMS_CONFIG_KEYS = {
+ KEY_VOIMS_OPT_IN_STATUS
+ };
+
+ /**
+ * Create a Lazy Executor that is not instantiated for this instance unless it is used. This
+ * is to stop threads from being started on ImsManagers that are created to do simple tasks.
+ */
+ private static class LazyExecutor implements Executor {
+ private Executor mExecutor;
+
+ @Override
+ public void execute(Runnable runnable) {
+ startExecutorIfNeeded();
+ mExecutor.execute(runnable);
+ }
+
+ private synchronized void startExecutorIfNeeded() {
+ if (mExecutor != null) return;
+ mExecutor = Executors.newSingleThreadExecutor();
+ }
+ }
+
+ @VisibleForTesting
+ public interface MmTelFeatureConnectionFactory {
+ MmTelFeatureConnection create(Context context, int phoneId, IImsMmTelFeature feature,
+ IImsConfig c, IImsRegistration r, ISipTransport s);
+ }
+
@VisibleForTesting
- public interface ExecutorFactory {
- void executeRunnable(Runnable runnable);
+ public interface SettingsProxy {
+ /** @see Settings.Secure#getInt(ContentResolver, String, int) */
+ int getSecureIntSetting(ContentResolver cr, String name, int def);
+ /** @see Settings.Secure#putInt(ContentResolver, String, int) */
+ boolean putSecureIntSetting(ContentResolver cr, String name, int value);
}
@VisibleForTesting
- public static class ImsExecutorFactory implements ExecutorFactory {
+ public interface SubscriptionManagerProxy {
+ boolean isValidSubscriptionId(int subId);
+ int[] getSubscriptionIds(int slotIndex);
+ int getDefaultVoicePhoneId();
+ int getIntegerSubscriptionProperty(int subId, String propKey, int defValue);
+ void setSubscriptionProperty(int subId, String propKey, String propValue);
+ int[] getActiveSubscriptionIdList();
+ }
+
+ // Default implementations, which is mocked for testing
+ private static class DefaultSettingsProxy implements SettingsProxy {
+ @Override
+ public int getSecureIntSetting(ContentResolver cr, String name, int def) {
+ return Settings.Secure.getInt(cr, name, def);
+ }
+
+ @Override
+ public boolean putSecureIntSetting(ContentResolver cr, String name, int value) {
+ return Settings.Secure.putInt(cr, name, value);
+ }
+ }
+
+ // Default implementation which is mocked to make static dependency validation easier.
+ private static class DefaultSubscriptionManagerProxy implements SubscriptionManagerProxy {
+
+ private Context mContext;
+
+ public DefaultSubscriptionManagerProxy(Context context) {
+ mContext = context;
+ }
+
+ @Override
+ public boolean isValidSubscriptionId(int subId) {
+ return SubscriptionManager.isValidSubscriptionId(subId);
+ }
+
+ @Override
+ public int[] getSubscriptionIds(int slotIndex) {
+ return getSubscriptionManager().getSubscriptionIds(slotIndex);
+ }
+
+ @Override
+ public int getDefaultVoicePhoneId() {
+ return SubscriptionManager.getDefaultVoicePhoneId();
+ }
- private final HandlerThread mThreadHandler;
- private final Handler mHandler;
+ @Override
+ public int getIntegerSubscriptionProperty(int subId, String propKey, int defValue) {
+ return SubscriptionManager.getIntegerSubscriptionProperty(subId, propKey, defValue,
+ mContext);
+ }
- public ImsExecutorFactory() {
- mThreadHandler = new HandlerThread("ImsHandlerThread");
- mThreadHandler.start();
- mHandler = new Handler(mThreadHandler.getLooper());
+ @Override
+ public void setSubscriptionProperty(int subId, String propKey, String propValue) {
+ SubscriptionManager.setSubscriptionProperty(subId, propKey, propValue);
}
@Override
- public void executeRunnable(Runnable runnable) {
- mHandler.post(runnable);
+ public int[] getActiveSubscriptionIdList() {
+ return getSubscriptionManager().getActiveSubscriptionIdList();
}
- public void destroy() {
- mThreadHandler.quit();
+ private SubscriptionManager getSubscriptionManager() {
+ return mContext.getSystemService(SubscriptionManager.class);
}
}
- // Replaced with single-threaded executor for testing.
- @VisibleForTesting
- public ExecutorFactory mExecutorFactory = new ImsExecutorFactory();
+ /**
+ * Events that will be triggered as part of metrics collection.
+ */
+ public interface ImsStatsCallback {
+ /**
+ * The MmTel capabilities that are enabled have changed.
+ * @param capability The MmTel capability
+ * @param regTech The IMS registration technology associated with the capability.
+ * @param isEnabled {@code true} if the capability is enabled, {@code false} if it is
+ * disabled.
+ */
+ void onEnabledMmTelCapabilitiesChanged(
+ @MmTelFeature.MmTelCapabilities.MmTelCapability int capability,
+ @ImsRegistrationImplBase.ImsRegistrationTech int regTech,
+ boolean isEnabled);
+ }
+
+ /**
+ * Internally we will create a FeatureConnector when {@link #getInstance(Context, int)} is
+ * called to keep the MmTelFeatureConnection instance fresh as new SIM cards are
+ * inserted/removed and MmTelFeature potentially changes.
+ * <p>
+ * For efficiency purposes, there is only one ImsManager created per-slot when using
+ * {@link #getInstance(Context, int)} and the same instance is returned for multiple callers.
+ * This is due to the ImsManager being a potentially heavyweight object depending on what it is
+ * being used for.
+ */
+ private static class InstanceManager implements FeatureConnector.Listener<ImsManager> {
+ // If this is the first time connecting, wait a small amount of time in case IMS has already
+ // connected. Otherwise, ImsManager will become ready when the ImsService is connected.
+ private static final int CONNECT_TIMEOUT_MS = 50;
+
+ private final FeatureConnector<ImsManager> mConnector;
+ private final ImsManager mImsManager;
+
+ private final Object mLock = new Object();
+ private boolean isConnectorActive = false;
+ private CountDownLatch mConnectedLatch;
+
+ public InstanceManager(ImsManager manager) {
+ mImsManager = manager;
+ // Set a special prefix so that logs generated by getInstance are distinguishable.
+ mImsManager.mLogTagPostfix = "IM";
+
+ ArrayList<Integer> readyFilter = new ArrayList<>();
+ readyFilter.add(ImsFeature.STATE_READY);
+ readyFilter.add(ImsFeature.STATE_INITIALIZING);
+ readyFilter.add(ImsFeature.STATE_UNAVAILABLE);
+ // Pass a reference of the ImsManager being managed into the connector, allowing it to
+ // update the internal MmTelFeatureConnection as it is being updated.
+ mConnector = new FeatureConnector<>(manager.mContext, manager.mPhoneId,
+ (c,p) -> mImsManager, "InstanceManager", readyFilter, this,
+ manager.getImsThreadExecutor());
+ }
+
+ public ImsManager getInstance() {
+ return mImsManager;
+ }
+
+ public void reconnect() {
+ boolean requiresReconnect = false;
+ synchronized (mLock) {
+ if (!isConnectorActive) {
+ requiresReconnect = true;
+ isConnectorActive = true;
+ mConnectedLatch = new CountDownLatch(1);
+ }
+ }
+ if (requiresReconnect) {
+ mConnector.connect();
+ }
+ try {
+ // If this is during initial reconnect, let all threads wait for connect
+ // (or timeout)
+ mConnectedLatch.await(CONNECT_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+ } catch (InterruptedException e) {
+ // Do nothing and allow ImsService to attach behind the scenes
+ }
+ }
+
+ @Override
+ public void connectionReady(ImsManager manager) {
+ synchronized (mLock) {
+ mConnectedLatch.countDown();
+ }
+ }
+
+ @Override
+ public void connectionUnavailable(int reason) {
+ synchronized (mLock) {
+ // only need to track the connection becoming unavailable due to telephony going
+ // down.
+ if (reason == FeatureConnector.UNAVAILABLE_REASON_SERVER_UNAVAILABLE) {
+ isConnectorActive = false;
+ }
+ mConnectedLatch.countDown();
+ }
+
+ }
+ }
- private static HashMap<Integer, ImsManager> sImsManagerInstances =
- new HashMap<Integer, ImsManager>();
+ // Replaced with single-threaded executor for testing.
+ private final Executor mExecutor;
+ // Replaced With mock for testing
+ private MmTelFeatureConnectionFactory mMmTelFeatureConnectionFactory =
+ MmTelFeatureConnection::new;
+ private final SubscriptionManagerProxy mSubscriptionManagerProxy;
+ private final SettingsProxy mSettingsProxy;
private Context mContext;
private CarrierConfigManager mConfigManager;
private int mPhoneId;
- private @Nullable MmTelFeatureConnection mMmTelFeatureConnection = null;
+ private AtomicReference<MmTelFeatureConnection> mMmTelConnectionRef = new AtomicReference<>();
+ // Used for debug purposes only currently
private boolean mConfigUpdated = false;
-
+ private BinderCacheManager<ITelephony> mBinderCache;
private ImsConfigListener mImsConfigListener;
- //TODO: Move these caches into the MmTelFeature Connection and restrict their lifetimes to the
- // lifetime of the MmTelFeature.
- // Ut interface for the supplementary service configuration
- private ImsUt mUt = null;
- // ECBM interface
- private ImsEcbm mEcbm = null;
- private ImsMultiEndpoint mMultiEndpoint = null;
-
- private Set<FeatureConnection.IFeatureUpdate> mStatusCallbacks = new CopyOnWriteArraySet<>();
-
public static final String TRUE = "true";
public static final String FALSE = "false";
-
- // mRecentDisconnectReasons stores the last 16 disconnect reasons
- private static final int MAX_RECENT_DISCONNECT_REASONS = 16;
- private ConcurrentLinkedDeque<ImsReasonInfo> mRecentDisconnectReasons =
- new ConcurrentLinkedDeque<>();
-
- /**
- * Gets a manager instance.
+ // Map of phoneId -> InstanceManager
+ private static final SparseArray<InstanceManager> IMS_MANAGER_INSTANCES = new SparseArray<>(2);
+ // Map of phoneId -> ImsStatsCallback
+ private static final SparseArray<ImsStatsCallback> IMS_STATS_CALLBACKS = new SparseArray<>(2);
+
+ // A log prefix added to some instances of ImsManager to make it distinguishable from others.
+ // - "IM" added to ImsManager for ImsManagers created using getInstance.
+ private String mLogTagPostfix = "";
+
+ /**
+ * Gets a manager instance and blocks for a limited period of time, connecting to the
+ * corresponding ImsService MmTelFeature if it exists.
+ * <p>
+ * If the ImsService is unavailable or becomes unavailable, the associated methods will fail and
+ * a new ImsManager will need to be requested. Instead, a {@link FeatureConnector} can be
+ * requested using {@link #getConnector}, which will notify the caller when a new ImsManager is
+ * available.
*
* @param context application context for creating the manager object
* @param phoneId the phone ID for the IMS Service
@@ -274,21 +460,41 @@ public class ImsManager implements IFeatureConnector {
*/
@UnsupportedAppUsage
public static ImsManager getInstance(Context context, int phoneId) {
- synchronized (sImsManagerInstances) {
- if (sImsManagerInstances.containsKey(phoneId)) {
- ImsManager m = sImsManagerInstances.get(phoneId);
- // May be null for some tests
- if (m != null) {
- m.connectIfServiceIsAvailable();
- }
- return m;
+ InstanceManager instanceManager;
+ synchronized (IMS_MANAGER_INSTANCES) {
+ instanceManager = IMS_MANAGER_INSTANCES.get(phoneId);
+ if (instanceManager == null) {
+ ImsManager m = new ImsManager(context, phoneId);
+ instanceManager = new InstanceManager(m);
+ IMS_MANAGER_INSTANCES.put(phoneId, instanceManager);
}
-
- ImsManager mgr = new ImsManager(context, phoneId);
- sImsManagerInstances.put(phoneId, mgr);
-
- return mgr;
}
+ // If the ImsManager became disconnected for some reason, try to reconnect it now.
+ instanceManager.reconnect();
+ return instanceManager.getInstance();
+ }
+
+ /**
+ * Retrieve an FeatureConnector for ImsManager, which allows a Listener to listen for when
+ * the ImsManager becomes available or unavailable due to the ImsService MmTelFeature moving to
+ * the READY state or destroyed on a specific phone modem index.
+ *
+ * @param context The Context that will be used to connect the ImsManager.
+ * @param phoneId The modem phone ID that the ImsManager will be created for.
+ * @param logPrefix The log prefix used for debugging purposes.
+ * @param listener The Listener that will deliver ImsManager updates as it becomes available.
+ * @param executor The Executor that the Listener callbacks will be called on.
+ * @return A FeatureConnector instance for generating ImsManagers as the associated
+ * MmTelFeatures become available.
+ */
+ public static FeatureConnector<ImsManager> getConnector(Context context,
+ int phoneId, String logPrefix, FeatureConnector.Listener<ImsManager> listener,
+ Executor executor) {
+ // Only listen for the READY state from the MmTelFeature here.
+ ArrayList<Integer> readyFilter = new ArrayList<>();
+ readyFilter.add(ImsFeature.STATE_READY);
+ return new FeatureConnector<>(context, phoneId, ImsManager::new, logPrefix, readyFilter,
+ listener, executor);
}
public static boolean isImsSupportedOnDevice(Context context) {
@@ -296,15 +502,40 @@ public class ImsManager implements IFeatureConnector {
}
/**
+ * Sets the callback that will be called when events related to IMS metric collection occur.
+ * <p>
+ * Note: Subsequent calls to this method will replace the previous stats callback.
+ */
+ public static void setImsStatsCallback(int phoneId, ImsStatsCallback cb) {
+ synchronized (IMS_STATS_CALLBACKS) {
+ if (cb == null) {
+ IMS_STATS_CALLBACKS.remove(phoneId);
+ } else {
+ IMS_STATS_CALLBACKS.put(phoneId, cb);
+ }
+ }
+ }
+
+ /**
+ * @return the {@link ImsStatsCallback} instance associated with the provided phoneId or
+ * {@link null} if none currently exists.
+ */
+ private static ImsStatsCallback getStatsCallback(int phoneId) {
+ synchronized (IMS_STATS_CALLBACKS) {
+ return IMS_STATS_CALLBACKS.get(phoneId);
+ }
+ }
+
+ /**
* Returns the user configuration of Enhanced 4G LTE Mode setting.
*
* @deprecated Doesn't support MSIM devices. Use
* {@link #isEnhanced4gLteModeSettingEnabledByUser()} instead.
*/
- @UnsupportedAppUsage
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
public static boolean isEnhanced4gLteModeSettingEnabledByUser(Context context) {
- ImsManager mgr = ImsManager.getInstance(context,
- SubscriptionManager.getDefaultVoicePhoneId());
+ DefaultSubscriptionManagerProxy p = new DefaultSubscriptionManagerProxy(context);
+ ImsManager mgr = ImsManager.getInstance(context, p.getDefaultVoicePhoneId());
if (mgr != null) {
return mgr.isEnhanced4gLteModeSettingEnabledByUser();
}
@@ -316,25 +547,28 @@ public class ImsManager implements IFeatureConnector {
/**
* Returns the user configuration of Enhanced 4G LTE Mode setting for slot. If the option is
* not editable ({@link CarrierConfigManager#KEY_EDITABLE_ENHANCED_4G_LTE_BOOL} is false),
- * hidden ({@link CarrierConfigManager#KEY_HIDE_ENHANCED_4G_LTE_BOOL} is true), or
- * the setting is not initialized, this method will return default value specified by
- * {@link CarrierConfigManager#KEY_ENHANCED_4G_LTE_ON_BY_DEFAULT_BOOL}.
+ * hidden ({@link CarrierConfigManager#KEY_HIDE_ENHANCED_4G_LTE_BOOL} is true), the setting is
+ * not initialized, and VoIMS opt-in status disabled, this method will return default value
+ * specified by {@link CarrierConfigManager#KEY_ENHANCED_4G_LTE_ON_BY_DEFAULT_BOOL}.
*
* Note that even if the setting was set, it may no longer be editable. If this is the case we
* return the default value.
*/
public boolean isEnhanced4gLteModeSettingEnabledByUser() {
- int setting = SubscriptionManager.getIntegerSubscriptionProperty(
+ int setting = mSubscriptionManagerProxy.getIntegerSubscriptionProperty(
getSubId(), SubscriptionManager.ENHANCED_4G_MODE_ENABLED,
- SUB_PROPERTY_NOT_INITIALIZED, mContext);
+ SUB_PROPERTY_NOT_INITIALIZED);
boolean onByDefault = getBooleanCarrierConfig(
CarrierConfigManager.KEY_ENHANCED_4G_LTE_ON_BY_DEFAULT_BOOL);
-
- // If Enhanced 4G LTE Mode is uneditable, hidden or not initialized, we use the default
- // value
- if (!getBooleanCarrierConfig(CarrierConfigManager.KEY_EDITABLE_ENHANCED_4G_LTE_BOOL)
- || getBooleanCarrierConfig(CarrierConfigManager.KEY_HIDE_ENHANCED_4G_LTE_BOOL)
- || setting == SUB_PROPERTY_NOT_INITIALIZED) {
+ boolean isUiUnEditable =
+ !getBooleanCarrierConfig(CarrierConfigManager.KEY_EDITABLE_ENHANCED_4G_LTE_BOOL)
+ || getBooleanCarrierConfig(CarrierConfigManager.KEY_HIDE_ENHANCED_4G_LTE_BOOL);
+ boolean isSettingNotInitialized = setting == SUB_PROPERTY_NOT_INITIALIZED;
+
+ // If Enhanced 4G LTE Mode is uneditable, hidden, not initialized and VoIMS opt-in disabled
+ // we use the default value. If VoIMS opt-in is enabled, we will always allow the user to
+ // change the IMS enabled setting.
+ if ((isUiUnEditable || isSettingNotInitialized) && !isVoImsOptInEnabled()) {
return onByDefault;
} else {
return (setting == ProvisioningManager.PROVISIONING_VALUE_ENABLED);
@@ -348,8 +582,8 @@ public class ImsManager implements IFeatureConnector {
* instead.
*/
public static void setEnhanced4gLteModeSetting(Context context, boolean enabled) {
- ImsManager mgr = ImsManager.getInstance(context,
- SubscriptionManager.getDefaultVoicePhoneId());
+ DefaultSubscriptionManagerProxy p = new DefaultSubscriptionManagerProxy(context);
+ ImsManager mgr = ImsManager.getInstance(context, p.getDefaultVoicePhoneId());
if (mgr != null) {
mgr.setEnhanced4gLteModeSetting(enabled);
}
@@ -358,9 +592,9 @@ public class ImsManager implements IFeatureConnector {
/**
* Change persistent Enhanced 4G LTE Mode setting. If the option is not editable
- * ({@link CarrierConfigManager#KEY_EDITABLE_ENHANCED_4G_LTE_BOOL} is false)
- * or hidden ({@link CarrierConfigManager#KEY_HIDE_ENHANCED_4G_LTE_BOOL} is true),
- * this method will set the setting to the default value specified by
+ * ({@link CarrierConfigManager#KEY_EDITABLE_ENHANCED_4G_LTE_BOOL} is false),
+ * hidden ({@link CarrierConfigManager#KEY_HIDE_ENHANCED_4G_LTE_BOOL} is true), and VoIMS opt-in
+ * status disabled, this method will set the setting to the default value specified by
* {@link CarrierConfigManager#KEY_ENHANCED_4G_LTE_ON_BY_DEFAULT_BOOL}.
*/
public void setEnhanced4gLteModeSetting(boolean enabled) {
@@ -369,35 +603,50 @@ public class ImsManager implements IFeatureConnector {
return;
}
int subId = getSubId();
+ if (!isSubIdValid(subId)) {
+ loge("setEnhanced4gLteModeSetting: invalid sub id, can not set property in " +
+ " siminfo db; subId=" + subId);
+ return;
+ }
// If editable=false or hidden=true, we must keep default advanced 4G mode.
- if (!getBooleanCarrierConfig(CarrierConfigManager.KEY_EDITABLE_ENHANCED_4G_LTE_BOOL) ||
- getBooleanCarrierConfig(CarrierConfigManager.KEY_HIDE_ENHANCED_4G_LTE_BOOL)) {
+ boolean isUiUnEditable =
+ !getBooleanCarrierConfig(CarrierConfigManager.KEY_EDITABLE_ENHANCED_4G_LTE_BOOL) ||
+ getBooleanCarrierConfig(CarrierConfigManager.KEY_HIDE_ENHANCED_4G_LTE_BOOL);
+
+ // If VoIMS opt-in is enabled, we will always allow the user to change the IMS enabled
+ // setting.
+ if (isUiUnEditable && !isVoImsOptInEnabled()) {
enabled = getBooleanCarrierConfig(
CarrierConfigManager.KEY_ENHANCED_4G_LTE_ON_BY_DEFAULT_BOOL);
}
- int prevSetting = SubscriptionManager.getIntegerSubscriptionProperty(subId,
- SubscriptionManager.ENHANCED_4G_MODE_ENABLED, SUB_PROPERTY_NOT_INITIALIZED,
- mContext);
+ int prevSetting = mSubscriptionManagerProxy.getIntegerSubscriptionProperty(subId,
+ SubscriptionManager.ENHANCED_4G_MODE_ENABLED, SUB_PROPERTY_NOT_INITIALIZED);
- if (prevSetting != (enabled ?
- ProvisioningManager.PROVISIONING_VALUE_ENABLED :
+ if (prevSetting == (enabled ? ProvisioningManager.PROVISIONING_VALUE_ENABLED :
ProvisioningManager.PROVISIONING_VALUE_DISABLED)) {
- if (isSubIdValid(subId)) {
- SubscriptionManager.setSubscriptionProperty(subId,
- SubscriptionManager.ENHANCED_4G_MODE_ENABLED,
- booleanToPropertyString(enabled));
+ // No change in setting.
+ return;
+ }
+ mSubscriptionManagerProxy.setSubscriptionProperty(subId,
+ SubscriptionManager.ENHANCED_4G_MODE_ENABLED,
+ booleanToPropertyString(enabled));
+ try {
+ if (enabled) {
+ CapabilityChangeRequest request = new CapabilityChangeRequest();
+ boolean isNonTty = isNonTtyOrTtyOnVolteEnabled();
+ // This affects voice and video enablement
+ updateVoiceCellFeatureValue(request, isNonTty);
+ updateVideoCallFeatureValue(request, isNonTty);
+ changeMmTelCapability(request);
+ // Ensure IMS is on if this setting is enabled.
+ turnOnIms();
} else {
- loge("setEnhanced4gLteModeSetting: invalid sub id, can not set property in " +
- " siminfo db; subId=" + subId);
- }
- if (isNonTtyOrTtyOnVolteEnabled()) {
- try {
- setAdvanced4GMode(enabled);
- } catch (ImsException ie) {
- // do nothing
- }
+ // This may trigger entire IMS interface to be disabled, so recalculate full state.
+ reevaluateCapabilities();
}
+ } catch (ImsException e) {
+ loge("setEnhanced4gLteModeSetting couldn't set config: " + e);
}
}
@@ -407,10 +656,10 @@ public class ImsManager implements IFeatureConnector {
* @deprecated Does not support MSIM devices. Please use
* {@link #isNonTtyOrTtyOnVolteEnabled()} instead.
*/
- @UnsupportedAppUsage
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
public static boolean isNonTtyOrTtyOnVolteEnabled(Context context) {
- ImsManager mgr = ImsManager.getInstance(context,
- SubscriptionManager.getDefaultVoicePhoneId());
+ DefaultSubscriptionManagerProxy p = new DefaultSubscriptionManagerProxy(context);
+ ImsManager mgr = ImsManager.getInstance(context, p.getDefaultVoicePhoneId());
if (mgr != null) {
return mgr.isNonTtyOrTtyOnVolteEnabled();
}
@@ -444,10 +693,10 @@ public class ImsManager implements IFeatureConnector {
* @deprecated Does not support MSIM devices. Please use
* {@link #isVolteEnabledByPlatform()} instead.
*/
- @UnsupportedAppUsage
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
public static boolean isVolteEnabledByPlatform(Context context) {
- ImsManager mgr = ImsManager.getInstance(context,
- SubscriptionManager.getDefaultVoicePhoneId());
+ DefaultSubscriptionManagerProxy p = new DefaultSubscriptionManagerProxy(context);
+ ImsManager mgr = ImsManager.getInstance(context, p.getDefaultVoicePhoneId());
if (mgr != null) {
return mgr.isVolteEnabledByPlatform();
}
@@ -460,8 +709,9 @@ public class ImsManager implements IFeatureConnector {
* supported.
*/
public void isSupported(int capability, int transportType, Consumer<Boolean> result) {
- mExecutorFactory.executeRunnable(() -> {
+ getImsThreadExecutor().execute(() -> {
switch(transportType) {
+ // Does not take into account NR, as NR is a superset of LTE support currently.
case (AccessNetworkConstants.TRANSPORT_TYPE_WWAN): {
switch (capability) {
case (MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VOICE): {
@@ -523,6 +773,11 @@ public class ImsManager implements IFeatureConnector {
return true;
}
+ if (getLocalImsConfigKeyInt(KEY_VOIMS_OPT_IN_STATUS)
+ == ProvisioningManager.PROVISIONING_VALUE_ENABLED) {
+ return true;
+ }
+
return mContext.getResources().getBoolean(
com.android.internal.R.bool.config_device_volte_available)
&& getBooleanCarrierConfig(CarrierConfigManager.KEY_CARRIER_VOLTE_AVAILABLE_BOOL)
@@ -530,14 +785,27 @@ public class ImsManager implements IFeatureConnector {
}
/**
+ * @return {@code true} if IMS over NR is enabled by the platform, {@code false} otherwise.
+ */
+ public boolean isImsOverNrEnabledByPlatform() {
+ int[] nrCarrierCaps = getIntArrayCarrierConfig(
+ CarrierConfigManager.KEY_CARRIER_NR_AVAILABILITIES_INT_ARRAY);
+ if (nrCarrierCaps == null) return false;
+ boolean voNrCarrierSupported = Arrays.stream(nrCarrierCaps)
+ .anyMatch(cap -> cap == CarrierConfigManager.CARRIER_NR_AVAILABILITY_SA);
+ if (!voNrCarrierSupported) return false;
+ return isGbaValid();
+ }
+
+ /**
* Indicates whether VoLTE is provisioned on device.
*
* @deprecated Does not support MSIM devices. Please use
* {@link #isVolteProvisionedOnDevice()} instead.
*/
public static boolean isVolteProvisionedOnDevice(Context context) {
- ImsManager mgr = ImsManager.getInstance(context,
- SubscriptionManager.getDefaultVoicePhoneId());
+ DefaultSubscriptionManagerProxy p = new DefaultSubscriptionManagerProxy(context);
+ ImsManager mgr = ImsManager.getInstance(context, p.getDefaultVoicePhoneId());
if (mgr != null) {
return mgr.isVolteProvisionedOnDevice();
}
@@ -579,8 +847,8 @@ public class ImsManager implements IFeatureConnector {
* {@link #isWfcProvisionedOnDevice()} instead.
*/
public static boolean isWfcProvisionedOnDevice(Context context) {
- ImsManager mgr = ImsManager.getInstance(context,
- SubscriptionManager.getDefaultVoicePhoneId());
+ DefaultSubscriptionManagerProxy p = new DefaultSubscriptionManagerProxy(context);
+ ImsManager mgr = ImsManager.getInstance(context, p.getDefaultVoicePhoneId());
if (mgr != null) {
return mgr.isWfcProvisionedOnDevice();
}
@@ -617,8 +885,8 @@ public class ImsManager implements IFeatureConnector {
* {@link #isVtProvisionedOnDevice()} instead.
*/
public static boolean isVtProvisionedOnDevice(Context context) {
- ImsManager mgr = ImsManager.getInstance(context,
- SubscriptionManager.getDefaultVoicePhoneId());
+ DefaultSubscriptionManagerProxy p = new DefaultSubscriptionManagerProxy(context);
+ ImsManager mgr = ImsManager.getInstance(context, p.getDefaultVoicePhoneId());
if (mgr != null) {
return mgr.isVtProvisionedOnDevice();
}
@@ -648,8 +916,8 @@ public class ImsManager implements IFeatureConnector {
* {@link #isVtEnabledByPlatform()} instead.
*/
public static boolean isVtEnabledByPlatform(Context context) {
- ImsManager mgr = ImsManager.getInstance(context,
- SubscriptionManager.getDefaultVoicePhoneId());
+ DefaultSubscriptionManagerProxy p = new DefaultSubscriptionManagerProxy(context);
+ ImsManager mgr = ImsManager.getInstance(context, p.getDefaultVoicePhoneId());
if (mgr != null) {
return mgr.isVtEnabledByPlatform();
}
@@ -685,8 +953,8 @@ public class ImsManager implements IFeatureConnector {
* {@link #isVtEnabledByUser()} instead.
*/
public static boolean isVtEnabledByUser(Context context) {
- ImsManager mgr = ImsManager.getInstance(context,
- SubscriptionManager.getDefaultVoicePhoneId());
+ DefaultSubscriptionManagerProxy p = new DefaultSubscriptionManagerProxy(context);
+ ImsManager mgr = ImsManager.getInstance(context, p.getDefaultVoicePhoneId());
if (mgr != null) {
return mgr.isVtEnabledByUser();
}
@@ -699,9 +967,9 @@ public class ImsManager implements IFeatureConnector {
* returns true as default value.
*/
public boolean isVtEnabledByUser() {
- int setting = SubscriptionManager.getIntegerSubscriptionProperty(
+ int setting = mSubscriptionManagerProxy.getIntegerSubscriptionProperty(
getSubId(), SubscriptionManager.VT_IMS_ENABLED,
- SUB_PROPERTY_NOT_INITIALIZED, mContext);
+ SUB_PROPERTY_NOT_INITIALIZED);
// If it's never set, by default we return true.
return (setting == SUB_PROPERTY_NOT_INITIALIZED
@@ -709,13 +977,25 @@ public class ImsManager implements IFeatureConnector {
}
/**
+ * Returns whether the user sets call composer setting per sub.
+ */
+ public boolean isCallComposerEnabledByUser() {
+ TelephonyManager tm = mContext.getSystemService(TelephonyManager.class);
+ if (tm == null) {
+ loge("isCallComposerEnabledByUser: TelephonyManager is null, returning false");
+ return false;
+ }
+ return tm.getCallComposerStatus() == TelephonyManager.CALL_COMPOSER_STATUS_ON;
+ }
+
+ /**
* Change persistent VT enabled setting
*
* @deprecated Does not support MSIM devices. Please use {@link #setVtSetting(boolean)} instead.
*/
public static void setVtSetting(Context context, boolean enabled) {
- ImsManager mgr = ImsManager.getInstance(context,
- SubscriptionManager.getDefaultVoicePhoneId());
+ DefaultSubscriptionManagerProxy p = new DefaultSubscriptionManagerProxy(context);
+ ImsManager mgr = ImsManager.getInstance(context, p.getDefaultVoicePhoneId());
if (mgr != null) {
mgr.setVtSetting(enabled);
}
@@ -732,26 +1012,23 @@ public class ImsManager implements IFeatureConnector {
}
int subId = getSubId();
- if (isSubIdValid(subId)) {
- SubscriptionManager.setSubscriptionProperty(subId, SubscriptionManager.VT_IMS_ENABLED,
- booleanToPropertyString(enabled));
- } else {
+ if (!isSubIdValid(subId)) {
loge("setVtSetting: sub id invalid, skip modifying vt state in subinfo db; subId="
+ subId);
+ return;
}
-
+ mSubscriptionManagerProxy.setSubscriptionProperty(subId, SubscriptionManager.VT_IMS_ENABLED,
+ booleanToPropertyString(enabled));
try {
- changeMmTelCapability(MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VIDEO,
- ImsRegistrationImplBase.REGISTRATION_TECH_LTE, enabled);
-
if (enabled) {
- log("setVtSetting(b) : turnOnIms");
+ CapabilityChangeRequest request = new CapabilityChangeRequest();
+ updateVideoCallFeatureValue(request, isNonTtyOrTtyOnVolteEnabled());
+ changeMmTelCapability(request);
+ // ensure IMS is enabled.
turnOnIms();
- } else if (isTurnOffImsAllowedByPlatform()
- && (!isVolteEnabledByPlatform()
- || !isEnhanced4gLteModeSettingEnabledByUser())) {
- log("setVtSetting(b) : imsServiceAllowTurnOff -> turnOffIms");
- turnOffIms();
+ } else {
+ // This may cause IMS to be disabled, re-evaluate all.
+ reevaluateCapabilities();
}
} catch (ImsException e) {
// The ImsService is down. Since the SubscriptionManager already recorded the user's
@@ -764,23 +1041,6 @@ public class ImsManager implements IFeatureConnector {
/**
* Returns whether turning off ims is allowed by platform.
* The platform property may override the carrier config.
- *
- * @deprecated Does not support MSIM devices. Please use
- * {@link #isTurnOffImsAllowedByPlatform()} instead.
- */
- private static boolean isTurnOffImsAllowedByPlatform(Context context) {
- ImsManager mgr = ImsManager.getInstance(context,
- SubscriptionManager.getDefaultVoicePhoneId());
- if (mgr != null) {
- return mgr.isTurnOffImsAllowedByPlatform();
- }
- Rlog.e(TAG, "isTurnOffImsAllowedByPlatform: ImsManager null, returning default value.");
- return true;
- }
-
- /**
- * Returns whether turning off ims is allowed by platform.
- * The platform property may override the carrier config.
*/
private boolean isTurnOffImsAllowedByPlatform() {
// We first read the per slot value. If doesn't exist, we read the general value. If still
@@ -803,8 +1063,8 @@ public class ImsManager implements IFeatureConnector {
* {@link #isWfcEnabledByUser()} instead.
*/
public static boolean isWfcEnabledByUser(Context context) {
- ImsManager mgr = ImsManager.getInstance(context,
- SubscriptionManager.getDefaultVoicePhoneId());
+ DefaultSubscriptionManagerProxy p = new DefaultSubscriptionManagerProxy(context);
+ ImsManager mgr = ImsManager.getInstance(context, p.getDefaultVoicePhoneId());
if (mgr != null) {
return mgr.isWfcEnabledByUser();
}
@@ -817,9 +1077,9 @@ public class ImsManager implements IFeatureConnector {
* queries CarrierConfig value as default.
*/
public boolean isWfcEnabledByUser() {
- int setting = SubscriptionManager.getIntegerSubscriptionProperty(
+ int setting = mSubscriptionManagerProxy.getIntegerSubscriptionProperty(
getSubId(), SubscriptionManager.WFC_IMS_ENABLED,
- SUB_PROPERTY_NOT_INITIALIZED, mContext);
+ SUB_PROPERTY_NOT_INITIALIZED);
// SUB_PROPERTY_NOT_INITIALIZED indicates it's never set in sub db.
if (setting == SUB_PROPERTY_NOT_INITIALIZED) {
@@ -836,8 +1096,8 @@ public class ImsManager implements IFeatureConnector {
* {@link #setWfcSetting} instead.
*/
public static void setWfcSetting(Context context, boolean enabled) {
- ImsManager mgr = ImsManager.getInstance(context,
- SubscriptionManager.getDefaultVoicePhoneId());
+ DefaultSubscriptionManagerProxy p = new DefaultSubscriptionManagerProxy(context);
+ ImsManager mgr = ImsManager.getInstance(context, p.getDefaultVoicePhoneId());
if (mgr != null) {
mgr.setWfcSetting(enabled);
}
@@ -852,20 +1112,93 @@ public class ImsManager implements IFeatureConnector {
log("setWfcSetting: Not possible to enable WFC due to provisioning.");
return;
}
-
int subId = getSubId();
- if (isSubIdValid(subId)) {
- SubscriptionManager.setSubscriptionProperty(subId, SubscriptionManager.WFC_IMS_ENABLED,
- booleanToPropertyString(enabled));
- } else {
+ if (!isSubIdValid(subId)) {
loge("setWfcSetting: invalid sub id, can not set WFC setting in siminfo db; subId="
+ subId);
+ return;
}
+ mSubscriptionManagerProxy.setSubscriptionProperty(subId,
+ SubscriptionManager.WFC_IMS_ENABLED, booleanToPropertyString(enabled));
- TelephonyManager tm = (TelephonyManager)
- mContext.getSystemService(Context.TELEPHONY_SERVICE);
- boolean isRoaming = tm.isNetworkRoaming(subId);
- setWfcNonPersistent(enabled, getWfcMode(isRoaming));
+ try {
+ if (enabled) {
+ CapabilityChangeRequest request = new CapabilityChangeRequest();
+ updateVoiceWifiFeatureAndProvisionedValues(request);
+ changeMmTelCapability(request);
+ // Ensure IMS is on if this setting is updated.
+ turnOnIms();
+ } else {
+ // This may cause IMS to be disabled, re-evaluate all caps
+ reevaluateCapabilities();
+ }
+ } catch (ImsException e) {
+ loge("setWfcSetting: " + e);
+ }
+ }
+
+ /**
+ * @return true if the user's setting for Voice over Cross SIM is enabled and
+ * false if it is not
+ */
+ public boolean isCrossSimCallingEnabledByUser() {
+ int setting = mSubscriptionManagerProxy.getIntegerSubscriptionProperty(
+ getSubId(), SubscriptionManager.CROSS_SIM_CALLING_ENABLED,
+ SUB_PROPERTY_NOT_INITIALIZED);
+
+ // SUB_PROPERTY_NOT_INITIALIZED indicates it's never set in sub db.
+ if (setting == SUB_PROPERTY_NOT_INITIALIZED) {
+ return false;
+ } else {
+ return setting == ProvisioningManager.PROVISIONING_VALUE_ENABLED;
+ }
+ }
+
+ /**
+ * @return true if Voice over Cross SIM is provisioned and enabled by user and platform.
+ * false if any of them is not true
+ */
+ public boolean isCrossSimCallingEnabled() {
+ boolean userEnabled = isCrossSimCallingEnabledByUser();
+ boolean platformEnabled = isCrossSimEnabledByPlatform();
+ boolean isProvisioned = isWfcProvisionedOnDevice();
+
+ log("isCrossSimCallingEnabled: platformEnabled = " + platformEnabled
+ + ", provisioned = " + isProvisioned
+ + ", userEnabled = " + userEnabled);
+ return userEnabled && platformEnabled && isProvisioned;
+ }
+
+ /**
+ * Sets the user's setting for whether or not Voice over Cross SIM is enabled.
+ */
+ public void setCrossSimCallingEnabled(boolean enabled) {
+ if (enabled && !isWfcProvisionedOnDevice()) {
+ log("setCrossSimCallingEnabled: Not possible to enable WFC due to provisioning.");
+ return;
+ }
+ int subId = getSubId();
+ if (!isSubIdValid(subId)) {
+ loge("setCrossSimCallingEnabled: "
+ + "invalid sub id, can not set Cross SIM setting in siminfo db; subId="
+ + subId);
+ return;
+ }
+ mSubscriptionManagerProxy.setSubscriptionProperty(subId,
+ SubscriptionManager.CROSS_SIM_CALLING_ENABLED, booleanToPropertyString(enabled));
+ try {
+ if (enabled) {
+ CapabilityChangeRequest request = new CapabilityChangeRequest();
+ updateCrossSimFeatureAndProvisionedValues(request);
+ changeMmTelCapability(request);
+ turnOnIms();
+ } else {
+ // Recalculate all caps to determine if IMS needs to be disabled.
+ reevaluateCapabilities();
+ }
+ } catch (ImsException e) {
+ loge("setCrossSimCallingEnabled(): ", e);
+ }
}
/**
@@ -880,28 +1213,23 @@ public class ImsManager implements IFeatureConnector {
// Force IMS to register over LTE when turning off WFC
int imsWfcModeFeatureValue =
enabled ? wfcMode : ImsMmTelManager.WIFI_MODE_CELLULAR_PREFERRED;
-
try {
- changeMmTelCapability(MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VOICE,
- ImsRegistrationImplBase.REGISTRATION_TECH_IWLAN, enabled);
-
+ changeMmTelCapability(enabled, MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VOICE,
+ ImsRegistrationImplBase.REGISTRATION_TECH_IWLAN);
// Set the mode and roaming enabled settings before turning on IMS
setWfcModeInternal(imsWfcModeFeatureValue);
// If enabled is false, shortcut to false because of the ImsService
// implementation for WFC roaming, otherwise use the correct user's setting.
setWfcRoamingSettingInternal(enabled && isWfcRoamingEnabledByUser());
-
+ // Do not re-evaluate all capabilities because this is a temporary override of WFC
+ // settings.
if (enabled) {
- log("setWfcSetting() : turnOnIms");
+ log("setWfcNonPersistent() : turnOnIms");
+ // Ensure IMS is turned on if this is enabled.
turnOnIms();
- } else if (isTurnOffImsAllowedByPlatform()
- && (!isVolteEnabledByPlatform()
- || !isEnhanced4gLteModeSettingEnabledByUser())) {
- log("setWfcSetting() : imsServiceAllowTurnOff -> turnOffIms");
- turnOffIms();
}
} catch (ImsException e) {
- loge("setWfcSetting(): ", e);
+ loge("setWfcNonPersistent(): ", e);
}
}
@@ -911,8 +1239,8 @@ public class ImsManager implements IFeatureConnector {
* @deprecated Doesn't support MSIM devices. Use {@link #getWfcMode(boolean roaming)} instead.
*/
public static int getWfcMode(Context context) {
- ImsManager mgr = ImsManager.getInstance(context,
- SubscriptionManager.getDefaultVoicePhoneId());
+ DefaultSubscriptionManagerProxy p = new DefaultSubscriptionManagerProxy(context);
+ ImsManager mgr = ImsManager.getInstance(context, p.getDefaultVoicePhoneId());
if (mgr != null) {
return mgr.getWfcMode();
}
@@ -934,8 +1262,8 @@ public class ImsManager implements IFeatureConnector {
* @deprecated Doesn't support MSIM devices. Use {@link #setWfcMode(int)} instead.
*/
public static void setWfcMode(Context context, int wfcMode) {
- ImsManager mgr = ImsManager.getInstance(context,
- SubscriptionManager.getDefaultVoicePhoneId());
+ DefaultSubscriptionManagerProxy p = new DefaultSubscriptionManagerProxy(context);
+ ImsManager mgr = ImsManager.getInstance(context, p.getDefaultVoicePhoneId());
if (mgr != null) {
mgr.setWfcMode(wfcMode);
}
@@ -958,8 +1286,8 @@ public class ImsManager implements IFeatureConnector {
* @deprecated Doesn't support MSIM devices. Use {@link #getWfcMode(boolean)} instead.
*/
public static int getWfcMode(Context context, boolean roaming) {
- ImsManager mgr = ImsManager.getInstance(context,
- SubscriptionManager.getDefaultVoicePhoneId());
+ DefaultSubscriptionManagerProxy p = new DefaultSubscriptionManagerProxy(context);
+ ImsManager mgr = ImsManager.getInstance(context, p.getDefaultVoicePhoneId());
if (mgr != null) {
return mgr.getWfcMode(roaming);
}
@@ -1011,8 +1339,8 @@ public class ImsManager implements IFeatureConnector {
*/
private int getSettingFromSubscriptionManager(String subSetting, String defaultConfigKey) {
int result;
- result = SubscriptionManager.getIntegerSubscriptionProperty(getSubId(), subSetting,
- SUB_PROPERTY_NOT_INITIALIZED, mContext);
+ result = mSubscriptionManagerProxy.getIntegerSubscriptionProperty(getSubId(), subSetting,
+ SUB_PROPERTY_NOT_INITIALIZED);
// SUB_PROPERTY_NOT_INITIALIZED indicates it's never set in sub db.
if (result == SUB_PROPERTY_NOT_INITIALIZED) {
@@ -1030,8 +1358,8 @@ public class ImsManager implements IFeatureConnector {
* instead.
*/
public static void setWfcMode(Context context, int wfcMode, boolean roaming) {
- ImsManager mgr = ImsManager.getInstance(context,
- SubscriptionManager.getDefaultVoicePhoneId());
+ DefaultSubscriptionManagerProxy p = new DefaultSubscriptionManagerProxy(context);
+ ImsManager mgr = ImsManager.getInstance(context, p.getDefaultVoicePhoneId());
if (mgr != null) {
mgr.setWfcMode(wfcMode, roaming);
}
@@ -1048,11 +1376,11 @@ public class ImsManager implements IFeatureConnector {
if (isSubIdValid(subId)) {
if (!roaming) {
if (DBG) log("setWfcMode(i,b) - setting=" + wfcMode);
- SubscriptionManager.setSubscriptionProperty(subId, SubscriptionManager.WFC_IMS_MODE,
+ mSubscriptionManagerProxy.setSubscriptionProperty(subId, SubscriptionManager.WFC_IMS_MODE,
Integer.toString(wfcMode));
} else {
if (DBG) log("setWfcMode(i,b) (roaming) - setting=" + wfcMode);
- SubscriptionManager.setSubscriptionProperty(subId,
+ mSubscriptionManagerProxy.setSubscriptionProperty(subId,
SubscriptionManager.WFC_IMS_ROAMING_MODE, Integer.toString(wfcMode));
}
} else {
@@ -1062,16 +1390,21 @@ public class ImsManager implements IFeatureConnector {
TelephonyManager tm = (TelephonyManager)
mContext.getSystemService(Context.TELEPHONY_SERVICE);
+ if (tm == null) {
+ loge("setWfcMode: TelephonyManager is null, can not set WFC.");
+ return;
+ }
+ tm = tm.createForSubscriptionId(getSubId());
// Unfortunately, the WFC mode is the same for home/roaming (we do not have separate
// config keys), so we have to change the WFC mode when moving home<->roaming. So, only
// call setWfcModeInternal when roaming == telephony roaming status. Otherwise, ignore.
- if (roaming == tm.isNetworkRoaming(getSubId())) {
+ if (roaming == tm.isNetworkRoaming()) {
setWfcModeInternal(wfcMode);
}
}
private int getSubId() {
- int[] subIds = SubscriptionManager.getSubId(mPhoneId);
+ int[] subIds = mSubscriptionManagerProxy.getSubscriptionIds(mPhoneId);
int subId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
if (subIds != null && subIds.length >= 1) {
subId = subIds[0];
@@ -1081,7 +1414,7 @@ public class ImsManager implements IFeatureConnector {
private void setWfcModeInternal(int wfcMode) {
final int value = wfcMode;
- mExecutorFactory.executeRunnable(() -> {
+ getImsThreadExecutor().execute(() -> {
try {
getConfigInterface().setConfig(
ProvisioningManager.KEY_VOICE_OVER_WIFI_MODE_OVERRIDE, value);
@@ -1098,8 +1431,8 @@ public class ImsManager implements IFeatureConnector {
* {@link #isWfcRoamingEnabledByUser()} instead.
*/
public static boolean isWfcRoamingEnabledByUser(Context context) {
- ImsManager mgr = ImsManager.getInstance(context,
- SubscriptionManager.getDefaultVoicePhoneId());
+ DefaultSubscriptionManagerProxy p = new DefaultSubscriptionManagerProxy(context);
+ ImsManager mgr = ImsManager.getInstance(context, p.getDefaultVoicePhoneId());
if (mgr != null) {
return mgr.isWfcRoamingEnabledByUser();
}
@@ -1112,9 +1445,9 @@ public class ImsManager implements IFeatureConnector {
* queries CarrierConfig value as default.
*/
public boolean isWfcRoamingEnabledByUser() {
- int setting = SubscriptionManager.getIntegerSubscriptionProperty(
+ int setting = mSubscriptionManagerProxy.getIntegerSubscriptionProperty(
getSubId(), SubscriptionManager.WFC_IMS_ROAMING_ENABLED,
- SUB_PROPERTY_NOT_INITIALIZED, mContext);
+ SUB_PROPERTY_NOT_INITIALIZED);
if (setting == SUB_PROPERTY_NOT_INITIALIZED) {
return getBooleanCarrierConfig(
CarrierConfigManager.KEY_CARRIER_DEFAULT_WFC_IMS_ROAMING_ENABLED_BOOL);
@@ -1127,8 +1460,8 @@ public class ImsManager implements IFeatureConnector {
* Change persistent WFC roaming enabled setting
*/
public static void setWfcRoamingSetting(Context context, boolean enabled) {
- ImsManager mgr = ImsManager.getInstance(context,
- SubscriptionManager.getDefaultVoicePhoneId());
+ DefaultSubscriptionManagerProxy p = new DefaultSubscriptionManagerProxy(context);
+ ImsManager mgr = ImsManager.getInstance(context, p.getDefaultVoicePhoneId());
if (mgr != null) {
mgr.setWfcRoamingSetting(enabled);
}
@@ -1139,7 +1472,7 @@ public class ImsManager implements IFeatureConnector {
* Change persistent WFC roaming enabled setting
*/
public void setWfcRoamingSetting(boolean enabled) {
- SubscriptionManager.setSubscriptionProperty(getSubId(),
+ mSubscriptionManagerProxy.setSubscriptionProperty(getSubId(),
SubscriptionManager.WFC_IMS_ROAMING_ENABLED, booleanToPropertyString(enabled)
);
@@ -1150,7 +1483,7 @@ public class ImsManager implements IFeatureConnector {
final int value = enabled
? ProvisioningManager.PROVISIONING_VALUE_ENABLED
: ProvisioningManager.PROVISIONING_VALUE_DISABLED;
- mExecutorFactory.executeRunnable(() -> {
+ getImsThreadExecutor().execute(() -> {
try {
getConfigInterface().setConfig(
ProvisioningManager.KEY_VOICE_OVER_WIFI_ROAMING_ENABLED_OVERRIDE, value);
@@ -1169,8 +1502,8 @@ public class ImsManager implements IFeatureConnector {
* instead.
*/
public static boolean isWfcEnabledByPlatform(Context context) {
- ImsManager mgr = ImsManager.getInstance(context,
- SubscriptionManager.getDefaultVoicePhoneId());
+ DefaultSubscriptionManagerProxy p = new DefaultSubscriptionManagerProxy(context);
+ ImsManager mgr = ImsManager.getInstance(context, p.getDefaultVoicePhoneId());
if (mgr != null) {
return mgr.isWfcEnabledByPlatform();
}
@@ -1200,6 +1533,19 @@ public class ImsManager implements IFeatureConnector {
isGbaValid();
}
+ /**
+ * Returns a platform configuration for Cross SIM which may override the user
+ * setting per slot. Note: Cross SIM presumes that VoLTE is enabled (these are
+ * configuration settings which must be done correctly).
+ */
+ public boolean isCrossSimEnabledByPlatform() {
+ if (isWfcEnabledByPlatform()) {
+ return getBooleanCarrierConfig(
+ CarrierConfigManager.KEY_CARRIER_CROSS_SIM_IMS_AVAILABLE_BOOL);
+ }
+ return false;
+ }
+
public boolean isSuppServicesOverUtEnabledByPlatform() {
TelephonyManager manager = (TelephonyManager) mContext.getSystemService(
Context.TELEPHONY_SERVICE);
@@ -1221,8 +1567,13 @@ public class ImsManager implements IFeatureConnector {
private boolean isGbaValid() {
if (getBooleanCarrierConfig(
CarrierConfigManager.KEY_CARRIER_IMS_GBA_REQUIRED_BOOL)) {
- final TelephonyManager telephonyManager = new TelephonyManager(mContext, getSubId());
- String efIst = telephonyManager.getIsimIst();
+ TelephonyManager tm = mContext.getSystemService(TelephonyManager.class);
+ if (tm == null) {
+ loge("isGbaValid: TelephonyManager is null, returning false.");
+ return false;
+ }
+ tm = tm.createForSubscriptionId(getSubId());
+ String efIst = tm.getIsimIst();
if (efIst == null) {
loge("isGbaValid - ISF is NULL");
return true;
@@ -1292,109 +1643,102 @@ public class ImsManager implements IFeatureConnector {
}
/**
- * Sync carrier config and user settings with ImsConfigImplBase implementation.
- *
- * @param context for the manager object
- * @param phoneId phone id
- * @param force update
- *
- * @deprecated Doesn't support MSIM devices. Use {@link #updateImsServiceConfig(boolean)}
- * instead.
+ * Push configuration updates to the ImsService implementation.
*/
- public static void updateImsServiceConfig(Context context, int phoneId, boolean force) {
- ImsManager mgr = ImsManager.getInstance(context, phoneId);
- if (mgr != null) {
- mgr.updateImsServiceConfig(force);
- }
- Rlog.e(TAG, "updateImsServiceConfig: ImsManager null, returning without update.");
- }
-
- /**
- * Sync carrier config and user settings with ImsConfigImplBase implementation.
- *
- * @param force update
- */
- public void updateImsServiceConfig(boolean force) {
- if (!force) {
- TelephonyManager tm = new TelephonyManager(mContext, getSubId());
- if (tm.getSimState() != TelephonyManager.SIM_STATE_READY) {
- log("updateImsServiceConfig: SIM not ready");
- // Don't disable IMS if SIM is not ready
+ public void updateImsServiceConfig() {
+ try {
+ int subId = getSubId();
+ if (!isSubIdValid(subId)) {
+ loge("updateImsServiceConfig: invalid sub id, skipping!");
return;
}
+ PersistableBundle imsCarrierConfigs =
+ mConfigManager.getConfigByComponentForSubId(
+ CarrierConfigManager.Ims.KEY_PREFIX, subId);
+ updateImsCarrierConfigs(imsCarrierConfigs);
+ reevaluateCapabilities();
+ mConfigUpdated = true;
+ } catch (ImsException e) {
+ loge("updateImsServiceConfig: ", e);
+ mConfigUpdated = false;
}
+ }
- if (!mConfigUpdated || force) {
- try {
- PersistableBundle imsCarrierConfigs =
- mConfigManager.getConfigByComponentForSubId(
- CarrierConfigManager.Ims.KEY_PREFIX, getSubId());
-
- updateImsCarrierConfigs(imsCarrierConfigs);
-
- // Note: currently the order of updates is set to produce different order of
- // changeEnabledCapabilities() function calls from setAdvanced4GMode(). This is done
- // to differentiate this code path from vendor code perspective.
- CapabilityChangeRequest request = new CapabilityChangeRequest();
- updateVolteFeatureValue(request);
- updateWfcFeatureAndProvisionedValues(request);
- updateVideoCallFeatureValue(request);
- // Only turn on IMS for RTT if there's an active subscription present. If not, the
- // modem will be in emergency-call-only mode and will use separate signaling to
- // establish an RTT emergency call.
- boolean isImsNeededForRtt = updateRttConfigValue() && isActiveSubscriptionPresent();
- // Supplementary services over UT do not require IMS registration. Do not alter IMS
- // registration based on UT.
- updateUtFeatureValue(request);
-
- // Send the batched request to the modem.
- changeMmTelCapability(request);
-
- if (isImsNeededForRtt || !isTurnOffImsAllowedByPlatform() || isImsNeeded(request)) {
- // Turn on IMS if it is used.
- // Also, if turning off is not allowed for current carrier,
- // we need to turn IMS on because it might be turned off before
- // phone switched to current carrier.
- log("updateImsServiceConfig: turnOnIms");
- turnOnIms();
- } else {
- // Turn off IMS if it is not used AND turning off is allowed for carrier.
- log("updateImsServiceConfig: turnOffIms");
- turnOffIms();
- }
+ /**
+ * Evaluate the state of the IMS capabilities and push the updated state to the ImsService.
+ */
+ private void reevaluateCapabilities() throws ImsException {
+ logi("reevaluateCapabilities");
+ CapabilityChangeRequest request = new CapabilityChangeRequest();
+ boolean isNonTty = isNonTtyOrTtyOnVolteEnabled();
+ updateVoiceCellFeatureValue(request, isNonTty);
+ updateVoiceWifiFeatureAndProvisionedValues(request);
+ updateCrossSimFeatureAndProvisionedValues(request);
+ updateVideoCallFeatureValue(request, isNonTty);
+ updateCallComposerFeatureValue(request);
+ // Only turn on IMS for RTT if there's an active subscription present. If not, the
+ // modem will be in emergency-call-only mode and will use separate signaling to
+ // establish an RTT emergency call.
+ boolean isImsNeededForRtt = updateRttConfigValue() && isActiveSubscriptionPresent();
+ // Supplementary services over UT do not require IMS registration. Do not alter IMS
+ // registration based on UT.
+ updateUtFeatureValue(request);
+
+ // Send the batched request to the modem.
+ changeMmTelCapability(request);
- mConfigUpdated = true;
- } catch (ImsException e) {
- loge("updateImsServiceConfig: ", e);
- mConfigUpdated = false;
- }
+ if (isImsNeededForRtt || !isTurnOffImsAllowedByPlatform() || isImsNeeded(request)) {
+ // Turn on IMS if it is used.
+ // Also, if turning off is not allowed for current carrier,
+ // we need to turn IMS on because it might be turned off before
+ // phone switched to current carrier.
+ log("reevaluateCapabilities: turnOnIms");
+ turnOnIms();
+ } else {
+ // Turn off IMS if it is not used AND turning off is allowed for carrier.
+ log("reevaluateCapabilities: turnOffIms");
+ turnOffIms();
}
}
+ /**
+ * @return {@code true} if IMS needs to be turned on for the request, {@code false} if it can
+ * be disabled.
+ */
private boolean isImsNeeded(CapabilityChangeRequest r) {
- // IMS is not needed for UT, so only enabled IMS if any other capability is enabled.
return r.getCapabilitiesToEnable().stream()
- .anyMatch((c) ->
- (c.getCapability() != MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_UT));
+ .anyMatch(c -> isImsNeededForCapability(c.getCapability()));
+ }
+
+ /**
+ * @return {@code true} if IMS needs to be turned on for the capability.
+ */
+ private boolean isImsNeededForCapability(int capability) {
+ // UT does not require IMS to be enabled.
+ return capability != MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_UT &&
+ // call composer is used as part of calling, so it should not trigger the enablement
+ // of IMS.
+ capability != MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_CALL_COMPOSER;
}
/**
* Update VoLTE config
*/
- private void updateVolteFeatureValue(CapabilityChangeRequest request) {
+ private void updateVoiceCellFeatureValue(CapabilityChangeRequest request, boolean isNonTty) {
boolean available = isVolteEnabledByPlatform();
boolean enabled = isEnhanced4gLteModeSettingEnabledByUser();
- boolean isNonTty = isNonTtyOrTtyOnVolteEnabled();
boolean isProvisioned = isVolteProvisionedOnDevice();
- boolean isFeatureOn = available && enabled && isNonTty && isProvisioned;
+ boolean voLteFeatureOn = available && enabled && isNonTty && isProvisioned;
+ boolean voNrAvailable = isImsOverNrEnabledByPlatform();
- log("updateVolteFeatureValue: available = " + available
+ log("updateVoiceCellFeatureValue: available = " + available
+ ", enabled = " + enabled
+ ", nonTTY = " + isNonTty
+ ", provisioned = " + isProvisioned
- + ", isFeatureOn = " + isFeatureOn);
+ + ", voLteFeatureOn = " + voLteFeatureOn
+ + ", voNrAvailable = " + voNrAvailable);
- if (isFeatureOn) {
+ if (voLteFeatureOn) {
request.addCapabilitiesToEnableForTech(
MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VOICE,
ImsRegistrationImplBase.REGISTRATION_TECH_LTE);
@@ -1403,46 +1747,80 @@ public class ImsManager implements IFeatureConnector {
MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VOICE,
ImsRegistrationImplBase.REGISTRATION_TECH_LTE);
}
+ if (voLteFeatureOn && voNrAvailable) {
+ request.addCapabilitiesToEnableForTech(
+ MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VOICE,
+ ImsRegistrationImplBase.REGISTRATION_TECH_NR);
+ } else {
+ request.addCapabilitiesToDisableForTech(
+ MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VOICE,
+ ImsRegistrationImplBase.REGISTRATION_TECH_NR);
+ }
}
/**
- * Update video call over LTE config
+ * Update video call configuration
*/
- private void updateVideoCallFeatureValue(CapabilityChangeRequest request) {
+ private void updateVideoCallFeatureValue(CapabilityChangeRequest request, boolean isNonTty) {
boolean available = isVtEnabledByPlatform();
- boolean enabled = isVtEnabledByUser();
- boolean isNonTty = isNonTtyOrTtyOnVolteEnabled();
+ boolean vtEnabled = isVtEnabledByUser();
+ boolean advancedEnabled = isEnhanced4gLteModeSettingEnabledByUser();
boolean isDataEnabled = isDataEnabled();
boolean ignoreDataEnabledChanged = getBooleanCarrierConfig(
CarrierConfigManager.KEY_IGNORE_DATA_ENABLED_CHANGED_FOR_VIDEO_CALLS);
boolean isProvisioned = isVtProvisionedOnDevice();
- boolean isFeatureOn = available && enabled && isNonTty && isProvisioned
- && (ignoreDataEnabledChanged || isDataEnabled);
+ // TODO: Support carrier config setting about if VT settings should be associated with
+ // advanced calling settings.
+ boolean isLteFeatureOn = available && vtEnabled && isNonTty && isProvisioned
+ && advancedEnabled && (ignoreDataEnabledChanged || isDataEnabled);
+ boolean nrAvailable = isImsOverNrEnabledByPlatform();
log("updateVideoCallFeatureValue: available = " + available
- + ", enabled = " + enabled
+ + ", vtenabled = " + vtEnabled
+ + ", advancedCallEnabled = " + advancedEnabled
+ ", nonTTY = " + isNonTty
+ ", data enabled = " + isDataEnabled
+ ", provisioned = " + isProvisioned
- + ", isFeatureOn = " + isFeatureOn);
+ + ", isLteFeatureOn = " + isLteFeatureOn
+ + ", nrAvailable = " + nrAvailable);
- if (isFeatureOn) {
+ if (isLteFeatureOn) {
request.addCapabilitiesToEnableForTech(
MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VIDEO,
ImsRegistrationImplBase.REGISTRATION_TECH_LTE);
+ // VT does not differentiate transport today, do not set IWLAN.
} else {
request.addCapabilitiesToDisableForTech(
MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VIDEO,
ImsRegistrationImplBase.REGISTRATION_TECH_LTE);
+ // VT does not differentiate transport today, do not set IWLAN.
+ }
+
+ if (isLteFeatureOn && nrAvailable) {
+ request.addCapabilitiesToEnableForTech(
+ MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VIDEO,
+ ImsRegistrationImplBase.REGISTRATION_TECH_NR);
+ } else {
+ request.addCapabilitiesToDisableForTech(
+ MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VIDEO,
+ ImsRegistrationImplBase.REGISTRATION_TECH_NR);
}
}
/**
* Update WFC config
*/
- private void updateWfcFeatureAndProvisionedValues(CapabilityChangeRequest request) {
- TelephonyManager tm = new TelephonyManager(mContext, getSubId());
- boolean isNetworkRoaming = tm.isNetworkRoaming();
+ private void updateVoiceWifiFeatureAndProvisionedValues(CapabilityChangeRequest request) {
+ TelephonyManager tm = mContext.getSystemService(TelephonyManager.class);
+ boolean isNetworkRoaming = false;
+ if (tm == null) {
+ loge("updateVoiceWifiFeatureAndProvisionedValues: TelephonyManager is null, assuming"
+ + " not roaming.");
+ } else {
+ tm = tm.createForSubscriptionId(getSubId());
+ isNetworkRoaming = tm.isNetworkRoaming();
+ }
+
boolean available = isWfcEnabledByPlatform();
boolean enabled = isWfcEnabledByUser();
boolean isProvisioned = isWfcProvisionedOnDevice();
@@ -1475,6 +1853,21 @@ public class ImsManager implements IFeatureConnector {
setWfcRoamingSettingInternal(roaming);
}
+ /**
+ * Update Cross SIM config
+ */
+ private void updateCrossSimFeatureAndProvisionedValues(CapabilityChangeRequest request) {
+ if (isCrossSimCallingEnabled()) {
+ request.addCapabilitiesToEnableForTech(
+ MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VOICE,
+ ImsRegistrationImplBase.REGISTRATION_TECH_CROSS_SIM);
+ } else {
+ request.addCapabilitiesToDisableForTech(
+ MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VOICE,
+ ImsRegistrationImplBase.REGISTRATION_TECH_CROSS_SIM);
+ }
+ }
+
private void updateUtFeatureValue(CapabilityChangeRequest request) {
boolean isCarrierSupported = isSuppServicesOverUtEnabledByPlatform();
@@ -1483,11 +1876,7 @@ public class ImsManager implements IFeatureConnector {
// Count as "provisioned" if we do not require provisioning.
boolean isProvisioned = true;
if (requiresProvisioning) {
- ITelephony telephony = ITelephony.Stub.asInterface(
- TelephonyFrameworkInitializer
- .getTelephonyServiceManager()
- .getTelephonyServiceRegisterer()
- .get());
+ ITelephony telephony = getITelephony();
// Only track UT over LTE, since we do not differentiate between UT over LTE and IWLAN
// currently.
try {
@@ -1518,96 +1907,118 @@ public class ImsManager implements IFeatureConnector {
}
/**
+ * Update call composer capability
+ */
+ private void updateCallComposerFeatureValue(CapabilityChangeRequest request) {
+ boolean isUserSetEnabled = isCallComposerEnabledByUser();
+ boolean isCarrierConfigEnabled = getBooleanCarrierConfig(
+ CarrierConfigManager.KEY_SUPPORTS_CALL_COMPOSER_BOOL);
+
+ boolean isFeatureOn = isUserSetEnabled && isCarrierConfigEnabled;
+ boolean nrAvailable = isImsOverNrEnabledByPlatform();
+
+ log("updateCallComposerFeatureValue: isUserSetEnabled = " + isUserSetEnabled
+ + ", isCarrierConfigEnabled = " + isCarrierConfigEnabled
+ + ", isFeatureOn = " + isFeatureOn
+ + ", nrAvailable = " + nrAvailable);
+
+ if (isFeatureOn) {
+ request.addCapabilitiesToEnableForTech(
+ MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_CALL_COMPOSER,
+ ImsRegistrationImplBase.REGISTRATION_TECH_LTE);
+ } else {
+ request.addCapabilitiesToDisableForTech(
+ MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_CALL_COMPOSER,
+ ImsRegistrationImplBase.REGISTRATION_TECH_LTE);
+ }
+ if (isFeatureOn && nrAvailable) {
+ request.addCapabilitiesToEnableForTech(
+ MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_CALL_COMPOSER,
+ ImsRegistrationImplBase.REGISTRATION_TECH_NR);
+ } else {
+ request.addCapabilitiesToDisableForTech(
+ MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_CALL_COMPOSER,
+ ImsRegistrationImplBase.REGISTRATION_TECH_NR);
+ }
+ }
+
+ /**
* Do NOT use this directly, instead use {@link #getInstance(Context, int)}.
*/
+ private ImsManager(Context context, int phoneId) {
+ mContext = context;
+ mPhoneId = phoneId;
+ mSubscriptionManagerProxy = new DefaultSubscriptionManagerProxy(context);
+ mSettingsProxy = new DefaultSettingsProxy();
+ mConfigManager = (CarrierConfigManager) context.getSystemService(
+ Context.CARRIER_CONFIG_SERVICE);
+ mExecutor = new LazyExecutor();
+ mBinderCache = new BinderCacheManager<>(ImsManager::getITelephonyInterface);
+ // Start off with an empty MmTelFeatureConnection, which will be replaced one an
+ // ImsService is available (ImsManager expects a non-null FeatureConnection)
+ associate(null /*container*/);
+ }
+
+ /**
+ * Used for testing only to inject dependencies.
+ */
@VisibleForTesting
- public ImsManager(Context context, int phoneId) {
+ public ImsManager(Context context, int phoneId, MmTelFeatureConnectionFactory factory,
+ SubscriptionManagerProxy subManagerProxy, SettingsProxy settingsProxy) {
mContext = context;
mPhoneId = phoneId;
+ mMmTelFeatureConnectionFactory = factory;
+ mSubscriptionManagerProxy = subManagerProxy;
+ mSettingsProxy = settingsProxy;
mConfigManager = (CarrierConfigManager) context.getSystemService(
Context.CARRIER_CONFIG_SERVICE);
- createImsService();
+ // Do not multithread tests
+ mExecutor = Runnable::run;
+ mBinderCache = new BinderCacheManager<>(ImsManager::getITelephonyInterface);
+ // MmTelFeatureConnection should be replaced for tests with mMmTelFeatureConnectionFactory.
+ associate(null /*container*/);
}
/*
- * Returns a flag indicating whether the IMS service is available. If it is not available or
- * busy, it will try to connect before reporting failure.
+ * Returns a flag indicating whether the IMS service is available.
*/
public boolean isServiceAvailable() {
- connectIfServiceIsAvailable();
- // mImsServiceProxy will always create an ImsServiceProxy.
- return mMmTelFeatureConnection.isBinderAlive();
+ return mMmTelConnectionRef.get().isBinderAlive();
}
/*
* Returns a flag indicating whether the IMS service is ready to send requests to lower layers.
*/
public boolean isServiceReady() {
- connectIfServiceIsAvailable();
- return mMmTelFeatureConnection.isBinderReady();
- }
-
- /**
- * If the service is available, try to reconnect.
- */
- public void connectIfServiceIsAvailable() {
- if (mMmTelFeatureConnection == null || !mMmTelFeatureConnection.isBinderAlive()) {
- createImsService();
- }
+ return mMmTelConnectionRef.get().isBinderReady();
}
- public void setConfigListener(ImsConfigListener listener) {
- mImsConfigListener = listener;
- }
-
-
/**
- * Adds a callback for status changed events if the binder is already available. If it is not,
- * this method will throw an ImsException.
- */
- @Override
- @VisibleForTesting
- public void addNotifyStatusChangedCallbackIfAvailable(FeatureConnection.IFeatureUpdate c)
- throws android.telephony.ims.ImsException {
- if (!mMmTelFeatureConnection.isBinderAlive()) {
- throw new android.telephony.ims.ImsException("Can not connect to ImsService",
- android.telephony.ims.ImsException.CODE_ERROR_SERVICE_UNAVAILABLE);
- }
- if (c != null) {
- mStatusCallbacks.add(c);
- }
- }
-
- @Override
- public void removeNotifyStatusChangedCallback(FeatureConnection.IFeatureUpdate c) {
- if (c != null) {
- mStatusCallbacks.remove(c);
- } else {
- logw("removeNotifyStatusChangedCallback: callback is null!");
- }
- }
-
- /**
- * Opens the IMS service for making calls and/or receiving generic IMS calls.
+ * Opens the IMS service for making calls and/or receiving generic IMS calls as well as
+ * register listeners for ECBM, Multiendpoint, and UT if the ImsService supports it.
+ * <p>
* The caller may make subsequent calls through {@link #makeCall}.
* The IMS service will register the device to the operator's network with the credentials
* (from ISIM) periodically in order to receive calls from the operator's network.
* When the IMS service receives a new call, it will call
* {@link MmTelFeature.Listener#onIncomingCall}
* @param listener A {@link MmTelFeature.Listener}, which is the interface the
- * {@link MmTelFeature} uses to notify the framework of updates
+ * {@link MmTelFeature} uses to notify the framework of updates.
+ * @param ecbmListener Listener used for ECBM indications.
+ * @param multiEndpointListener Listener used for multiEndpoint indications.
* @throws NullPointerException if {@code listener} is null
* @throws ImsException if calling the IMS service results in an error
*/
- public void open(MmTelFeature.Listener listener) throws ImsException {
- checkAndThrowExceptionIfServiceUnavailable();
+ public void open(MmTelFeature.Listener listener, ImsEcbmStateListener ecbmListener,
+ ImsExternalCallStateListener multiEndpointListener) throws ImsException {
+ MmTelFeatureConnection c = getOrThrowExceptionIfServiceUnavailable();
if (listener == null) {
throw new NullPointerException("listener can't be null");
}
try {
- mMmTelFeatureConnection.openConnection(listener);
+ c.openConnection(listener, ecbmListener, multiEndpointListener);
} catch (RemoteException e) {
throw new ImsException("open()", e, ImsReasonInfo.CODE_LOCAL_IMS_SERVICE_DOWN);
}
@@ -1635,14 +2046,14 @@ public class ImsManager implements IFeatureConnector {
* @param listener To listen to IMS registration events; It cannot be null
* @throws NullPointerException if {@code listener} is null
* @throws ImsException if calling the IMS service results in an error
- * @deprecated use {@link #addRegistrationCallback(RegistrationManager.RegistrationCallback)}
- * instead.
+ * @deprecated use {@link #addRegistrationCallback(RegistrationManager.RegistrationCallback,
+ * Executor)} instead.
*/
public void addRegistrationListener(ImsConnectionStateListener listener) throws ImsException {
if (listener == null) {
throw new NullPointerException("listener can't be null");
}
- addRegistrationCallback(listener);
+ addRegistrationCallback(listener, getImsThreadExecutor());
// connect the ImsConnectionStateListener to the new CapabilityCallback.
addCapabilitiesCallback(new ImsMmTelManager.CapabilityCallback() {
@Override
@@ -1650,7 +2061,7 @@ public class ImsManager implements IFeatureConnector {
MmTelFeature.MmTelCapabilities capabilities) {
listener.onFeatureCapabilityChangedAdapter(getRegistrationTech(), capabilities);
}
- });
+ }, getImsThreadExecutor());
log("Registration Callback registered.");
}
@@ -1659,17 +2070,19 @@ public class ImsManager implements IFeatureConnector {
* associated with this ImsManager.
* @param callback A {@link RegistrationManager.RegistrationCallback} that will notify the
* caller when IMS registration status has changed.
+ * @param executor The Executor that the callback should be called on.
* @throws ImsException when the ImsService connection is not available.
*/
- public void addRegistrationCallback(RegistrationManager.RegistrationCallback callback)
+ public void addRegistrationCallback(RegistrationManager.RegistrationCallback callback,
+ Executor executor)
throws ImsException {
if (callback == null) {
throw new NullPointerException("registration callback can't be null");
}
try {
- callback.setExecutor(getThreadExecutor());
- mMmTelFeatureConnection.addRegistrationCallback(callback.getBinder());
+ callback.setExecutor(executor);
+ mMmTelConnectionRef.get().addRegistrationCallback(callback.getBinder());
log("Registration Callback registered.");
// Only record if there isn't a RemoteException.
} catch (IllegalStateException e) {
@@ -1680,15 +2093,14 @@ public class ImsManager implements IFeatureConnector {
/**
* Removes a previously added registration callback that was added via
- * {@link #addRegistrationCallback(RegistrationManager.RegistrationCallback)} .
+ * {@link #addRegistrationCallback(RegistrationManager.RegistrationCallback, Executor)} .
* @param callback A {@link RegistrationManager.RegistrationCallback} that was previously added.
*/
public void removeRegistrationListener(RegistrationManager.RegistrationCallback callback) {
if (callback == null) {
throw new NullPointerException("registration callback can't be null");
}
-
- mMmTelFeatureConnection.removeRegistrationCallback(callback.getBinder());
+ mMmTelConnectionRef.get().removeRegistrationCallback(callback.getBinder());
log("Registration callback removed.");
}
@@ -1706,7 +2118,7 @@ public class ImsManager implements IFeatureConnector {
if (callback == null) {
throw new IllegalArgumentException("registration callback can't be null");
}
- mMmTelFeatureConnection.addRegistrationCallbackForSubscription(callback, subId);
+ mMmTelConnectionRef.get().addRegistrationCallbackForSubscription(callback, subId);
log("Registration Callback registered.");
// Only record if there isn't a RemoteException.
}
@@ -1720,8 +2132,7 @@ public class ImsManager implements IFeatureConnector {
if (callback == null) {
throw new IllegalArgumentException("registration callback can't be null");
}
-
- mMmTelFeatureConnection.removeRegistrationCallbackForSubscription(callback, subId);
+ mMmTelConnectionRef.get().removeRegistrationCallbackForSubscription(callback, subId);
}
/**
@@ -1729,18 +2140,19 @@ public class ImsManager implements IFeatureConnector {
* Voice over IMS or VT over IMS is not available currently.
* @param callback A {@link ImsMmTelManager.CapabilityCallback} that will notify the caller when
* MMTel capability status has changed.
+ * @param executor The Executor that the callback should be called on.
* @throws ImsException when the ImsService connection is not available.
*/
- public void addCapabilitiesCallback(ImsMmTelManager.CapabilityCallback callback)
- throws ImsException {
+ public void addCapabilitiesCallback(ImsMmTelManager.CapabilityCallback callback,
+ Executor executor) throws ImsException {
if (callback == null) {
throw new NullPointerException("capabilities callback can't be null");
}
- checkAndThrowExceptionIfServiceUnavailable();
+ MmTelFeatureConnection c = getOrThrowExceptionIfServiceUnavailable();
try {
- callback.setExecutor(getThreadExecutor());
- mMmTelFeatureConnection.addCapabilityCallback(callback.getBinder());
+ callback.setExecutor(executor);
+ c.addCapabilityCallback(callback.getBinder());
log("Capability Callback registered.");
// Only record if there isn't a RemoteException.
} catch (IllegalStateException e) {
@@ -1751,16 +2163,18 @@ public class ImsManager implements IFeatureConnector {
/**
* Removes a previously registered {@link ImsMmTelManager.CapabilityCallback} callback.
- * @throws ImsException when the ImsService connection is not available.
*/
- public void removeCapabilitiesCallback(ImsMmTelManager.CapabilityCallback callback)
- throws ImsException {
+ public void removeCapabilitiesCallback(ImsMmTelManager.CapabilityCallback callback) {
if (callback == null) {
throw new NullPointerException("capabilities callback can't be null");
}
- checkAndThrowExceptionIfServiceUnavailable();
- mMmTelFeatureConnection.removeCapabilityCallback(callback.getBinder());
+ try {
+ MmTelFeatureConnection c = getOrThrowExceptionIfServiceUnavailable();
+ c.removeCapabilityCallback(callback.getBinder());
+ } catch (ImsException e) {
+ log("Exception removing Capability , exception=" + e);
+ }
}
/**
@@ -1776,8 +2190,7 @@ public class ImsManager implements IFeatureConnector {
if (callback == null) {
throw new IllegalArgumentException("registration callback can't be null");
}
-
- mMmTelFeatureConnection.addCapabilityCallbackForSubscription(callback, subId);
+ mMmTelConnectionRef.get().addCapabilityCallbackForSubscription(callback, subId);
log("Capability Callback registered for subscription.");
}
@@ -1790,8 +2203,7 @@ public class ImsManager implements IFeatureConnector {
if (callback == null) {
throw new IllegalArgumentException("capabilities callback can't be null");
}
-
- mMmTelFeatureConnection.removeCapabilityCallbackForSubscription(callback, subId);
+ mMmTelConnectionRef.get().removeCapabilityCallbackForSubscription(callback, subId);
}
/**
@@ -1808,8 +2220,8 @@ public class ImsManager implements IFeatureConnector {
throw new NullPointerException("listener can't be null");
}
- checkAndThrowExceptionIfServiceUnavailable();
- mMmTelFeatureConnection.removeRegistrationCallback(listener.getBinder());
+ MmTelFeatureConnection c = getOrThrowExceptionIfServiceUnavailable();
+ c.removeRegistrationCallback(listener.getBinder());
log("Registration Callback/Listener registered.");
// Only record if there isn't a RemoteException.
}
@@ -1827,7 +2239,7 @@ public class ImsManager implements IFeatureConnector {
throw new IllegalArgumentException("provisioning callback can't be null");
}
- mMmTelFeatureConnection.addProvisioningCallbackForSubscription(callback, subId);
+ mMmTelConnectionRef.get().addProvisioningCallbackForSubscription(callback, subId);
log("Capability Callback registered for subscription.");
}
@@ -1842,12 +2254,12 @@ public class ImsManager implements IFeatureConnector {
throw new IllegalArgumentException("provisioning callback can't be null");
}
- mMmTelFeatureConnection.removeProvisioningCallbackForSubscription(callback, subId);
+ mMmTelConnectionRef.get().removeProvisioningCallbackForSubscription(callback, subId);
}
public @ImsRegistrationImplBase.ImsRegistrationTech int getRegistrationTech() {
try {
- return mMmTelFeatureConnection.getRegistrationTech();
+ return mMmTelConnectionRef.get().getRegistrationTech();
} catch (RemoteException e) {
logw("getRegistrationTech: no connection to ImsService.");
return ImsRegistrationImplBase.REGISTRATION_TECH_NONE;
@@ -1855,9 +2267,9 @@ public class ImsManager implements IFeatureConnector {
}
public void getRegistrationTech(Consumer<Integer> callback) {
- mExecutorFactory.executeRunnable(() -> {
+ getImsThreadExecutor().execute(() -> {
try {
- int tech = mMmTelFeatureConnection.getRegistrationTech();
+ int tech = mMmTelConnectionRef.get().getRegistrationTech();
callback.accept(tech);
} catch (RemoteException e) {
logw("getRegistrationTech(C): no connection to ImsService.");
@@ -1867,45 +2279,36 @@ public class ImsManager implements IFeatureConnector {
}
/**
- * Closes the connection and removes all active callbacks.
- * All the resources that were allocated to the service are also released.
+ * Closes the connection opened in {@link #open} and removes the associated listeners.
*/
public void close() {
- if (mMmTelFeatureConnection != null) {
- mMmTelFeatureConnection.closeConnection();
- }
- mUt = null;
- mEcbm = null;
- mMultiEndpoint = null;
+ mMmTelConnectionRef.get().closeConnection();
}
/**
- * Gets the configuration interface to provision / withdraw the supplementary service settings.
+ * Create or get the existing configuration interface to provision / withdraw the supplementary
+ * service settings.
+ * <p>
+ * There can only be one connection to the UT interface, so this may only be called by one
+ * ImsManager instance. Otherwise, an IllegalStateException will be thrown.
*
* @return the Ut interface instance
* @throws ImsException if getting the Ut interface results in an error
*/
- public ImsUtInterface getSupplementaryServiceConfiguration() throws ImsException {
- // FIXME: manage the multiple Ut interfaces based on the session id
- if (mUt != null && mUt.isBinderAlive()) {
- return mUt;
- }
-
- checkAndThrowExceptionIfServiceUnavailable();
+ public ImsUtInterface createOrGetSupplementaryServiceConfiguration() throws ImsException {
+ ImsUt iUt;
+ MmTelFeatureConnection c = getOrThrowExceptionIfServiceUnavailable();
try {
- IImsUt iUt = mMmTelFeatureConnection.getUtInterface();
-
+ iUt = c.createOrGetUtInterface();
if (iUt == null) {
throw new ImsException("getSupplementaryServiceConfiguration()",
ImsReasonInfo.CODE_UT_NOT_SUPPORTED);
}
-
- mUt = new ImsUt(iUt);
} catch (RemoteException e) {
throw new ImsException("getSupplementaryServiceConfiguration()", e,
ImsReasonInfo.CODE_LOCAL_IMS_SERVICE_DOWN);
}
- return mUt;
+ return iUt;
}
/**
@@ -1928,10 +2331,10 @@ public class ImsManager implements IFeatureConnector {
* @throws ImsException if calling the IMS service results in an error
*/
public ImsCallProfile createCallProfile(int serviceType, int callType) throws ImsException {
- checkAndThrowExceptionIfServiceUnavailable();
+ MmTelFeatureConnection c = getOrThrowExceptionIfServiceUnavailable();
try {
- return mMmTelFeatureConnection.createCallProfile(serviceType, callType);
+ return c.createCallProfile(serviceType, callType);
} catch (RemoteException e) {
throw new ImsException("createCallProfile()", e,
ImsReasonInfo.CODE_LOCAL_IMS_SERVICE_DOWN);
@@ -1939,6 +2342,27 @@ public class ImsManager implements IFeatureConnector {
}
/**
+ * Informs the {@link ImsService} of the {@link RtpHeaderExtensionType}s which the framework
+ * intends to use for incoming and outgoing calls.
+ * <p>
+ * See {@link RtpHeaderExtensionType} for more information.
+ * @param types The RTP header extension types to use for incoming and outgoing calls, or
+ * empty list if none defined.
+ * @throws ImsException
+ */
+ public void setOfferedRtpHeaderExtensionTypes(@NonNull Set<RtpHeaderExtensionType> types)
+ throws ImsException {
+ MmTelFeatureConnection c = getOrThrowExceptionIfServiceUnavailable();
+
+ try {
+ c.changeOfferedRtpHeaderExtensionTypes(types);
+ } catch (RemoteException e) {
+ throw new ImsException("setOfferedRtpHeaderExtensionTypes()", e,
+ ImsReasonInfo.CODE_LOCAL_IMS_SERVICE_DOWN);
+ }
+ }
+
+ /**
* Creates a {@link ImsCall} to make a call.
*
* @param profile a call profile to make the call
@@ -1954,7 +2378,8 @@ public class ImsManager implements IFeatureConnector {
log("makeCall :: profile=" + profile);
}
- checkAndThrowExceptionIfServiceUnavailable();
+ // Check we are still alive
+ getOrThrowExceptionIfServiceUnavailable();
ImsCall call = new ImsCall(mContext, profile);
@@ -1979,7 +2404,8 @@ public class ImsManager implements IFeatureConnector {
*/
public ImsCall takeCall(IImsCallSession session, ImsCall.Listener listener)
throws ImsException {
- checkAndThrowExceptionIfServiceUnavailable();
+ // Check we are still alive
+ getOrThrowExceptionIfServiceUnavailable();
try {
if (session == null) {
throw new ImsException("No pending session for the call",
@@ -2006,9 +2432,9 @@ public class ImsManager implements IFeatureConnector {
*/
@UnsupportedAppUsage
public ImsConfig getConfigInterface() throws ImsException {
- checkAndThrowExceptionIfServiceUnavailable();
+ MmTelFeatureConnection c = getOrThrowExceptionIfServiceUnavailable();
- IImsConfig config = mMmTelFeatureConnection.getConfigInterface();
+ IImsConfig config = c.getConfig();
if (config == null) {
throw new ImsException("getConfigInterface()",
ImsReasonInfo.CODE_LOCAL_SERVICE_UNAVAILABLE);
@@ -2016,39 +2442,42 @@ public class ImsManager implements IFeatureConnector {
return new ImsConfig(config);
}
- public void changeMmTelCapability(
- @MmTelFeature.MmTelCapabilities.MmTelCapability int capability,
- @ImsRegistrationImplBase.ImsRegistrationTech int radioTech,
- boolean isEnabled) throws ImsException {
-
+ /**
+ * Enable or disable a capability for multiple radio technologies.
+ */
+ public void changeMmTelCapability(boolean isEnabled, int capability,
+ int... radioTechs) throws ImsException {
CapabilityChangeRequest request = new CapabilityChangeRequest();
if (isEnabled) {
- request.addCapabilitiesToEnableForTech(capability, radioTech);
+ for (int tech : radioTechs) {
+ request.addCapabilitiesToEnableForTech(capability, tech);
+ }
} else {
- request.addCapabilitiesToDisableForTech(capability, radioTech);
+ for (int tech : radioTechs) {
+ request.addCapabilitiesToDisableForTech(capability, tech);
+ }
}
changeMmTelCapability(request);
}
- public void changeMmTelCapability(CapabilityChangeRequest r) throws ImsException {
- checkAndThrowExceptionIfServiceUnavailable();
+ private void changeMmTelCapability(CapabilityChangeRequest r) throws ImsException {
+ MmTelFeatureConnection c = getOrThrowExceptionIfServiceUnavailable();
try {
logi("changeMmTelCapability: changing capabilities for sub: " + getSubId()
+ ", request: " + r);
- mMmTelFeatureConnection.changeEnabledCapabilities(r, null);
- if (mImsConfigListener == null) {
+ c.changeEnabledCapabilities(r, null);
+ ImsStatsCallback cb = getStatsCallback(mPhoneId);
+ if (cb == null) {
return;
}
for (CapabilityChangeRequest.CapabilityPair enabledCaps : r.getCapabilitiesToEnable()) {
- mImsConfigListener.onSetFeatureResponse(enabledCaps.getCapability(),
- enabledCaps.getRadioTech(),
- ProvisioningManager.PROVISIONING_VALUE_ENABLED, -1);
+ cb.onEnabledMmTelCapabilitiesChanged(enabledCaps.getCapability(),
+ enabledCaps.getRadioTech(), true);
}
for (CapabilityChangeRequest.CapabilityPair disabledCaps :
r.getCapabilitiesToDisable()) {
- mImsConfigListener.onSetFeatureResponse(disabledCaps.getCapability(),
- disabledCaps.getRadioTech(),
- ProvisioningManager.PROVISIONING_VALUE_DISABLED, -1);
+ cb.onEnabledMmTelCapabilitiesChanged(disabledCaps.getCapability(),
+ disabledCaps.getRadioTech(), false);
}
} catch (RemoteException e) {
throw new ImsException("changeMmTelCapability(CCR)", e,
@@ -2056,7 +2485,7 @@ public class ImsManager implements IFeatureConnector {
}
}
- public boolean updateRttConfigValue() {
+ private boolean updateRttConfigValue() {
// If there's no active sub anywhere on the device, enable RTT on the modem so that
// the device can make an emergency call.
@@ -2065,10 +2494,20 @@ public class ImsManager implements IFeatureConnector {
getBooleanCarrierConfig(CarrierConfigManager.KEY_RTT_SUPPORTED_BOOL)
|| !isActiveSubscriptionPresent;
- boolean isRttUiSettingEnabled = Settings.Secure.getInt(mContext.getContentResolver(),
- Settings.Secure.RTT_CALLING_MODE, 0) != 0;
+ int defaultRttMode =
+ getIntCarrierConfig(CarrierConfigManager.KEY_DEFAULT_RTT_MODE_INT);
+ int rttMode = mSettingsProxy.getSecureIntSetting(mContext.getContentResolver(),
+ Settings.Secure.RTT_CALLING_MODE, defaultRttMode);
+ logi("defaultRttMode = " + defaultRttMode + " rttMode = " + rttMode);
boolean isRttAlwaysOnCarrierConfig = getBooleanCarrierConfig(
CarrierConfigManager.KEY_IGNORE_RTT_MODE_SETTING_BOOL);
+ if (isRttAlwaysOnCarrierConfig && rttMode == defaultRttMode) {
+ mSettingsProxy.putSecureIntSetting(mContext.getContentResolver(),
+ Settings.Secure.RTT_CALLING_MODE, defaultRttMode);
+ }
+
+ boolean isRttUiSettingEnabled = mSettingsProxy.getSecureIntSetting(
+ mContext.getContentResolver(), Settings.Secure.RTT_CALLING_MODE, 0) != 0;
boolean shouldImsRttBeOn = isRttUiSettingEnabled || isRttAlwaysOnCarrierConfig;
logi("update RTT: settings value: " + isRttUiSettingEnabled + " always-on carrierconfig: "
@@ -2086,7 +2525,7 @@ public class ImsManager implements IFeatureConnector {
private void setRttConfig(boolean enabled) {
final int value = enabled ? ProvisioningManager.PROVISIONING_VALUE_ENABLED :
ProvisioningManager.PROVISIONING_VALUE_DISABLED;
- mExecutorFactory.executeRunnable(() -> {
+ getImsThreadExecutor().execute(() -> {
try {
logi("Setting RTT enabled to " + enabled);
getConfigInterface().setProvisionedValue(
@@ -2100,13 +2539,12 @@ public class ImsManager implements IFeatureConnector {
public boolean queryMmTelCapability(
@MmTelFeature.MmTelCapabilities.MmTelCapability int capability,
@ImsRegistrationImplBase.ImsRegistrationTech int radioTech) throws ImsException {
- checkAndThrowExceptionIfServiceUnavailable();
+ MmTelFeatureConnection c = getOrThrowExceptionIfServiceUnavailable();
BlockingQueue<Boolean> result = new LinkedBlockingDeque<>(1);
try {
- mMmTelFeatureConnection.queryEnabledCapabilities(capability, radioTech,
- new IImsCapabilityCallback.Stub() {
+ c.queryEnabledCapabilities(capability, radioTech, new IImsCapabilityCallback.Stub() {
@Override
public void onQueryCapabilityConfiguration(int resCap, int resTech,
boolean enabled) {
@@ -2142,7 +2580,7 @@ public class ImsManager implements IFeatureConnector {
public boolean queryMmTelCapabilityStatus(
@MmTelFeature.MmTelCapabilities.MmTelCapability int capability,
@ImsRegistrationImplBase.ImsRegistrationTech int radioTech) throws ImsException {
- checkAndThrowExceptionIfServiceUnavailable();
+ MmTelFeatureConnection c = getOrThrowExceptionIfServiceUnavailable();
if (getRegistrationTech() != radioTech)
return false;
@@ -2150,7 +2588,7 @@ public class ImsManager implements IFeatureConnector {
try {
MmTelFeature.MmTelCapabilities capabilities =
- mMmTelFeatureConnection.queryCapabilityStatus();
+ c.queryCapabilityStatus();
return capabilities.isCapable(capability);
} catch (RemoteException e) {
@@ -2159,27 +2597,30 @@ public class ImsManager implements IFeatureConnector {
}
}
+ /**
+ * Enable the RTT configuration on this device.
+ */
public void setRttEnabled(boolean enabled) {
- try {
- if (enabled) {
- setEnhanced4gLteModeSetting(enabled);
- } else {
- setAdvanced4GMode(enabled || isEnhanced4gLteModeSettingEnabledByUser());
- }
- setRttConfig(enabled);
- } catch (ImsException e) {
- loge("Unable to set RTT enabled to " + enabled + ": " + e);
+ if (enabled) {
+ // Override this setting if RTT is enabled.
+ setEnhanced4gLteModeSetting(true /*enabled*/);
}
+ setRttConfig(enabled);
}
/**
* Set the TTY mode. This is the actual tty mode (varies depending on peripheral status)
*/
public void setTtyMode(int ttyMode) throws ImsException {
- if (!getBooleanCarrierConfig(
- CarrierConfigManager.KEY_CARRIER_VOLTE_TTY_SUPPORTED_BOOL)) {
- setAdvanced4GMode((ttyMode == TelecomManager.TTY_MODE_OFF) &&
- isEnhanced4gLteModeSettingEnabledByUser());
+ boolean isNonTtyOrTtyOnVolteEnabled = isTtyOnVoLteCapable() ||
+ (ttyMode == TelecomManager.TTY_MODE_OFF);
+ logi("setTtyMode: isNonTtyOrTtyOnVolteEnabled=" + isNonTtyOrTtyOnVolteEnabled);
+ CapabilityChangeRequest request = new CapabilityChangeRequest();
+ updateVoiceCellFeatureValue(request, isNonTtyOrTtyOnVolteEnabled);
+ updateVideoCallFeatureValue(request, isNonTtyOrTtyOnVolteEnabled);
+ if (isImsNeeded(request)) {
+ changeMmTelCapability(request);
+ turnOnIms();
}
}
@@ -2199,48 +2640,32 @@ public class ImsManager implements IFeatureConnector {
public void setUiTTYMode(Context context, int uiTtyMode, Message onComplete)
throws ImsException {
- checkAndThrowExceptionIfServiceUnavailable();
-
+ MmTelFeatureConnection c = getOrThrowExceptionIfServiceUnavailable();
try {
- mMmTelFeatureConnection.setUiTTYMode(uiTtyMode, onComplete);
+ c.setUiTTYMode(uiTtyMode, onComplete);
} catch (RemoteException e) {
throw new ImsException("setTTYMode()", e,
ImsReasonInfo.CODE_LOCAL_IMS_SERVICE_DOWN);
}
}
- private ImsReasonInfo makeACopy(ImsReasonInfo imsReasonInfo) {
- Parcel p = Parcel.obtain();
- imsReasonInfo.writeToParcel(p, 0);
- p.setDataPosition(0);
- ImsReasonInfo clonedReasonInfo = ImsReasonInfo.CREATOR.createFromParcel(p);
- p.recycle();
- return clonedReasonInfo;
+ public int getImsServiceState() throws ImsException {
+ MmTelFeatureConnection c = getOrThrowExceptionIfServiceUnavailable();
+ return c.getFeatureState();
}
- /**
- * Get Recent IMS Disconnect Reasons.
- *
- * @return ArrayList of ImsReasonInfo objects. MAX size of the arraylist
- * is MAX_RECENT_DISCONNECT_REASONS. The objects are in the
- * chronological order.
- */
- public ArrayList<ImsReasonInfo> getRecentImsDisconnectReasons() {
- ArrayList<ImsReasonInfo> disconnectReasons = new ArrayList<>();
-
- for (ImsReasonInfo reason : mRecentDisconnectReasons) {
- disconnectReasons.add(makeACopy(reason));
- }
- return disconnectReasons;
+ @Override
+ public void updateFeatureState(int state) {
+ mMmTelConnectionRef.get().updateFeatureState(state);
}
@Override
- public int getImsServiceState() throws ImsException {
- return mMmTelFeatureConnection.getFeatureState();
+ public void updateFeatureCapabilities(long capabilities) {
+ mMmTelConnectionRef.get().updateFeatureCapabilities(capabilities);
}
public void getImsServiceState(Consumer<Integer> result) {
- mExecutorFactory.executeRunnable(() -> {
+ getImsThreadExecutor().execute(() -> {
try {
result.accept(getImsServiceState());
} catch (ImsException e) {
@@ -2250,11 +2675,11 @@ public class ImsManager implements IFeatureConnector {
});
}
- private Executor getThreadExecutor() {
- if (Looper.myLooper() == null) {
- Looper.prepare();
- }
- return new HandlerExecutor(new Handler(Looper.myLooper()));
+ /**
+ * @return An Executor that should be used to execute potentially long-running operations.
+ */
+ private Executor getImsThreadExecutor() {
+ return mExecutor;
}
/**
@@ -2298,43 +2723,117 @@ public class ImsManager implements IFeatureConnector {
}
/**
+ * Get the int[] config from carrier config manager.
+ *
+ * @param key config key defined in CarrierConfigManager
+ * @return int[] values of the corresponding key.
+ */
+ private int[] getIntArrayCarrierConfig(String key) {
+ PersistableBundle b = null;
+ if (mConfigManager != null) {
+ // If an invalid subId is used, this bundle will contain default values.
+ b = mConfigManager.getConfigForSubId(getSubId());
+ }
+ if (b != null) {
+ return b.getIntArray(key);
+ } else {
+ // Return static default defined in CarrierConfigManager.
+ return CarrierConfigManager.getDefaultConfig().getIntArray(key);
+ }
+ }
+
+ /**
* Checks to see if the ImsService Binder is connected. If it is not, we try to create the
* connection again.
*/
- private void checkAndThrowExceptionIfServiceUnavailable()
+ private MmTelFeatureConnection getOrThrowExceptionIfServiceUnavailable()
throws ImsException {
if (!isImsSupportedOnDevice(mContext)) {
throw new ImsException("IMS not supported on device.",
ImsReasonInfo.CODE_LOCAL_IMS_NOT_SUPPORTED_ON_DEVICE);
}
- if (mMmTelFeatureConnection == null || !mMmTelFeatureConnection.isBinderAlive()) {
- createImsService();
-
- if (mMmTelFeatureConnection == null) {
- throw new ImsException("Service is unavailable",
- ImsReasonInfo.CODE_LOCAL_IMS_SERVICE_DOWN);
- }
+ MmTelFeatureConnection c = mMmTelConnectionRef.get();
+ if (c == null || !c.isBinderAlive()) {
+ throw new ImsException("Service is unavailable",
+ ImsReasonInfo.CODE_LOCAL_IMS_SERVICE_DOWN);
}
+ return c;
}
- /**
- * Creates a connection to the ImsService associated with this slot.
- */
- private void createImsService() {
- mMmTelFeatureConnection = MmTelFeatureConnection.create(mContext, mPhoneId);
+ @Override
+ public void registerFeatureCallback(int slotId, IImsServiceFeatureCallback cb) {
+ try {
+ ITelephony telephony = mBinderCache.listenOnBinder(cb, () -> {
+ try {
+ cb.imsFeatureRemoved(
+ FeatureConnector.UNAVAILABLE_REASON_SERVER_UNAVAILABLE);
+ } catch (RemoteException ignore) {} // This is local.
+ });
- // Forwarding interface to tell mStatusCallbacks that the Proxy is unavailable.
- mMmTelFeatureConnection.setStatusCallback(new FeatureConnection.IFeatureUpdate() {
- @Override
- public void notifyStateChanged() {
- mStatusCallbacks.forEach(FeatureConnection.IFeatureUpdate::notifyStateChanged);
+ if (telephony != null) {
+ telephony.registerMmTelFeatureCallback(slotId, cb);
+ } else {
+ cb.imsFeatureRemoved(FeatureConnector.UNAVAILABLE_REASON_SERVER_UNAVAILABLE);
}
+ } catch (ServiceSpecificException e) {
+ try {
+ switch (e.errorCode) {
+ case android.telephony.ims.ImsException.CODE_ERROR_UNSUPPORTED_OPERATION:
+ cb.imsFeatureRemoved(FeatureConnector.UNAVAILABLE_REASON_IMS_UNSUPPORTED);
+ break;
+ default: {
+ cb.imsFeatureRemoved(
+ FeatureConnector.UNAVAILABLE_REASON_SERVER_UNAVAILABLE);
+ }
+ }
+ } catch (RemoteException ignore) {} // Already dead anyway if this happens.
+ } catch (RemoteException e) {
+ try {
+ cb.imsFeatureRemoved(FeatureConnector.UNAVAILABLE_REASON_SERVER_UNAVAILABLE);
+ } catch (RemoteException ignore) {} // Already dead if this happens.
+ }
+ }
- @Override
- public void notifyUnavailable() {
- mStatusCallbacks.forEach(FeatureConnection.IFeatureUpdate::notifyUnavailable);
+ @Override
+ public void unregisterFeatureCallback(IImsServiceFeatureCallback cb) {
+ try {
+ ITelephony telephony = mBinderCache.removeRunnable(cb);
+ if (telephony != null) {
+ telephony.unregisterImsFeatureCallback(cb);
}
- });
+ } catch (RemoteException e) {
+ // This means that telephony died, so do not worry about it.
+ loge("unregisterImsFeatureCallback (MMTEL), RemoteException: " + e.getMessage());
+ }
+ }
+
+ @Override
+ public void associate(ImsFeatureContainer c) {
+ if (c == null) {
+ mMmTelConnectionRef.set(mMmTelFeatureConnectionFactory.create(
+ mContext, mPhoneId, null, null, null, null));
+ } else {
+ mMmTelConnectionRef.set(mMmTelFeatureConnectionFactory.create(
+ mContext, mPhoneId, IImsMmTelFeature.Stub.asInterface(c.imsFeature),
+ c.imsConfig, c.imsRegistration, c.sipTransport));
+ }
+ }
+
+ @Override
+ public void invalidate() {
+ mMmTelConnectionRef.get().onRemovedOrDied();
+ }
+
+ private ITelephony getITelephony() {
+ return mBinderCache.getBinder();
+ }
+
+ private static ITelephony getITelephonyInterface() {
+ return ITelephony.Stub.asInterface(
+ TelephonyFrameworkInitializer
+ .getTelephonyServiceManager()
+ .getTelephonyServiceRegisterer()
+ .get());
}
/**
@@ -2346,8 +2845,9 @@ public class ImsManager implements IFeatureConnector {
*/
private ImsCallSession createCallSession(ImsCallProfile profile) throws ImsException {
try {
+ MmTelFeatureConnection c = mMmTelConnectionRef.get();
// Throws an exception if the ImsService Feature is not ready to accept commands.
- return new ImsCallSession(mMmTelFeatureConnection.createCallSession(profile));
+ return new ImsCallSession(c.createCallSession(profile));
} catch (RemoteException e) {
logw("CreateCallSession: Error, remote exception: " + e.getMessage());
throw new ImsException("createCallSession()", e,
@@ -2357,23 +2857,23 @@ public class ImsManager implements IFeatureConnector {
}
private void log(String s) {
- Rlog.d(TAG + " [" + mPhoneId + "]", s);
+ Rlog.d(TAG + mLogTagPostfix + " [" + mPhoneId + "]", s);
}
private void logi(String s) {
- Rlog.i(TAG + " [" + mPhoneId + "]", s);
+ Rlog.i(TAG + mLogTagPostfix + " [" + mPhoneId + "]", s);
}
private void logw(String s) {
- Rlog.w(TAG + " [" + mPhoneId + "]", s);
+ Rlog.w(TAG + mLogTagPostfix + " [" + mPhoneId + "]", s);
}
private void loge(String s) {
- Rlog.e(TAG + " [" + mPhoneId + "]", s);
+ Rlog.e(TAG + mLogTagPostfix + " [" + mPhoneId + "]", s);
}
private void loge(String s, Throwable t) {
- Rlog.e(TAG + " [" + mPhoneId + "]", s, t);
+ Rlog.e(TAG + mLogTagPostfix + " [" + mPhoneId + "]", s, t);
}
/**
@@ -2391,60 +2891,6 @@ public class ImsManager implements IFeatureConnector {
|| !isWfcEnabledByUser());
}
- private void setLteFeatureValues(boolean turnOn) {
- log("setLteFeatureValues: " + turnOn);
- CapabilityChangeRequest request = new CapabilityChangeRequest();
- if (turnOn) {
- request.addCapabilitiesToEnableForTech(
- MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VOICE,
- ImsRegistrationImplBase.REGISTRATION_TECH_LTE);
- } else {
- request.addCapabilitiesToDisableForTech(
- MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VOICE,
- ImsRegistrationImplBase.REGISTRATION_TECH_LTE);
- }
-
- if (isVtEnabledByPlatform()) {
- boolean ignoreDataEnabledChanged = getBooleanCarrierConfig(
- CarrierConfigManager.KEY_IGNORE_DATA_ENABLED_CHANGED_FOR_VIDEO_CALLS);
- boolean enableViLte = turnOn && isVtEnabledByUser() &&
- (ignoreDataEnabledChanged || isDataEnabled());
- if (enableViLte) {
- request.addCapabilitiesToEnableForTech(
- MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VIDEO,
- ImsRegistrationImplBase.REGISTRATION_TECH_LTE);
- } else {
- request.addCapabilitiesToDisableForTech(
- MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VIDEO,
- ImsRegistrationImplBase.REGISTRATION_TECH_LTE);
- }
- }
- try {
- mMmTelFeatureConnection.changeEnabledCapabilities(request, null);
- } catch (RemoteException e) {
- loge("setLteFeatureValues: Exception: " + e.getMessage());
- }
- }
-
- private void setAdvanced4GMode(boolean turnOn) throws ImsException {
- checkAndThrowExceptionIfServiceUnavailable();
-
- // if turnOn: first set feature values then call turnOnIms()
- // if turnOff: only set feature values if IMS turn off is not allowed. If turn off is
- // allowed, first call turnOffIms() then set feature values
- if (turnOn) {
- setLteFeatureValues(turnOn);
- log("setAdvanced4GMode: turnOnIms");
- turnOnIms();
- } else {
- if (isImsTurnOffAllowed()) {
- log("setAdvanced4GMode: turnOffIms");
- turnOffIms();
- }
- setLteFeatureValues(turnOn);
- }
- }
-
/**
* Used for turning off IMS completely in order to make the device CSFB'ed.
* Once turned off, all calls will be over CS.
@@ -2455,45 +2901,29 @@ public class ImsManager implements IFeatureConnector {
tm.disableIms(mPhoneId);
}
- private void addToRecentDisconnectReasons(ImsReasonInfo reason) {
- if (reason == null) return;
- while (mRecentDisconnectReasons.size() >= MAX_RECENT_DISCONNECT_REASONS) {
- mRecentDisconnectReasons.removeFirst();
- }
- mRecentDisconnectReasons.addLast(reason);
- }
-
/**
* Gets the ECBM interface to request ECBM exit.
+ * <p>
+ * This should only be called after {@link #open} has been called.
*
* @return the ECBM interface instance
* @throws ImsException if getting the ECBM interface results in an error
*/
public ImsEcbm getEcbmInterface() throws ImsException {
- if (mEcbm != null && mEcbm.isBinderAlive()) {
- return mEcbm;
- }
-
- checkAndThrowExceptionIfServiceUnavailable();
- try {
- IImsEcbm iEcbm = mMmTelFeatureConnection.getEcbmInterface();
+ MmTelFeatureConnection c = getOrThrowExceptionIfServiceUnavailable();
+ ImsEcbm iEcbm = c.getEcbmInterface();
- if (iEcbm == null) {
- throw new ImsException("getEcbmInterface()",
- ImsReasonInfo.CODE_ECBM_NOT_SUPPORTED);
- }
- mEcbm = new ImsEcbm(iEcbm);
- } catch (RemoteException e) {
- throw new ImsException("getEcbmInterface()", e,
- ImsReasonInfo.CODE_LOCAL_IMS_SERVICE_DOWN);
+ if (iEcbm == null) {
+ throw new ImsException("getEcbmInterface()",
+ ImsReasonInfo.CODE_ECBM_NOT_SUPPORTED);
}
- return mEcbm;
+ return iEcbm;
}
public void sendSms(int token, int messageRef, String format, String smsc, boolean isRetry,
byte[] pdu) throws ImsException {
try {
- mMmTelFeatureConnection.sendSms(token, messageRef, format, smsc, isRetry, pdu);
+ mMmTelConnectionRef.get().sendSms(token, messageRef, format, smsc, isRetry, pdu);
} catch (RemoteException e) {
throw new ImsException("sendSms()", e, ImsReasonInfo.CODE_LOCAL_IMS_SERVICE_DOWN);
}
@@ -2501,7 +2931,7 @@ public class ImsManager implements IFeatureConnector {
public void acknowledgeSms(int token, int messageRef, int result) throws ImsException {
try {
- mMmTelFeatureConnection.acknowledgeSms(token, messageRef, result);
+ mMmTelConnectionRef.get().acknowledgeSms(token, messageRef, result);
} catch (RemoteException e) {
throw new ImsException("acknowledgeSms()", e,
ImsReasonInfo.CODE_LOCAL_IMS_SERVICE_DOWN);
@@ -2510,7 +2940,7 @@ public class ImsManager implements IFeatureConnector {
public void acknowledgeSmsReport(int token, int messageRef, int result) throws ImsException{
try {
- mMmTelFeatureConnection.acknowledgeSmsReport(token, messageRef, result);
+ mMmTelConnectionRef.get().acknowledgeSmsReport(token, messageRef, result);
} catch (RemoteException e) {
throw new ImsException("acknowledgeSmsReport()", e,
ImsReasonInfo.CODE_LOCAL_IMS_SERVICE_DOWN);
@@ -2519,7 +2949,7 @@ public class ImsManager implements IFeatureConnector {
public String getSmsFormat() throws ImsException{
try {
- return mMmTelFeatureConnection.getSmsFormat();
+ return mMmTelConnectionRef.get().getSmsFormat();
} catch (RemoteException e) {
throw new ImsException("getSmsFormat()", e,
ImsReasonInfo.CODE_LOCAL_IMS_SERVICE_DOWN);
@@ -2528,7 +2958,7 @@ public class ImsManager implements IFeatureConnector {
public void setSmsListener(IImsSmsListener listener) throws ImsException {
try {
- mMmTelFeatureConnection.setSmsListener(listener);
+ mMmTelConnectionRef.get().setSmsListener(listener);
} catch (RemoteException e) {
throw new ImsException("setSmsListener()", e,
ImsReasonInfo.CODE_LOCAL_IMS_SERVICE_DOWN);
@@ -2537,7 +2967,7 @@ public class ImsManager implements IFeatureConnector {
public void onSmsReady() throws ImsException {
try {
- mMmTelFeatureConnection.onSmsReady();
+ mMmTelConnectionRef.get().onSmsReady();
} catch (RemoteException e) {
throw new ImsException("onSmsReady()", e,
ImsReasonInfo.CODE_LOCAL_IMS_SERVICE_DOWN);
@@ -2559,7 +2989,7 @@ public class ImsManager implements IFeatureConnector {
public @MmTelFeature.ProcessCallResult int shouldProcessCall(boolean isEmergency,
String[] numbers) throws ImsException {
try {
- return mMmTelFeatureConnection.shouldProcessCall(isEmergency, numbers);
+ return mMmTelConnectionRef.get().shouldProcessCall(isEmergency, numbers);
} catch (RemoteException e) {
throw new ImsException("shouldProcessCall()", e,
ImsReasonInfo.CODE_LOCAL_IMS_SERVICE_DOWN);
@@ -2567,34 +2997,6 @@ public class ImsManager implements IFeatureConnector {
}
/**
- * Gets the Multi-Endpoint interface to subscribe to multi-enpoint notifications..
- *
- * @return the multi-endpoint interface instance
- * @throws ImsException if getting the multi-endpoint interface results in an error
- */
- public ImsMultiEndpoint getMultiEndpointInterface() throws ImsException {
- if (mMultiEndpoint != null && mMultiEndpoint.isBinderAlive()) {
- return mMultiEndpoint;
- }
-
- checkAndThrowExceptionIfServiceUnavailable();
- try {
- IImsMultiEndpoint iImsMultiEndpoint = mMmTelFeatureConnection.getMultiEndpointInterface();
-
- if (iImsMultiEndpoint == null) {
- throw new ImsException("getMultiEndpointInterface()",
- ImsReasonInfo.CODE_MULTIENDPOINT_NOT_SUPPORTED);
- }
- mMultiEndpoint = new ImsMultiEndpoint(iImsMultiEndpoint);
- } catch (RemoteException e) {
- throw new ImsException("getMultiEndpointInterface()", e,
- ImsReasonInfo.CODE_LOCAL_IMS_SERVICE_DOWN);
- }
-
- return mMultiEndpoint;
- }
-
- /**
* Resets ImsManager settings back to factory defaults.
*
* @deprecated Doesn't support MSIM devices. Use {@link #factoryReset()} instead.
@@ -2602,8 +3004,8 @@ public class ImsManager implements IFeatureConnector {
* @hide
*/
public static void factoryReset(Context context) {
- ImsManager mgr = ImsManager.getInstance(context,
- SubscriptionManager.getDefaultVoicePhoneId());
+ DefaultSubscriptionManagerProxy p = new DefaultSubscriptionManagerProxy(context);
+ ImsManager mgr = ImsManager.getInstance(context, p.getDefaultVoicePhoneId());
if (mgr != null) {
mgr.factoryReset();
}
@@ -2617,48 +3019,52 @@ public class ImsManager implements IFeatureConnector {
*/
public void factoryReset() {
int subId = getSubId();
- if (isSubIdValid(subId)) {
- // Set VoLTE to default
- SubscriptionManager.setSubscriptionProperty(subId,
- SubscriptionManager.ENHANCED_4G_MODE_ENABLED,
- Integer.toString(SUB_PROPERTY_NOT_INITIALIZED));
-
- // Set VoWiFi to default
- SubscriptionManager.setSubscriptionProperty(subId,
- SubscriptionManager.WFC_IMS_ENABLED,
- Integer.toString(SUB_PROPERTY_NOT_INITIALIZED));
-
- // Set VoWiFi mode to default
- SubscriptionManager.setSubscriptionProperty(subId,
- SubscriptionManager.WFC_IMS_MODE,
- Integer.toString(SUB_PROPERTY_NOT_INITIALIZED));
-
- // Set VoWiFi roaming to default
- SubscriptionManager.setSubscriptionProperty(subId,
- SubscriptionManager.WFC_IMS_ROAMING_ENABLED,
- Integer.toString(SUB_PROPERTY_NOT_INITIALIZED));
-
- // Set VoWiFi roaming mode to default
- SubscriptionManager.setSubscriptionProperty(subId,
- SubscriptionManager.WFC_IMS_ROAMING_MODE,
- Integer.toString(SUB_PROPERTY_NOT_INITIALIZED));
-
-
- // Set VT to default
- SubscriptionManager.setSubscriptionProperty(subId,
- SubscriptionManager.VT_IMS_ENABLED,
- Integer.toString(SUB_PROPERTY_NOT_INITIALIZED));
-
- // Set RCS UCE to default
- SubscriptionManager.setSubscriptionProperty(subId,
- SubscriptionManager.IMS_RCS_UCE_ENABLED, Integer.toString(
- SUBINFO_PROPERTY_FALSE));
- } else {
- loge("factoryReset: invalid sub id, can not reset siminfo db settings; subId=" + subId);
+ if (!isSubIdValid(subId)) {
+ loge("factoryReset: invalid sub id, can not reset siminfo db settings; subId="
+ + subId);
+ return;
+ }
+ // Set VoLTE to default
+ mSubscriptionManagerProxy.setSubscriptionProperty(subId,
+ SubscriptionManager.ENHANCED_4G_MODE_ENABLED,
+ Integer.toString(SUB_PROPERTY_NOT_INITIALIZED));
+
+ // Set VoWiFi to default
+ mSubscriptionManagerProxy.setSubscriptionProperty(subId,
+ SubscriptionManager.WFC_IMS_ENABLED,
+ Integer.toString(SUB_PROPERTY_NOT_INITIALIZED));
+
+ // Set VoWiFi mode to default
+ mSubscriptionManagerProxy.setSubscriptionProperty(subId,
+ SubscriptionManager.WFC_IMS_MODE,
+ Integer.toString(SUB_PROPERTY_NOT_INITIALIZED));
+
+ // Set VoWiFi roaming to default
+ mSubscriptionManagerProxy.setSubscriptionProperty(subId,
+ SubscriptionManager.WFC_IMS_ROAMING_ENABLED,
+ Integer.toString(SUB_PROPERTY_NOT_INITIALIZED));
+
+ // Set VoWiFi roaming mode to default
+ mSubscriptionManagerProxy.setSubscriptionProperty(subId,
+ SubscriptionManager.WFC_IMS_ROAMING_MODE,
+ Integer.toString(SUB_PROPERTY_NOT_INITIALIZED));
+
+
+ // Set VT to default
+ mSubscriptionManagerProxy.setSubscriptionProperty(subId,
+ SubscriptionManager.VT_IMS_ENABLED,
+ Integer.toString(SUB_PROPERTY_NOT_INITIALIZED));
+
+ // Set RCS UCE to default
+ mSubscriptionManagerProxy.setSubscriptionProperty(subId,
+ SubscriptionManager.IMS_RCS_UCE_ENABLED, Integer.toString(
+ SUBINFO_PROPERTY_FALSE));
+ // Push settings
+ try {
+ reevaluateCapabilities();
+ } catch (ImsException e) {
+ loge("factoryReset, exception: " + e);
}
-
- // Push settings to ImsConfig
- updateImsServiceConfig(true);
}
public void setVolteProvisioned(boolean isProvisioned) {
@@ -2690,7 +3096,13 @@ public class ImsManager implements IFeatureConnector {
}
private boolean isDataEnabled() {
- return new TelephonyManager(mContext, getSubId()).isDataConnectionAllowed();
+ TelephonyManager tm = mContext.getSystemService(TelephonyManager.class);
+ if (tm == null) {
+ loge("isDataEnabled: TelephonyManager not available, returning false...");
+ return false;
+ }
+ tm = tm.createForSubscriptionId(getSubId());
+ return tm.isDataConnectionAllowed();
}
private boolean isVolteProvisioned() {
@@ -2717,13 +3129,165 @@ public class ImsManager implements IFeatureConnector {
return bool ? "1" : "0";
}
+ public int getConfigInt(int key) throws ImsException {
+ if (isLocalImsConfigKey(key)) {
+ return getLocalImsConfigKeyInt(key);
+ } else {
+ return getConfigInterface().getConfigInt(key);
+ }
+ }
+
+ public String getConfigString(int key) throws ImsException {
+ if (isLocalImsConfigKey(key)) {
+ return getLocalImsConfigKeyString(key);
+ } else {
+ return getConfigInterface().getConfigString(key);
+ }
+ }
+
+ public int setConfig(int key, int value) throws ImsException, RemoteException {
+ if (isLocalImsConfigKey(key)) {
+ return setLocalImsConfigKeyInt(key, value);
+ } else {
+ return getConfigInterface().setConfig(key, value);
+ }
+ }
+
+ public int setConfig(int key, String value) throws ImsException, RemoteException {
+ if (isLocalImsConfigKey(key)) {
+ return setLocalImsConfigKeyString(key, value);
+ } else {
+ return getConfigInterface().setConfig(key, value);
+ }
+ }
+
+ /**
+ * Gets the configuration value that supported in frameworks.
+ *
+ * @param key, as defined in com.android.ims.ProvisioningManager.
+ * @return the value in Integer format
+ */
+ private int getLocalImsConfigKeyInt(int key) {
+ int result = ProvisioningManager.PROVISIONING_RESULT_UNKNOWN;
+
+ switch (key) {
+ case KEY_VOIMS_OPT_IN_STATUS:
+ result = isVoImsOptInEnabled() ? 1 : 0;
+ break;
+ }
+ log("getLocalImsConfigKeInt() for key:" + key + ", result: " + result);
+ return result;
+ }
+
+ /**
+ * Gets the configuration value that supported in frameworks.
+ *
+ * @param key, as defined in com.android.ims.ProvisioningManager.
+ * @return the value in String format
+ */
+ private String getLocalImsConfigKeyString(int key) {
+ String result = "";
+
+ switch (key) {
+ case KEY_VOIMS_OPT_IN_STATUS:
+ result = booleanToPropertyString(isVoImsOptInEnabled());
+
+ break;
+ }
+ log("getLocalImsConfigKeyString() for key:" + key + ", result: " + result);
+ return result;
+ }
+
+ /**
+ * Sets the configuration value that supported in frameworks.
+ *
+ * @param key, as defined in com.android.ims.ProvisioningManager.
+ * @param value in Integer format.
+ * @return as defined in com.android.ims.ProvisioningManager#OperationStatusConstants
+ */
+ private int setLocalImsConfigKeyInt(int key, int value) throws ImsException, RemoteException {
+ int result = ImsConfig.OperationStatusConstants.UNKNOWN;
+
+ switch (key) {
+ case KEY_VOIMS_OPT_IN_STATUS:
+ result = setVoImsOptInSetting(value);
+ reevaluateCapabilities();
+ break;
+ }
+ log("setLocalImsConfigKeyInt() for" +
+ " key: " + key +
+ ", value: " + value +
+ ", result: " + result);
+
+ // Notify ims config changed
+ MmTelFeatureConnection c = getOrThrowExceptionIfServiceUnavailable();
+ IImsConfig config = c.getConfig();
+ config.notifyIntImsConfigChanged(key, value);
+
+ return result;
+ }
+
+ /**
+ * Sets the configuration value that supported in frameworks.
+ *
+ * @param key, as defined in com.android.ims.ProvisioningManager.
+ * @param value in String format.
+ * @return as defined in com.android.ims.ProvisioningManager#OperationStatusConstants
+ */
+ private int setLocalImsConfigKeyString(int key, String value)
+ throws ImsException, RemoteException {
+ int result = ImsConfig.OperationStatusConstants.UNKNOWN;
+
+ switch (key) {
+ case KEY_VOIMS_OPT_IN_STATUS:
+ result = setVoImsOptInSetting(Integer.parseInt(value));
+ reevaluateCapabilities();
+ break;
+ }
+ log("setLocalImsConfigKeyString() for" +
+ " key: " + key +
+ ", value: " + value +
+ ", result: " + result);
+
+ // Notify ims config changed
+ MmTelFeatureConnection c = getOrThrowExceptionIfServiceUnavailable();
+ IImsConfig config = c.getConfig();
+ config.notifyStringImsConfigChanged(key, value);
+
+ return result;
+ }
+
+ /**
+ * Check the config whether supported by framework.
+ *
+ * @param key, as defined in com.android.ims.ProvisioningManager.
+ * @return true if the config is supported by framework.
+ */
+ private boolean isLocalImsConfigKey(int key) {
+ return Arrays.stream(LOCAL_IMS_CONFIG_KEYS).anyMatch(value -> value == key);
+ }
+
+ private boolean isVoImsOptInEnabled() {
+ int setting = mSubscriptionManagerProxy.getIntegerSubscriptionProperty(
+ getSubId(), SubscriptionManager.VOIMS_OPT_IN_STATUS,
+ SUB_PROPERTY_NOT_INITIALIZED);
+ return (setting == ProvisioningManager.PROVISIONING_VALUE_ENABLED);
+ }
+
+ private int setVoImsOptInSetting(int value) {
+ mSubscriptionManagerProxy.setSubscriptionProperty(
+ getSubId(),
+ SubscriptionManager.VOIMS_OPT_IN_STATUS,
+ String.valueOf(value));
+ return ImsConfig.OperationStatusConstants.SUCCESS;
+ }
public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
pw.println("ImsManager:");
pw.println(" device supports IMS = " + isImsSupportedOnDevice(mContext));
pw.println(" mPhoneId = " + mPhoneId);
pw.println(" mConfigUpdated = " + mConfigUpdated);
- pw.println(" mImsServiceProxy = " + mMmTelFeatureConnection);
+ pw.println(" mImsServiceProxy = " + mMmTelConnectionRef.get());
pw.println(" mDataEnabled = " + isDataEnabled());
pw.println(" ignoreDataEnabledChanged = " + getBooleanCarrierConfig(
CarrierConfigManager.KEY_IGNORE_DATA_ENABLED_CHANGED_FOR_VIDEO_CALLS));
@@ -2733,6 +3297,7 @@ public class ImsManager implements IFeatureConnector {
pw.println(" isNonTtyOrTtyOnVolteEnabled = " + isNonTtyOrTtyOnVolteEnabled());
pw.println(" isVolteEnabledByPlatform = " + isVolteEnabledByPlatform());
+ pw.println(" isVoImsOptInEnabled = " + isVoImsOptInEnabled());
pw.println(" isVolteProvisionedOnDevice = " + isVolteProvisionedOnDevice());
pw.println(" isEnhanced4gLteModeSettingEnabledByUser = " +
isEnhanced4gLteModeSettingEnabledByUser());
@@ -2747,6 +3312,10 @@ public class ImsManager implements IFeatureConnector {
pw.println(" isVtProvisionedOnDevice = " + isVtProvisionedOnDevice());
pw.println(" isWfcProvisionedOnDevice = " + isWfcProvisionedOnDevice());
+
+ pw.println(" isCrossSimEnabledByPlatform = " + isCrossSimEnabledByPlatform());
+ pw.println(" isCrossSimCallingEnabledByUser = " + isCrossSimCallingEnabledByUser());
+ pw.println(" isImsOverNrEnabledByPlatform = " + isImsOverNrEnabledByPlatform());
pw.flush();
}
@@ -2757,20 +3326,18 @@ public class ImsManager implements IFeatureConnector {
* @return {@code true} if valid, {@code false} otherwise.
*/
private boolean isSubIdValid(int subId) {
- return SubscriptionManager.isValidSubscriptionId(subId) &&
+ return mSubscriptionManagerProxy.isValidSubscriptionId(subId) &&
subId != SubscriptionManager.DEFAULT_SUBSCRIPTION_ID;
}
private boolean isActiveSubscriptionPresent() {
- SubscriptionManager sm = (SubscriptionManager) mContext.getSystemService(
- Context.TELEPHONY_SUBSCRIPTION_SERVICE);
- return sm.getActiveSubscriptionIdList().length > 0;
+ return mSubscriptionManagerProxy.getActiveSubscriptionIdList().length > 0;
}
private void updateImsCarrierConfigs(PersistableBundle configs) throws ImsException {
- checkAndThrowExceptionIfServiceUnavailable();
+ MmTelFeatureConnection c = getOrThrowExceptionIfServiceUnavailable();
- IImsConfig config = mMmTelFeatureConnection.getConfigInterface();
+ IImsConfig config = c.getConfig();
if (config == null) {
throw new ImsException("getConfigInterface()",
ImsReasonInfo.CODE_LOCAL_SERVICE_UNAVAILABLE);
diff --git a/src/java/com/android/ims/ImsMultiEndpoint.java b/src/java/com/android/ims/ImsMultiEndpoint.java
index dc297b6e..7c225379 100644
--- a/src/java/com/android/ims/ImsMultiEndpoint.java
+++ b/src/java/com/android/ims/ImsMultiEndpoint.java
@@ -70,15 +70,10 @@ public class ImsMultiEndpoint {
}
public void setExternalCallStateListener(ImsExternalCallStateListener externalCallStateListener)
- throws ImsException {
- try {
- if (DBG) Rlog.d(TAG, "setExternalCallStateListener");
- mImsMultiendpoint.setListener(new ImsExternalCallStateListenerProxy(
- externalCallStateListener));
- } catch (RemoteException e) {
- throw new ImsException("setExternalCallStateListener could not be set.", e,
- ImsReasonInfo.CODE_LOCAL_IMS_SERVICE_DOWN);
- }
+ throws RemoteException {
+ if (DBG) Rlog.d(TAG, "setExternalCallStateListener");
+ mImsMultiendpoint.setListener(externalCallStateListener != null ?
+ new ImsExternalCallStateListenerProxy(externalCallStateListener) : null);
}
public boolean isBinderAlive() {
diff --git a/src/java/com/android/ims/MmTelFeatureConnection.java b/src/java/com/android/ims/MmTelFeatureConnection.java
index 4d5a1799..72013136 100644
--- a/src/java/com/android/ims/MmTelFeatureConnection.java
+++ b/src/java/com/android/ims/MmTelFeatureConnection.java
@@ -16,13 +16,15 @@
package com.android.ims;
-import android.annotation.NonNull;
import android.content.Context;
+import android.os.Binder;
import android.os.IBinder;
+import android.os.IInterface;
import android.os.Message;
import android.os.RemoteException;
-import android.telephony.TelephonyManager;
import android.telephony.ims.ImsCallProfile;
+import android.telephony.ims.ImsService;
+import android.telephony.ims.RtpHeaderExtensionType;
import android.telephony.ims.aidl.IImsCapabilityCallback;
import android.telephony.ims.aidl.IImsConfig;
import android.telephony.ims.aidl.IImsConfigCallback;
@@ -30,9 +32,10 @@ import android.telephony.ims.aidl.IImsMmTelFeature;
import android.telephony.ims.aidl.IImsRegistration;
import android.telephony.ims.aidl.IImsRegistrationCallback;
import android.telephony.ims.aidl.IImsSmsListener;
+import android.telephony.ims.aidl.ISipTransport;
import android.telephony.ims.feature.CapabilityChangeRequest;
-import android.telephony.ims.feature.ImsFeature;
import android.telephony.ims.feature.MmTelFeature;
+import android.telephony.ims.stub.ImsEcbmImplBase;
import android.telephony.ims.stub.ImsSmsImplBase;
import android.util.Log;
@@ -40,7 +43,10 @@ import com.android.ims.internal.IImsCallSession;
import com.android.ims.internal.IImsEcbm;
import com.android.ims.internal.IImsMultiEndpoint;
import com.android.ims.internal.IImsUt;
-import com.android.telephony.Rlog;
+
+import java.util.ArrayList;
+import java.util.Optional;
+import java.util.Set;
/**
* A container of the IImsServiceController binder, which implements all of the ImsFeatures that
@@ -48,7 +54,7 @@ import com.android.telephony.Rlog;
*/
public class MmTelFeatureConnection extends FeatureConnection {
- protected static final String TAG = "MmTelFeatureConnection";
+ protected static final String TAG = "MmTelFeatureConn";
private class ImsRegistrationCallbackAdapter extends
ImsCallbackAdapterManager<IImsRegistrationCallback> {
@@ -160,7 +166,7 @@ public class MmTelFeatureConnection extends FeatureConnection {
@Override
public void registerCallback(IImsConfigCallback localCallback) {
- IImsConfig binder = getConfigInterface();
+ IImsConfig binder = getConfig();
if (binder == null) {
// Config interface is not currently available.
Log.w(TAG + " [" + mSlotId + "]", "ProvisioningCallbackManager - couldn't register,"
@@ -176,7 +182,7 @@ public class MmTelFeatureConnection extends FeatureConnection {
@Override
public void unregisterCallback(IImsConfigCallback localCallback) {
- IImsConfig binder = getConfigInterface();
+ IImsConfig binder = getConfig();
if (binder == null) {
Log.w(TAG + " [" + mSlotId + "]", "ProvisioningCallbackManager - couldn't"
+ " unregister, binder is null.");
@@ -191,148 +197,86 @@ public class MmTelFeatureConnection extends FeatureConnection {
}
}
- // Updated by IImsServiceFeatureCallback when FEATURE_EMERGENCY_MMTEL is sent.
- private boolean mSupportsEmergencyCalling = false;
-
- // Cache the Registration and Config interfaces as long as the MmTel feature is connected. If
- // it becomes disconnected, invalidate.
- private IImsConfig mConfigBinder;
- private final ImsRegistrationCallbackAdapter mRegistrationCallbackManager;
- private final CapabilityCallbackManager mCapabilityCallbackManager;
- private final ProvisioningCallbackManager mProvisioningCallbackManager;
+ private static final class BinderAccessState<T> {
+ /**
+ * We have not tried to get the interface yet.
+ */
+ static final int STATE_NOT_SET = 0;
+ /**
+ * We have tried to get the interface, but it is not supported.
+ */
+ static final int STATE_NOT_SUPPORTED = 1;
+ /**
+ * The interface is available from the service.
+ */
+ static final int STATE_AVAILABLE = 2;
- public static @NonNull MmTelFeatureConnection create(Context context , int slotId) {
- MmTelFeatureConnection serviceProxy = new MmTelFeatureConnection(context, slotId);
- if (!ImsManager.isImsSupportedOnDevice(context)) {
- // Return empty service proxy in the case that IMS is not supported.
- sImsSupportedOnDevice = false;
- return serviceProxy;
+ public static <T> BinderAccessState<T> of(T value) {
+ return new BinderAccessState<>(value);
}
- TelephonyManager tm = serviceProxy.getTelephonyManager();
- if (tm == null) {
- Rlog.w(TAG + " [" + slotId + "]", "create: TelephonyManager is null!");
- // Binder can be unset in this case because it will be torn down/recreated as part of
- // a retry mechanism until the serviceProxy binder is set successfully.
- return serviceProxy;
- }
+ private final int mState;
+ private final T mInterface;
- IImsMmTelFeature binder = tm.getImsMmTelFeatureAndListen(slotId,
- serviceProxy.getListener());
- if (binder != null) {
- serviceProxy.setBinder(binder.asBinder());
- // Trigger the cache to be updated for feature status.
- serviceProxy.getFeatureState();
- } else {
- Rlog.w(TAG + " [" + slotId + "]", "create: binder is null!");
+ public BinderAccessState(int state) {
+ mState = state;
+ mInterface = null;
}
- return serviceProxy;
- }
- public MmTelFeatureConnection(Context context, int slotId) {
- super(context, slotId);
-
- mRegistrationCallbackManager = new ImsRegistrationCallbackAdapter(context, mLock);
- mCapabilityCallbackManager = new CapabilityCallbackManager(context, mLock);
- mProvisioningCallbackManager = new ProvisioningCallbackManager(context, mLock);
- }
-
- @Override
- protected void onRemovedOrDied() {
- removeImsFeatureCallback();
- synchronized (mLock) {
- super.onRemovedOrDied();
- mRegistrationCallbackManager.close();
- mCapabilityCallbackManager.close();
- mProvisioningCallbackManager.close();
- mConfigBinder = null;
+ public BinderAccessState(T binderInterface) {
+ mState = STATE_AVAILABLE;
+ mInterface = binderInterface;
}
- }
- private void removeImsFeatureCallback() {
- TelephonyManager tm = getTelephonyManager();
- if (tm != null) {
- tm.unregisterImsFeatureCallback(mSlotId, ImsFeature.FEATURE_MMTEL, getListener());
+ public int getState() {
+ return mState;
}
- }
- private IImsConfig getConfig() {
- synchronized (mLock) {
- // null if cache is invalid;
- if (mConfigBinder != null) {
- return mConfigBinder;
- }
+ public T getInterface() {
+ return mInterface;
}
- TelephonyManager tm = getTelephonyManager();
- IImsConfig configBinder = tm != null
- ? tm.getImsConfig(mSlotId, ImsFeature.FEATURE_MMTEL) : null;
- synchronized (mLock) {
- // mConfigBinder may have changed while we tried to get the config interface.
- if (mConfigBinder == null) {
- mConfigBinder = configBinder;
- }
- }
- return mConfigBinder;
}
- @Override
- protected void handleImsFeatureCreatedCallback(int slotId, int feature) {
- // The feature has been enabled. This happens when the feature is first created and
- // may happen when the feature is re-enabled.
- synchronized (mLock) {
- if(mSlotId != slotId) {
- return;
- }
- switch (feature) {
- case ImsFeature.FEATURE_MMTEL: {
- if (!mIsAvailable) {
- Log.i(TAG + " [" + mSlotId + "]", "MmTel enabled");
- mIsAvailable = true;
- }
- break;
- }
- case ImsFeature.FEATURE_EMERGENCY_MMTEL: {
- mSupportsEmergencyCalling = true;
- Log.i(TAG + " [" + mSlotId + "]", "Emergency calling enabled");
- break;
- }
- }
- }
- }
+ // Updated by IImsServiceFeatureCallback when FEATURE_EMERGENCY_MMTEL is sent.
+ private boolean mSupportsEmergencyCalling = false;
+ private BinderAccessState<ImsEcbm> mEcbm =
+ new BinderAccessState<>(BinderAccessState.STATE_NOT_SET);
+ private BinderAccessState<ImsMultiEndpoint> mMultiEndpoint =
+ new BinderAccessState<>(BinderAccessState.STATE_NOT_SET);
+ private MmTelFeature.Listener mMmTelFeatureListener;
+ private ImsUt mUt;
- @Override
- protected void handleImsFeatureRemovedCallback(int slotId, int feature) {
- synchronized (mLock) {
- if (mSlotId != slotId) {
- return;
- }
- switch (feature) {
- case ImsFeature.FEATURE_MMTEL: {
- Log.i(TAG + " [" + mSlotId + "]", "MmTel removed");
- onRemovedOrDied();
- break;
- }
- case ImsFeature.FEATURE_EMERGENCY_MMTEL: {
- mSupportsEmergencyCalling = false;
- Log.i(TAG + " [" + mSlotId + "]", "Emergency calling disabled");
- break;
- }
- }
- }
+ private final ImsRegistrationCallbackAdapter mRegistrationCallbackManager;
+ private final CapabilityCallbackManager mCapabilityCallbackManager;
+ private final ProvisioningCallbackManager mProvisioningCallbackManager;
+
+ public MmTelFeatureConnection(Context context, int slotId, IImsMmTelFeature f,
+ IImsConfig c, IImsRegistration r, ISipTransport s) {
+ super(context, slotId, c, r, s);
+
+ setBinder((f != null) ? f.asBinder() : null);
+ mRegistrationCallbackManager = new ImsRegistrationCallbackAdapter(context, mLock);
+ mCapabilityCallbackManager = new CapabilityCallbackManager(context, mLock);
+ mProvisioningCallbackManager = new ProvisioningCallbackManager(context, mLock);
}
@Override
- protected void handleImsStatusChangedCallback(int slotId, int feature, int status) {
+ protected void onRemovedOrDied() {
+ // Release all callbacks being tracked and unregister them from the connected MmTelFeature.
+ mRegistrationCallbackManager.close();
+ mCapabilityCallbackManager.close();
+ mProvisioningCallbackManager.close();
+ // Close mUt interface separately from other listeners, as it is not tied directly to
+ // calling. There is still a limitation currently that only one UT listener can be set
+ // (through ImsPhoneCallTracker), but this could be relaxed in the future via the ability
+ // to register multiple callbacks.
synchronized (mLock) {
- Log.i(TAG + " [" + mSlotId + "]", "imsStatusChanged: slot: " + slotId + " feature: "
- + ImsFeature.FEATURE_LOG_MAP.get(feature) +
- " status: " + ImsFeature.STATE_LOG_MAP.get(status));
- if (mSlotId == slotId && feature == ImsFeature.FEATURE_MMTEL) {
- mFeatureStateCached = status;
- if (mStatusCallback != null) {
- mStatusCallback.notifyStateChanged();
- }
+ if (mUt != null) {
+ mUt.close();
+ mUt = null;
}
+ closeConnection();
+ super.onRemovedOrDied();
}
}
@@ -344,28 +288,46 @@ public class MmTelFeatureConnection extends FeatureConnection {
* Opens the connection to the {@link MmTelFeature} and establishes a listener back to the
* framework. Calling this method multiple times will reset the listener attached to the
* {@link MmTelFeature}.
- * @param listener A {@link MmTelFeature.Listener} that will be used by the {@link MmTelFeature}
- * to notify the framework of updates.
+ * @param mmTelListener A {@link MmTelFeature.Listener} that will be used by the
+ * {@link MmTelFeature} to notify the framework of mmtel calling updates.
+ * @param ecbmListener Listener used to listen for ECBM updates from {@link ImsEcbmImplBase}
+ * implementation.
*/
- public void openConnection(MmTelFeature.Listener listener) throws RemoteException {
+ public void openConnection(MmTelFeature.Listener mmTelListener,
+ ImsEcbmStateListener ecbmListener,
+ ImsExternalCallStateListener multiEndpointListener) throws RemoteException {
synchronized (mLock) {
checkServiceIsReady();
- getServiceInterface(mBinder).setListener(listener);
+ mMmTelFeatureListener = mmTelListener;
+ getServiceInterface(mBinder).setListener(mmTelListener);
+ setEcbmInterface(ecbmListener);
+ setMultiEndpointInterface(multiEndpointListener);
}
}
+ /**
+ * Closes the connection to the {@link MmTelFeature} if it was previously opened via
+ * {@link #openConnection} by removing all listeners.
+ */
public void closeConnection() {
- mRegistrationCallbackManager.close();
- mCapabilityCallbackManager.close();
- mProvisioningCallbackManager.close();
- try {
- synchronized (mLock) {
- if (isBinderAlive()) {
+ synchronized (mLock) {
+ if (!isBinderAlive()) return;
+ try {
+ if (mMmTelFeatureListener != null) {
+ mMmTelFeatureListener = null;
getServiceInterface(mBinder).setListener(null);
}
+ if (mEcbm.getState() == BinderAccessState.STATE_AVAILABLE) {
+ mEcbm.getInterface().setEcbmStateListener(null);
+ mEcbm = new BinderAccessState<>(BinderAccessState.STATE_NOT_SET);
+ }
+ if (mMultiEndpoint.getState() == BinderAccessState.STATE_AVAILABLE) {
+ mMultiEndpoint.getInterface().setExternalCallStateListener(null);
+ mMultiEndpoint = new BinderAccessState<>(BinderAccessState.STATE_NOT_SET);
+ }
+ } catch (RemoteException e) {
+ Log.w(TAG + " [" + mSlotId + "]", "closeConnection: couldn't remove listeners!");
}
- } catch (RemoteException e) {
- Log.w(TAG + " [" + mSlotId + "]", "closeConnection: couldn't remove listener!");
}
}
@@ -448,6 +410,15 @@ public class MmTelFeatureConnection extends FeatureConnection {
}
}
+ public void changeOfferedRtpHeaderExtensionTypes(Set<RtpHeaderExtensionType> types)
+ throws RemoteException {
+ synchronized (mLock) {
+ checkServiceIsReady();
+ getServiceInterface(mBinder).changeOfferedRtpHeaderExtensionTypes(
+ new ArrayList<>(types));
+ }
+ }
+
public IImsCallSession createCallSession(ImsCallProfile profile)
throws RemoteException {
synchronized (mLock) {
@@ -456,21 +427,45 @@ public class MmTelFeatureConnection extends FeatureConnection {
}
}
- public IImsUt getUtInterface() throws RemoteException {
+ public ImsUt createOrGetUtInterface() throws RemoteException {
synchronized (mLock) {
+ if (mUt != null) return mUt;
+
checkServiceIsReady();
- return getServiceInterface(mBinder).getUtInterface();
+ IImsUt imsUt = getServiceInterface(mBinder).getUtInterface();
+ // This will internally set up a listener on the ImsUtImplBase interface, and there is
+ // a limitation that there can only be one. If multiple connections try to create this
+ // UT interface, it will throw an IllegalStateException.
+ mUt = (imsUt != null) ? new ImsUt(imsUt) : null;
+ return mUt;
}
}
- public IImsConfig getConfigInterface() {
- return getConfig();
+ private void setEcbmInterface(ImsEcbmStateListener ecbmListener) throws RemoteException {
+ synchronized (mLock) {
+ if (mEcbm.getState() != BinderAccessState.STATE_NOT_SET) {
+ throw new IllegalStateException("ECBM interface already open");
+ }
+
+ checkServiceIsReady();
+ IImsEcbm imsEcbm = getServiceInterface(mBinder).getEcbmInterface();
+ mEcbm = (imsEcbm != null) ? BinderAccessState.of(new ImsEcbm(imsEcbm)) :
+ new BinderAccessState<>(BinderAccessState.STATE_NOT_SUPPORTED);
+ if (mEcbm.getState() == BinderAccessState.STATE_AVAILABLE) {
+ // May throw an IllegalStateException if a listener already exists.
+ mEcbm.getInterface().setEcbmStateListener(ecbmListener);
+ }
+ }
}
- public IImsEcbm getEcbmInterface() throws RemoteException {
+ public ImsEcbm getEcbmInterface() {
synchronized (mLock) {
- checkServiceIsReady();
- return getServiceInterface(mBinder).getEcbmInterface();
+ if (mEcbm.getState() == BinderAccessState.STATE_NOT_SET) {
+ throw new IllegalStateException("ECBM interface has not been opened");
+ }
+
+ return mEcbm.getState() == BinderAccessState.STATE_AVAILABLE ?
+ mEcbm.getInterface() : null;
}
}
@@ -482,10 +477,22 @@ public class MmTelFeatureConnection extends FeatureConnection {
}
}
- public IImsMultiEndpoint getMultiEndpointInterface() throws RemoteException {
+ private void setMultiEndpointInterface(ImsExternalCallStateListener listener)
+ throws RemoteException {
synchronized (mLock) {
+ if (mMultiEndpoint.getState() != BinderAccessState.STATE_NOT_SET) {
+ throw new IllegalStateException("multiendpoint interface is already open");
+ }
+
checkServiceIsReady();
- return getServiceInterface(mBinder).getMultiEndpointInterface();
+ IImsMultiEndpoint imEndpoint = getServiceInterface(mBinder).getMultiEndpointInterface();
+ mMultiEndpoint = (imEndpoint != null)
+ ? BinderAccessState.of(new ImsMultiEndpoint(imEndpoint)) :
+ new BinderAccessState<>(BinderAccessState.STATE_NOT_SUPPORTED);
+ if (mMultiEndpoint.getState() == BinderAccessState.STATE_AVAILABLE) {
+ // May throw an IllegalStateException if a listener already exists.
+ mMultiEndpoint.getInterface().setExternalCallStateListener(listener);
+ }
}
}
@@ -562,9 +569,12 @@ public class MmTelFeatureConnection extends FeatureConnection {
}
@Override
- protected IImsRegistration getRegistrationBinder() {
- TelephonyManager tm = getTelephonyManager();
- return tm != null ? tm.getImsRegistration(mSlotId, ImsFeature.FEATURE_MMTEL) : null;
+ public void onFeatureCapabilitiesUpdated(long capabilities)
+ {
+ synchronized (mLock) {
+ mSupportsEmergencyCalling =
+ ((capabilities | ImsService.CAPABILITY_EMERGENCY_OVER_MMTEL) > 0);
+ }
}
private IImsMmTelFeature getServiceInterface(IBinder b) {
diff --git a/src/java/com/android/ims/RcsFeatureConnection.java b/src/java/com/android/ims/RcsFeatureConnection.java
index 98e5576f..b0908104 100644
--- a/src/java/com/android/ims/RcsFeatureConnection.java
+++ b/src/java/com/android/ims/RcsFeatureConnection.java
@@ -18,20 +18,26 @@ package com.android.ims;
import android.annotation.NonNull;
import android.content.Context;
+import android.net.Uri;
import android.os.IBinder;
import android.os.RemoteException;
-import android.telephony.TelephonyManager;
+import android.telephony.ims.aidl.ICapabilityExchangeEventListener;
import android.telephony.ims.aidl.IImsCapabilityCallback;
+import android.telephony.ims.aidl.IImsConfig;
import android.telephony.ims.aidl.IImsRcsFeature;
import android.telephony.ims.aidl.IImsRegistration;
import android.telephony.ims.aidl.IImsRegistrationCallback;
-import android.telephony.ims.aidl.IRcsFeatureListener;
+import android.telephony.ims.aidl.IPublishResponseCallback;
+import android.telephony.ims.aidl.IOptionsResponseCallback;
+import android.telephony.ims.aidl.ISipTransport;
+import android.telephony.ims.aidl.ISubscribeResponseCallback;
import android.telephony.ims.feature.CapabilityChangeRequest;
-import android.telephony.ims.feature.ImsFeature;
import com.android.internal.annotations.VisibleForTesting;
import com.android.telephony.Rlog;
+import java.util.List;
+
/**
* A container of the IImsServiceController binder, which implements all of the RcsFeatures that
* the platform currently supports: RCS
@@ -107,134 +113,57 @@ public class RcsFeatureConnection extends FeatureConnection {
}
}
- public static @NonNull RcsFeatureConnection create(Context context , int slotId,
- IFeatureUpdate callback) {
-
- RcsFeatureConnection serviceProxy = new RcsFeatureConnection(context, slotId, callback);
-
- if (!ImsManager.isImsSupportedOnDevice(context)) {
- // Return empty service proxy in the case that IMS is not supported.
- sImsSupportedOnDevice = false;
- Rlog.w(TAG, "create: IMS is not supported");
- return serviceProxy;
- }
-
- TelephonyManager tm =
- (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
- if (tm == null) {
- Rlog.w(TAG, "create: TelephonyManager is null");
- return serviceProxy;
- }
-
- IImsRcsFeature binder = tm.getImsRcsFeatureAndListen(slotId, serviceProxy.getListener());
- if (binder != null) {
- Rlog.d(TAG, "create: set binder");
- serviceProxy.setBinder(binder.asBinder());
- // Trigger the cache to be updated for feature status.
- serviceProxy.getFeatureState();
- } else {
- Rlog.i(TAG, "create: binder is null! Slot Id: " + slotId);
- }
- return serviceProxy;
- }
-
@VisibleForTesting
public AvailabilityCallbackManager mAvailabilityCallbackManager;
@VisibleForTesting
public RegistrationCallbackManager mRegistrationCallbackManager;
- private RcsFeatureConnection(Context context, int slotId, IFeatureUpdate callback) {
- super(context, slotId);
- setStatusCallback(callback);
+ public RcsFeatureConnection(Context context, int slotId, IImsRcsFeature feature, IImsConfig c,
+ IImsRegistration r, ISipTransport s) {
+ super(context, slotId, c, r, s);
+ setBinder(feature != null ? feature.asBinder() : null);
mAvailabilityCallbackManager = new AvailabilityCallbackManager(mContext);
mRegistrationCallbackManager = new RegistrationCallbackManager(mContext);
}
public void close() {
- removeRcsFeatureListener();
+ removeCapabilityExchangeEventListener();
mAvailabilityCallbackManager.close();
mRegistrationCallbackManager.close();
}
@Override
protected void onRemovedOrDied() {
- removeImsFeatureCallback();
+ close();
super.onRemovedOrDied();
- synchronized (mLock) {
- close();
- }
- }
-
- private void removeImsFeatureCallback() {
- TelephonyManager tm = getTelephonyManager();
- if (tm != null) {
- tm.unregisterImsFeatureCallback(mSlotId, ImsFeature.FEATURE_RCS, getListener());
- }
}
- @Override
- @VisibleForTesting
- public void handleImsFeatureCreatedCallback(int slotId, int feature) {
- logi("IMS feature created: slotId= " + slotId + ", feature=" + feature);
- if (!isUpdateForThisFeatureAndSlot(slotId, feature)) {
- return;
- }
- synchronized(mLock) {
- if (!mIsAvailable) {
- logi("RCS enabled on slotId: " + slotId);
- mIsAvailable = true;
- }
- }
- }
-
- @Override
- @VisibleForTesting
- public void handleImsFeatureRemovedCallback(int slotId, int feature) {
- logi("IMS feature removed: slotId= " + slotId + ", feature=" + feature);
- if (!isUpdateForThisFeatureAndSlot(slotId, feature)) {
- return;
- }
- synchronized(mLock) {
- logi("Rcs UCE removed on slotId: " + slotId);
- onRemovedOrDied();
- }
- }
-
- @Override
- @VisibleForTesting
- public void handleImsStatusChangedCallback(int slotId, int feature, int status) {
- logi("IMS status changed: slotId=" + slotId + ", feature=" + feature + ", status="
- + status);
- if (!isUpdateForThisFeatureAndSlot(slotId, feature)) {
- return;
- }
- synchronized(mLock) {
- mFeatureStateCached = status;
- }
- }
-
- private boolean isUpdateForThisFeatureAndSlot(int slotId, int feature) {
- if (mSlotId == slotId && feature == ImsFeature.FEATURE_RCS) {
- return true;
- }
- return false;
- }
-
- public void setRcsFeatureListener(IRcsFeatureListener listener) throws RemoteException {
+ public void setCapabilityExchangeEventListener(ICapabilityExchangeEventListener listener)
+ throws RemoteException {
synchronized (mLock) {
- checkServiceIsReady();
- getServiceInterface(mBinder).setListener(listener);
+ // Only check if service is alive. The feature status may not be READY.
+ checkServiceIsAlive();
+ getServiceInterface(mBinder).setCapabilityExchangeEventListener(listener);
}
}
- public void removeRcsFeatureListener() {
+ public void removeCapabilityExchangeEventListener() {
try {
- setRcsFeatureListener(null);
+ setCapabilityExchangeEventListener(null);
} catch (RemoteException e) {
// If we are not still connected, there is no need to fail removing.
}
}
+ private void checkServiceIsAlive() throws RemoteException {
+ if (!sImsSupportedOnDevice) {
+ throw new RemoteException("IMS is not supported on this device.");
+ }
+ if (!isBinderAlive()) {
+ throw new RemoteException("ImsServiceProxy is not alive.");
+ }
+ }
+
public int queryCapabilityStatus() throws RemoteException {
synchronized (mLock) {
checkServiceIsReady();
@@ -298,6 +227,31 @@ public class RcsFeatureConnection extends FeatureConnection {
}
}
+ public void requestPublication(String pidfXml, IPublishResponseCallback responseCallback)
+ throws RemoteException {
+ synchronized (mLock) {
+ checkServiceIsReady();
+ getServiceInterface(mBinder).publishCapabilities(pidfXml, responseCallback);
+ }
+ }
+
+ public void requestCapabilities(List<Uri> uris, ISubscribeResponseCallback c)
+ throws RemoteException {
+ synchronized (mLock) {
+ checkServiceIsReady();
+ getServiceInterface(mBinder).subscribeForCapabilities(uris, c);
+ }
+ }
+
+ public void sendOptionsCapabilityRequest(Uri contactUri, List<String> myCapabilities,
+ IOptionsResponseCallback callback) throws RemoteException {
+ synchronized (mLock) {
+ checkServiceIsReady();
+ getServiceInterface(mBinder).sendOptionsCapabilityRequest(contactUri, myCapabilities,
+ callback);
+ }
+ }
+
@Override
@VisibleForTesting
public Integer retrieveFeatureState() {
@@ -312,9 +266,9 @@ public class RcsFeatureConnection extends FeatureConnection {
}
@Override
- protected IImsRegistration getRegistrationBinder() {
- TelephonyManager tm = getTelephonyManager();
- return tm != null ? tm.getImsRegistration(mSlotId, ImsFeature.FEATURE_RCS) : null;
+ public void onFeatureCapabilitiesUpdated(long capabilities)
+ {
+ // doesn't do anything for RCS yet.
}
@VisibleForTesting
diff --git a/src/java/com/android/ims/RcsFeatureManager.java b/src/java/com/android/ims/RcsFeatureManager.java
index 7f1f8191..af2298aa 100644
--- a/src/java/com/android/ims/RcsFeatureManager.java
+++ b/src/java/com/android/ims/RcsFeatureManager.java
@@ -18,32 +18,47 @@ package com.android.ims;
import android.content.Context;
import android.net.Uri;
+import android.os.IBinder;
import android.os.PersistableBundle;
import android.os.RemoteException;
+import android.os.ServiceSpecificException;
+import android.telephony.BinderCacheManager;
import android.telephony.CarrierConfigManager;
import android.telephony.SubscriptionManager;
-import android.telephony.ims.RcsContactUceCapability;
+import android.telephony.TelephonyFrameworkInitializer;
+import android.telephony.ims.ImsException;
+import android.telephony.ims.ImsService;
+import android.telephony.ims.RcsUceAdapter.StackPublishTriggerType;
import android.telephony.ims.RegistrationManager;
+import android.telephony.ims.aidl.ICapabilityExchangeEventListener;
import android.telephony.ims.aidl.IImsCapabilityCallback;
+import android.telephony.ims.aidl.IImsConfig;
+import android.telephony.ims.aidl.IImsRcsController;
+import android.telephony.ims.aidl.IImsRcsFeature;
+import android.telephony.ims.aidl.IImsRegistration;
import android.telephony.ims.aidl.IImsRegistrationCallback;
-import android.telephony.ims.aidl.IRcsFeatureListener;
+import android.telephony.ims.aidl.IOptionsRequestCallback;
+import android.telephony.ims.aidl.IOptionsResponseCallback;
+import android.telephony.ims.aidl.IPublishResponseCallback;
+import android.telephony.ims.aidl.ISipTransport;
+import android.telephony.ims.aidl.ISubscribeResponseCallback;
import android.telephony.ims.feature.CapabilityChangeRequest;
+import android.telephony.ims.feature.ImsFeature;
import android.telephony.ims.feature.RcsFeature;
import android.telephony.ims.feature.RcsFeature.RcsImsCapabilities;
import android.telephony.ims.stub.ImsRegistrationImplBase;
-import android.telephony.ims.stub.RcsCapabilityExchange;
-import android.telephony.ims.stub.RcsPresenceExchangeImplBase;
-import android.telephony.ims.stub.RcsSipOptionsImplBase;
import android.util.Log;
-import com.android.ims.FeatureConnection.IFeatureUpdate;
+import com.android.ims.internal.IImsServiceFeatureCallback;
import com.android.internal.annotations.VisibleForTesting;
import com.android.telephony.Rlog;
+import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
@@ -53,7 +68,7 @@ import java.util.function.Consumer;
* - Registering/Unregistering availability/registration callbacks.
* - Querying Registration and Capability information.
*/
-public class RcsFeatureManager implements IFeatureConnector {
+public class RcsFeatureManager implements FeatureUpdates {
private static final String TAG = "RcsFeatureManager";
private static boolean DBG = true;
@@ -61,129 +76,98 @@ public class RcsFeatureManager implements IFeatureConnector {
private static final int CAPABILITY_PRESENCE = RcsImsCapabilities.CAPABILITY_TYPE_PRESENCE_UCE;
/**
- * Callbacks from the RcsFeature, which have an empty default implementation and can be
- * overridden for each Feature.
+ * The capability exchange event callbacks from the RcsFeature.
*/
- public static class RcsFeatureCallbacks {
- /** See {@link RcsCapabilityExchange#onCommandUpdate(int, int)} */
- void onCommandUpdate(int commandCode, int operationToken) {}
-
- /** See {@link RcsPresenceExchangeImplBase#onNetworkResponse(int, String, int)} */
- public void onNetworkResponse(int code, String reason, int operationToken) {}
-
- /** See {@link RcsPresenceExchangeImplBase#onCapabilityRequestResponse(List, int)} */
- public void onCapabilityRequestResponsePresence(List<RcsContactUceCapability> infos,
- int operationToken) {}
-
- /** See {@link RcsPresenceExchangeImplBase#onNotifyUpdateCapabilites(int)} */
- public void onNotifyUpdateCapabilities(int publishTriggerType) {}
-
- /** See {@link RcsPresenceExchangeImplBase#onUnpublish()} */
- public void onUnpublish() {}
+ public interface CapabilityExchangeEventCallback {
+ /**
+ * Triggered by RcsFeature to publish the device's capabilities to the network.
+ */
+ void onRequestPublishCapabilities(@StackPublishTriggerType int publishTriggerType);
/**
- * See {@link RcsSipOptionsImplBase#onCapabilityRequestResponse(int,String,
- * RcsContactUceCapability, int)}
+ * Notify that the devices is unpublished.
*/
- public void onCapabilityRequestResponseOptions(int code, String reason,
- RcsContactUceCapability info, int operationToken) {}
+ void onUnpublish();
/**
- * See {@link RcsSipOptionsImplBase#onRemoteCapabilityRequest(Uri, RcsContactUceCapability,
- * int)}
+ * Receive a capabilities request from the remote client.
*/
- public void onRemoteCapabilityRequest(Uri contactUri, RcsContactUceCapability remoteInfo,
- int operationToken) {}
+ void onRemoteCapabilityRequest(Uri contactUri,
+ List<String> remoteCapabilities, IOptionsRequestCallback cb);
}
- private final IRcsFeatureListener mRcsFeatureCallbackAdapter = new IRcsFeatureListener.Stub() {
- @Override
- public void onCommandUpdate(int commandCode, int operationToken) {
- mRcsFeatureCallbacks.forEach(listener-> listener.onCommandUpdate(commandCode,
- operationToken));
- }
-
- @Override
- public void onNetworkResponse(int code, String reason, int operationToken) {
- mRcsFeatureCallbacks.forEach(listener-> listener.onNetworkResponse(code, reason,
- operationToken));
- }
-
- @Override
- public void onCapabilityRequestResponsePresence(List<RcsContactUceCapability> infos,
- int operationToken) {
- mRcsFeatureCallbacks.forEach(listener-> listener.onCapabilityRequestResponsePresence(
- infos, operationToken));
- }
-
- @Override
- public void onNotifyUpdateCapabilities(int publishTriggerType) {
- mRcsFeatureCallbacks.forEach(listener-> listener.onNotifyUpdateCapabilities(
- publishTriggerType));
- }
-
- @Override
- public void onUnpublish() {
- mRcsFeatureCallbacks.forEach(listener-> listener.onUnpublish());
- }
+ /*
+ * Setup the listener to listen to the requests and updates from ImsService.
+ */
+ private ICapabilityExchangeEventListener mCapabilityEventListener =
+ new ICapabilityExchangeEventListener.Stub() {
+ @Override
+ public void onRequestPublishCapabilities(@StackPublishTriggerType int type) {
+ mCapabilityEventCallback.forEach(
+ callback -> callback.onRequestPublishCapabilities(type));
+ }
- @Override
- public void onCapabilityRequestResponseOptions(int code, String reason,
- RcsContactUceCapability info, int operationToken) {
- mRcsFeatureCallbacks.forEach(listener -> listener.onCapabilityRequestResponseOptions(
- code, reason, info, operationToken));
- }
+ @Override
+ public void onUnpublish() {
+ mCapabilityEventCallback.forEach(callback -> callback.onUnpublish());
+ }
- @Override
- public void onRemoteCapabilityRequest(Uri contactUri, RcsContactUceCapability remoteInfo,
- int operationToken) {
- mRcsFeatureCallbacks.forEach(listener -> listener.onRemoteCapabilityRequest(
- contactUri, remoteInfo, operationToken));
- }
- };
+ @Override
+ public void onRemoteCapabilityRequest(Uri contactUri,
+ List<String> remoteCapabilities, IOptionsRequestCallback cb) {
+ mCapabilityEventCallback.forEach(
+ callback -> callback.onRemoteCapabilityRequest(
+ contactUri, remoteCapabilities, cb));
+ }
+ };
private final int mSlotId;
private final Context mContext;
- @VisibleForTesting
- public final Set<IFeatureUpdate> mStatusCallbacks = new CopyOnWriteArraySet<>();
- private final Set<RcsFeatureCallbacks> mRcsFeatureCallbacks = new CopyOnWriteArraySet<>();
+ private final Set<CapabilityExchangeEventCallback> mCapabilityEventCallback
+ = new CopyOnWriteArraySet<>();
+ private final BinderCacheManager<IImsRcsController> mBinderCache
+ = new BinderCacheManager<>(RcsFeatureManager::getIImsRcsControllerInterface);
@VisibleForTesting
public RcsFeatureConnection mRcsFeatureConnection;
- public RcsFeatureManager(Context context, int slotId) {
- mContext = context;
- mSlotId = slotId;
-
- createImsService();
+ /**
+ * Use to obtain a FeatureConnector, which will maintain a consistent listener to the
+ * RcsFeature attached to the specified slotId. If the RcsFeature changes (due to things like
+ * SIM swap), a new RcsFeatureManager will be delivered to this Listener.
+ * @param context The Context this connector should use.
+ * @param slotId The slotId associated with the Listener and requested RcsFeature
+ * @param listener The listener, which will be used to generate RcsFeatureManager instances.
+ * @param executor The executor that the Listener callbacks will be called on.
+ * @param logPrefix The prefix used in logging of the FeatureConnector for notable events.
+ * @return A FeatureConnector, which will start delivering RcsFeatureManagers as the underlying
+ * RcsFeature instances become available to the platform.
+ * @see {@link FeatureConnector#connect()}.
+ */
+ public static FeatureConnector<RcsFeatureManager> getConnector(Context context, int slotId,
+ FeatureConnector.Listener<RcsFeatureManager> listener, Executor executor,
+ String logPrefix) {
+ ArrayList<Integer> filter = new ArrayList<>();
+ filter.add(ImsFeature.STATE_READY);
+ return new FeatureConnector<>(context, slotId, RcsFeatureManager::new, logPrefix, filter,
+ listener, executor);
}
- // Binds the IMS service to the RcsFeature instance.
- private void createImsService() {
- mRcsFeatureConnection = RcsFeatureConnection.create(mContext, mSlotId,
- new IFeatureUpdate() {
- @Override
- public void notifyStateChanged() {
- mStatusCallbacks.forEach(
- FeatureConnection.IFeatureUpdate::notifyStateChanged);
- }
- @Override
- public void notifyUnavailable() {
- logi("RcsFeature is unavailable");
- mStatusCallbacks.forEach(
- FeatureConnection.IFeatureUpdate::notifyUnavailable);
- }
- });
+ /**
+ * Use {@link #getConnector} to get an instance of this class.
+ */
+ private RcsFeatureManager(Context context, int slotId) {
+ mContext = context;
+ mSlotId = slotId;
}
/**
* Opens a persistent connection to the RcsFeature. This must be called before the RcsFeature
- * can be used to communicate. Triggers a {@link RcsFeature#onFeatureReady()} call on the
- * service side.
+ * can be used to communicate.
*/
public void openConnection() throws android.telephony.ims.ImsException {
try {
- mRcsFeatureConnection.setRcsFeatureListener(mRcsFeatureCallbackAdapter);
+ mRcsFeatureConnection.setCapabilityExchangeEventListener(mCapabilityEventListener);
} catch (RemoteException e){
throw new android.telephony.ims.ImsException("Service is not available.",
android.telephony.ims.ImsException.CODE_ERROR_SERVICE_UNAVAILABLE);
@@ -196,39 +180,38 @@ public class RcsFeatureManager implements IFeatureConnector {
*/
public void releaseConnection() {
try {
- mRcsFeatureConnection.setRcsFeatureListener(null);
+ mRcsFeatureConnection.setCapabilityExchangeEventListener(null);
} catch (RemoteException e){
// Connection may not be available at this point.
}
- mStatusCallbacks.clear();
mRcsFeatureConnection.close();
- mRcsFeatureCallbacks.clear();
+ mCapabilityEventCallback.clear();
}
/**
- * Adds a callback for {@link RcsFeatureCallbacks}.
+ * Adds a callback for {@link CapabilityExchangeEventCallback}.
* Note: These callbacks will be sent on the binder thread used to notify the callback.
*/
- public void addFeatureListenerCallback(RcsFeatureCallbacks listener) {
- mRcsFeatureCallbacks.add(listener);
+ public void addCapabilityEventCallback(CapabilityExchangeEventCallback listener) {
+ mCapabilityEventCallback.add(listener);
}
/**
- * Removes an existing {@link RcsFeatureCallbacks}.
+ * Removes an existing {@link CapabilityExchangeEventCallback}.
*/
- public void removeFeatureListenerCallback(RcsFeatureCallbacks listener) {
- mRcsFeatureCallbacks.remove(listener);
+ public void removeCapabilityEventCallback(CapabilityExchangeEventCallback listener) {
+ mCapabilityEventCallback.remove(listener);
}
/**
* Update the capabilities for this RcsFeature.
*/
- public void updateCapabilities() throws android.telephony.ims.ImsException {
- boolean optionsSupport = isOptionsSupported();
- boolean presenceSupported = isPresenceSupported();
+ public void updateCapabilities(int newSubId) throws android.telephony.ims.ImsException {
+ boolean optionsSupport = isOptionsSupported(newSubId);
+ boolean presenceSupported = isPresenceSupported(newSubId);
- logi("Update capabilities for slot " + mSlotId + ": options=" + optionsSupport
- + ", presence=" + presenceSupported);
+ logi("Update capabilities for slot " + mSlotId + " and sub " + newSubId + ": options="
+ + optionsSupport+ ", presence=" + presenceSupported);
if (optionsSupport || presenceSupported) {
CapabilityChangeRequest request = new CapabilityChangeRequest();
@@ -326,6 +309,30 @@ public class RcsFeatureManager implements IFeatureConnector {
mRcsFeatureConnection.removeCallbackForSubscription(subId, callback);
}
+ public boolean isImsServiceCapable(@ImsService.ImsServiceCapability long capabilities)
+ throws ImsException {
+ try {
+ return mRcsFeatureConnection.isCapable(capabilities);
+ } catch (RemoteException e) {
+ throw new ImsException(e.getMessage(), ImsException.CODE_ERROR_SERVICE_UNAVAILABLE);
+ }
+ }
+
+ /**
+ * @return The SipTransport interface if it exists or {@code null} if it does not exist due to
+ * the ImsService not supporting it.
+ */
+ public ISipTransport getSipTransport() throws ImsException {
+ if (!isImsServiceCapable(ImsService.CAPABILITY_SIP_DELEGATE_CREATION)) {
+ return null;
+ }
+ return mRcsFeatureConnection.getSipTransport();
+ }
+
+ public IImsRegistration getImsRegistration() {
+ return mRcsFeatureConnection.getRegistration();
+ }
+
/**
* Query for the specific capability.
*/
@@ -383,9 +390,13 @@ public class RcsFeatureManager implements IFeatureConnector {
/**
* Query the availability of an IMS RCS capability.
*/
- public boolean isAvailable(@RcsImsCapabilities.RcsImsCapabilityFlag int capability)
+ public boolean isAvailable(@RcsImsCapabilities.RcsImsCapabilityFlag int capability,
+ @ImsRegistrationImplBase.ImsRegistrationTech int radioTech)
throws android.telephony.ims.ImsException {
try {
+ if (mRcsFeatureConnection.getRegistrationTech() != radioTech) {
+ return false;
+ }
int currentStatus = mRcsFeatureConnection.queryCapabilityStatus();
return new RcsImsCapabilities(currentStatus).isCapable(capability);
} catch (RemoteException e) {
@@ -396,49 +407,46 @@ public class RcsFeatureManager implements IFeatureConnector {
}
/**
- * Adds a callback for status changed events if the binder is already available. If it is not,
- * this method will throw an ImsException.
- */
- @Override
- public void addNotifyStatusChangedCallbackIfAvailable(FeatureConnection.IFeatureUpdate c)
- throws android.telephony.ims.ImsException {
- if (!mRcsFeatureConnection.isBinderAlive()) {
- throw new android.telephony.ims.ImsException("Can not connect to service.",
- android.telephony.ims.ImsException.CODE_ERROR_SERVICE_UNAVAILABLE);
- }
- if (c != null) {
- mStatusCallbacks.add(c);
- }
- }
-
- @Override
- public void removeNotifyStatusChangedCallback(FeatureConnection.IFeatureUpdate c) {
- if (c != null) {
- mStatusCallbacks.remove(c);
- }
- }
-
- /**
* Add UCE capabilities with given type.
* @param capability the specific RCS UCE capability wants to enable
*/
public void addRcsUceCapability(CapabilityChangeRequest request,
@RcsImsCapabilities.RcsImsCapabilityFlag int capability) {
request.addCapabilitiesToEnableForTech(capability,
+ ImsRegistrationImplBase.REGISTRATION_TECH_NR);
+ request.addCapabilitiesToEnableForTech(capability,
ImsRegistrationImplBase.REGISTRATION_TECH_LTE);
request.addCapabilitiesToEnableForTech(capability,
ImsRegistrationImplBase.REGISTRATION_TECH_IWLAN);
}
+ public void requestPublication(String pidfXml, IPublishResponseCallback responseCallback)
+ throws RemoteException {
+ mRcsFeatureConnection.requestPublication(pidfXml, responseCallback);
+ }
+
+ public void requestCapabilities(List<Uri> uris, ISubscribeResponseCallback c)
+ throws RemoteException {
+ mRcsFeatureConnection.requestCapabilities(uris, c);
+ }
+
+ public void sendOptionsCapabilityRequest(Uri contactUri, List<String> myCapabilities,
+ IOptionsResponseCallback callback) throws RemoteException {
+ mRcsFeatureConnection.sendOptionsCapabilityRequest(contactUri, myCapabilities, callback);
+ }
+
/**
* Disable all of the UCE capabilities.
*/
private void disableAllRcsUceCapabilities() throws android.telephony.ims.ImsException {
+ final int techNr = ImsRegistrationImplBase.REGISTRATION_TECH_NR;
final int techLte = ImsRegistrationImplBase.REGISTRATION_TECH_LTE;
final int techIWlan = ImsRegistrationImplBase.REGISTRATION_TECH_IWLAN;
CapabilityChangeRequest request = new CapabilityChangeRequest();
+ request.addCapabilitiesToDisableForTech(CAPABILITY_OPTIONS, techNr);
request.addCapabilitiesToDisableForTech(CAPABILITY_OPTIONS, techLte);
request.addCapabilitiesToDisableForTech(CAPABILITY_OPTIONS, techIWlan);
+ request.addCapabilitiesToDisableForTech(CAPABILITY_PRESENCE, techNr);
request.addCapabilitiesToDisableForTech(CAPABILITY_PRESENCE, techLte);
request.addCapabilitiesToDisableForTech(CAPABILITY_PRESENCE, techIWlan);
sendCapabilityChangeRequest(request);
@@ -455,50 +463,128 @@ public class RcsFeatureManager implements IFeatureConnector {
}
}
- private boolean isOptionsSupported() {
- return isCapabilityTypeSupported(mContext, mSlotId, CAPABILITY_OPTIONS);
+ private boolean isOptionsSupported(int subId) {
+ return isCapabilityTypeSupported(mContext, subId, CAPABILITY_OPTIONS);
}
- private boolean isPresenceSupported() {
- return isCapabilityTypeSupported(mContext, mSlotId, CAPABILITY_PRESENCE);
+ private boolean isPresenceSupported(int subId) {
+ return isCapabilityTypeSupported(mContext, subId, CAPABILITY_PRESENCE);
}
/*
* Check if the given type of capability is supported.
*/
private static boolean isCapabilityTypeSupported(
- Context context, int slotId, int capabilityType) {
+ Context context, int subId, int capabilityType) {
- int subId = sSubscriptionManagerProxy.getSubId(slotId);
if (subId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
- Log.e(TAG, "isCapabilityTypeSupported: Getting subIds is failure! slotId=" + slotId);
+ Log.e(TAG, "isCapabilityTypeSupported: Invalid subId=" + subId);
return false;
}
CarrierConfigManager configManager =
(CarrierConfigManager) context.getSystemService(Context.CARRIER_CONFIG_SERVICE);
if (configManager == null) {
- Log.e(TAG, "isCapabilityTypeSupported: CarrierConfigManager is null, " + slotId);
+ Log.e(TAG, "isCapabilityTypeSupported: CarrierConfigManager is null, " + subId);
return false;
}
PersistableBundle b = configManager.getConfigForSubId(subId);
if (b == null) {
- Log.e(TAG, "isCapabilityTypeSupported: PersistableBundle is null, " + slotId);
+ Log.e(TAG, "isCapabilityTypeSupported: PersistableBundle is null, " + subId);
return false;
}
if (capabilityType == CAPABILITY_OPTIONS) {
return b.getBoolean(CarrierConfigManager.KEY_USE_RCS_SIP_OPTIONS_BOOL, false);
} else if (capabilityType == CAPABILITY_PRESENCE) {
- return b.getBoolean(CarrierConfigManager.KEY_USE_RCS_PRESENCE_BOOL, false);
+ return b.getBoolean(CarrierConfigManager.Ims.KEY_ENABLE_PRESENCE_PUBLISH_BOOL, false);
}
return false;
}
@Override
- public int getImsServiceState() throws ImsException {
- return mRcsFeatureConnection.getFeatureState();
+ public void registerFeatureCallback(int slotId, IImsServiceFeatureCallback cb) {
+ IImsRcsController controller = mBinderCache.listenOnBinder(cb, () -> {
+ try {
+ cb.imsFeatureRemoved(
+ FeatureConnector.UNAVAILABLE_REASON_SERVER_UNAVAILABLE);
+ } catch (RemoteException ignore) {} // This is local.
+ });
+
+ try {
+ if (controller == null) {
+ Log.e(TAG, "registerRcsFeatureListener: IImsRcsController is null");
+ cb.imsFeatureRemoved(FeatureConnector.UNAVAILABLE_REASON_SERVER_UNAVAILABLE);
+ return;
+ }
+ controller.registerRcsFeatureCallback(slotId, cb);
+ } catch (ServiceSpecificException e) {
+ try {
+ switch (e.errorCode) {
+ case ImsException.CODE_ERROR_UNSUPPORTED_OPERATION:
+ cb.imsFeatureRemoved(FeatureConnector.UNAVAILABLE_REASON_IMS_UNSUPPORTED);
+ break;
+ default: {
+ cb.imsFeatureRemoved(
+ FeatureConnector.UNAVAILABLE_REASON_SERVER_UNAVAILABLE);
+ }
+ }
+ } catch (RemoteException ignore) {} // Already dead anyway if this happens.
+ } catch (RemoteException e) {
+ try {
+ cb.imsFeatureRemoved(FeatureConnector.UNAVAILABLE_REASON_SERVER_UNAVAILABLE);
+ } catch (RemoteException ignore) {} // Already dead if this happens.
+ }
+ }
+
+ @Override
+ public void unregisterFeatureCallback(IImsServiceFeatureCallback cb) {
+ try {
+ IImsRcsController imsRcsController = mBinderCache.removeRunnable(cb);
+ if (imsRcsController != null) {
+ imsRcsController.unregisterImsFeatureCallback(cb);
+ }
+ } catch (RemoteException e) {
+ // This means that telephony died, so do not worry about it.
+ Rlog.e(TAG, "unregisterImsFeatureCallback (RCS), RemoteException: "
+ + e.getMessage());
+ }
+ }
+
+ private IImsRcsController getIImsRcsController() {
+ return mBinderCache.getBinder();
+ }
+
+ private static IImsRcsController getIImsRcsControllerInterface() {
+ IBinder binder = TelephonyFrameworkInitializer
+ .getTelephonyServiceManager()
+ .getTelephonyImsServiceRegisterer()
+ .get();
+ IImsRcsController c = IImsRcsController.Stub.asInterface(binder);
+ return c;
+ }
+
+ @Override
+ public void associate(ImsFeatureContainer c) {
+ IImsRcsFeature f = IImsRcsFeature.Stub.asInterface(c.imsFeature);
+ mRcsFeatureConnection = new RcsFeatureConnection(mContext, mSlotId, f, c.imsConfig,
+ c.imsRegistration, c.sipTransport);
+ }
+
+ @Override
+ public void invalidate() {
+ mRcsFeatureConnection.onRemovedOrDied();
+ }
+
+ @Override
+ public void updateFeatureState(int state) {
+ mRcsFeatureConnection.updateFeatureState(state);
+ }
+
+ @Override
+ public void updateFeatureCapabilities(long capabilities) {
+ mRcsFeatureConnection.updateFeatureCapabilities(capabilities);
}
/**
@@ -513,6 +599,10 @@ public class RcsFeatureManager implements IFeatureConnector {
int getSubId(int slotId);
}
+ public IImsConfig getConfig() {
+ return mRcsFeatureConnection.getConfig();
+ }
+
private static SubscriptionManagerProxy sSubscriptionManagerProxy
= slotId -> {
int[] subIds = SubscriptionManager.getSubId(slotId);
diff --git a/src/java/com/android/ims/internal/ConferenceParticipant.java b/src/java/com/android/ims/internal/ConferenceParticipant.java
index 12edd166..d48ecf63 100644
--- a/src/java/com/android/ims/internal/ConferenceParticipant.java
+++ b/src/java/com/android/ims/internal/ConferenceParticipant.java
@@ -119,7 +119,6 @@ public class ConferenceParticipant implements Parcelable {
callDirection);
participant.setConnectTime(connectTime);
participant.setConnectElapsedTime(elapsedRealTime);
- participant.setCallDirection(callDirection);
return participant;
}
diff --git a/src/java/com/android/ims/internal/ImsVideoCallProviderWrapper.java b/src/java/com/android/ims/internal/ImsVideoCallProviderWrapper.java
index 57bdad0f..d0592e16 100644
--- a/src/java/com/android/ims/internal/ImsVideoCallProviderWrapper.java
+++ b/src/java/com/android/ims/internal/ImsVideoCallProviderWrapper.java
@@ -19,6 +19,7 @@ package com.android.ims.internal;
import android.compat.annotation.UnsupportedAppUsage;
import android.net.Uri;
import android.os.Binder;
+import android.os.Build;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
@@ -235,7 +236,7 @@ public class ImsVideoCallProviderWrapper extends Connection.VideoProvider {
*
* @param videoProvider
*/
- @UnsupportedAppUsage
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
public ImsVideoCallProviderWrapper(IImsVideoCallProvider videoProvider)
throws RemoteException {
diff --git a/src/java/com/android/ims/rcs/uce/ControllerBase.java b/src/java/com/android/ims/rcs/uce/ControllerBase.java
new file mode 100644
index 00000000..57f0fc7f
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/ControllerBase.java
@@ -0,0 +1,44 @@
+/*
+ * 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.ims.rcs.uce;
+
+import com.android.ims.RcsFeatureManager;
+
+/**
+ * The base interface of each controllers.
+ */
+public interface ControllerBase {
+ /**
+ * The RcsFeature has been connected to the framework.
+ */
+ void onRcsConnected(RcsFeatureManager manager);
+
+ /**
+ * The framework has lost the binding to the RcsFeature.
+ */
+ void onRcsDisconnected();
+
+ /**
+ * Notify to destroy this instance. The UceController instance is unusable after destroyed.
+ */
+ void onDestroy();
+
+ /**
+ * Notify the controller that the Carrier Config has changed.
+ */
+ void onCarrierConfigChanged();
+}
diff --git a/src/java/com/android/ims/rcs/uce/OWNERS b/src/java/com/android/ims/rcs/uce/OWNERS
new file mode 100644
index 00000000..dff71c49
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/OWNERS
@@ -0,0 +1,3 @@
+jamescflin@google.com
+calvinpan@google.com
+allenwtsu@google.com \ No newline at end of file
diff --git a/src/java/com/android/ims/rcs/uce/UceController.java b/src/java/com/android/ims/rcs/uce/UceController.java
new file mode 100644
index 00000000..c6099097
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/UceController.java
@@ -0,0 +1,833 @@
+/*
+ * 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.ims.rcs.uce;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.content.Context;
+import android.net.Uri;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.telephony.ims.RcsContactUceCapability;
+import android.telephony.ims.RcsContactUceCapability.CapabilityMechanism;
+import android.telephony.ims.RcsUceAdapter;
+import android.telephony.ims.RcsUceAdapter.PublishState;
+import android.telephony.ims.RcsUceAdapter.StackPublishTriggerType;
+import android.telephony.ims.aidl.IOptionsRequestCallback;
+import android.telephony.ims.aidl.IRcsUceControllerCallback;
+import android.telephony.ims.aidl.IRcsUcePublishStateCallback;
+import android.util.IndentingPrintWriter;
+import android.util.LocalLog;
+import android.util.Log;
+
+import com.android.ims.RcsFeatureManager;
+import com.android.ims.rcs.uce.UceDeviceState.DeviceStateResult;
+import com.android.ims.rcs.uce.eab.EabCapabilityResult;
+import com.android.ims.rcs.uce.eab.EabController;
+import com.android.ims.rcs.uce.eab.EabControllerImpl;
+import com.android.ims.rcs.uce.options.OptionsController;
+import com.android.ims.rcs.uce.options.OptionsControllerImpl;
+import com.android.ims.rcs.uce.presence.publish.PublishController;
+import com.android.ims.rcs.uce.presence.publish.PublishControllerImpl;
+import com.android.ims.rcs.uce.presence.subscribe.SubscribeController;
+import com.android.ims.rcs.uce.presence.subscribe.SubscribeControllerImpl;
+import com.android.ims.rcs.uce.request.UceRequestManager;
+import com.android.ims.rcs.uce.util.UceUtils;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.os.SomeArgs;
+
+import java.io.PrintWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+
+/**
+ * The UceController will manage the RCS UCE requests on a per subscription basis. When it receives
+ * the UCE requests from the RCS applications and from the ImsService, it will coordinate the
+ * cooperation between the publish/subscribe/options components to complete the requests.
+ */
+public class UceController {
+
+ private static final String LOG_TAG = UceUtils.getLogPrefix() + "UceController";
+
+ /**
+ * The callback interface is called by the internal controllers to receive information from
+ * others controllers.
+ */
+ public interface UceControllerCallback {
+ /**
+ * Retrieve the capabilities associated with the given uris from the cache.
+ */
+ List<EabCapabilityResult> getCapabilitiesFromCache(@NonNull List<Uri> uris);
+
+ /**
+ * Retrieve the contact's capabilities from the availability cache.
+ */
+ EabCapabilityResult getAvailabilityFromCache(@NonNull Uri uri);
+
+ /**
+ * Store the given capabilities to the cache.
+ */
+ void saveCapabilities(List<RcsContactUceCapability> contactCapabilities);
+
+ /**
+ * Retrieve the device's capabilities.
+ */
+ RcsContactUceCapability getDeviceCapabilities(@CapabilityMechanism int mechanism);
+
+ /**
+ * Refresh the device state. It is called when receive the UCE request response.
+ * @param sipCode The SIP code of the request response.
+ * @param reason The reason from the network response.
+ * @param type The type of the request
+ */
+ void refreshDeviceState(int sipCode, String reason, @RequestType int type);
+
+ /**
+ * Reset the device state when then device disallowed state is expired.
+ */
+ void resetDeviceState();
+
+ /**
+ * Get the current device state to check if the device is allowed to send UCE requests.
+ */
+ DeviceStateResult getDeviceState();
+
+ /**
+ * Setup timer to exit device disallowed state.
+ */
+ void setupResetDeviceStateTimer(long resetAfterSec);
+
+ /**
+ * The device state is already reset, clear the timer.
+ */
+ void clearResetDeviceStateTimer();
+
+ /**
+ * The method is called when the given contacts' capabilities are expired and need to be
+ * refreshed.
+ */
+ void refreshCapabilities(@NonNull List<Uri> contactNumbers,
+ @NonNull IRcsUceControllerCallback callback) throws RemoteException;
+ }
+
+ /**
+ * Used to inject RequestManger instances for testing.
+ */
+ @VisibleForTesting
+ public interface RequestManagerFactory {
+ UceRequestManager createRequestManager(Context context, int subId, Looper looper,
+ UceControllerCallback callback);
+ }
+
+ private RequestManagerFactory mRequestManagerFactory = (context, subId, looper, callback) ->
+ new UceRequestManager(context, subId, looper, callback);
+
+ /**
+ * Used to inject Controller instances for testing.
+ */
+ @VisibleForTesting
+ public interface ControllerFactory {
+ /**
+ * @return an {@link EabController} associated with the subscription id specified.
+ */
+ EabController createEabController(Context context, int subId, UceControllerCallback c,
+ Looper looper);
+
+ /**
+ * @return an {@link PublishController} associated with the subscription id specified.
+ */
+ PublishController createPublishController(Context context, int subId,
+ UceControllerCallback c, Looper looper);
+
+ /**
+ * @return an {@link SubscribeController} associated with the subscription id specified.
+ */
+ SubscribeController createSubscribeController(Context context, int subId);
+
+ /**
+ * @return an {@link OptionsController} associated with the subscription id specified.
+ */
+ OptionsController createOptionsController(Context context, int subId);
+ }
+
+ private ControllerFactory mControllerFactory = new ControllerFactory() {
+ @Override
+ public EabController createEabController(Context context, int subId,
+ UceControllerCallback c, Looper looper) {
+ return new EabControllerImpl(context, subId, c, looper);
+ }
+
+ @Override
+ public PublishController createPublishController(Context context, int subId,
+ UceControllerCallback c, Looper looper) {
+ return new PublishControllerImpl(context, subId, c, looper);
+ }
+
+ @Override
+ public SubscribeController createSubscribeController(Context context, int subId) {
+ return new SubscribeControllerImpl(context, subId);
+ }
+
+ @Override
+ public OptionsController createOptionsController(Context context, int subId) {
+ return new OptionsControllerImpl(context, subId);
+ }
+ };
+
+ /**
+ * Cache the capabilities events triggered by the ImsService during the RCS connected procedure.
+ */
+ private static class CachedCapabilityEvent {
+ private Optional<Integer> mRequestPublishCapabilitiesEvent;
+ private Optional<Boolean> mUnpublishEvent;
+ private Optional<SomeArgs> mRemoteCapabilityRequestEvent;
+
+ public CachedCapabilityEvent() {
+ mRequestPublishCapabilitiesEvent = Optional.empty();
+ mUnpublishEvent = Optional.empty();
+ mRemoteCapabilityRequestEvent = Optional.empty();
+ }
+
+ /**
+ * Cache the publish capabilities request event triggered by the ImsService.
+ */
+ public synchronized void setRequestPublishCapabilitiesEvent(int triggerType) {
+ mRequestPublishCapabilitiesEvent = Optional.of(triggerType);
+ }
+
+ /**
+ * Cache the unpublish event triggered by the ImsService.
+ */
+ public synchronized void setOnUnpublishEvent() {
+ mUnpublishEvent = Optional.of(Boolean.TRUE);
+ }
+
+ /**
+ * Cache the remote capability request event triggered by the ImsService.
+ */
+ public synchronized void setRemoteCapabilityRequestEvent(Uri contactUri,
+ List<String> remoteCapabilities, IOptionsRequestCallback callback) {
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = contactUri;
+ args.arg2 = remoteCapabilities;
+ args.arg3 = callback;
+ mRemoteCapabilityRequestEvent = Optional.of(args);
+ }
+
+ /** @Return the cached publish request event */
+ public synchronized Optional<Integer> getRequestPublishEvent() {
+ return mRequestPublishCapabilitiesEvent;
+ }
+
+ /** @Return the cached unpublish event */
+ public synchronized Optional<Boolean> getUnpublishEvent() {
+ return mUnpublishEvent;
+ }
+
+ /** @Return the cached remote capability request event */
+ public synchronized Optional<SomeArgs> getRemoteCapabilityRequestEvent() {
+ return mRemoteCapabilityRequestEvent;
+ }
+
+ /** Clear the cached */
+ public synchronized void clear() {
+ mRequestPublishCapabilitiesEvent = Optional.empty();
+ mUnpublishEvent = Optional.empty();
+ mRemoteCapabilityRequestEvent.ifPresent(args -> args.recycle());
+ mRemoteCapabilityRequestEvent = Optional.empty();
+ }
+ }
+
+ /**
+ * The request type is PUBLISH.
+ */
+ public static final int REQUEST_TYPE_PUBLISH = 1;
+
+ /**
+ * The request type is CAPABILITY.
+ */
+ public static final int REQUEST_TYPE_CAPABILITY = 2;
+
+ @IntDef(value = {
+ REQUEST_TYPE_PUBLISH,
+ REQUEST_TYPE_CAPABILITY,
+ }, prefix="REQUEST_TYPE_")
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface RequestType {}
+
+ public static final Map<Integer, String> REQUEST_TYPE_DESCRIPTION = new HashMap<>();
+ static {
+ REQUEST_TYPE_DESCRIPTION.put(REQUEST_TYPE_PUBLISH, "REQUEST_TYPE_PUBLISH");
+ REQUEST_TYPE_DESCRIPTION.put(REQUEST_TYPE_CAPABILITY, "REQUEST_TYPE_CAPABILITY");
+ }
+
+ /** The RCS state is disconnected */
+ private static final int RCS_STATE_DISCONNECTED = 0;
+
+ /** The RCS state is connecting */
+ private static final int RCS_STATE_CONNECTING = 1;
+
+ /** The RCS state is connected */
+ private static final int RCS_STATE_CONNECTED = 2;
+
+ @IntDef(value = {
+ RCS_STATE_DISCONNECTED,
+ RCS_STATE_CONNECTING,
+ RCS_STATE_CONNECTED,
+ }, prefix="RCS_STATE_")
+ @Retention(RetentionPolicy.SOURCE)
+ @interface RcsConnectedState {}
+
+ private final int mSubId;
+ private final Context mContext;
+ private final LocalLog mLocalLog = new LocalLog(UceUtils.LOG_SIZE);
+
+ private volatile Looper mLooper;
+ private volatile boolean mIsDestroyedFlag;
+ private volatile @RcsConnectedState int mRcsConnectedState;
+
+ private RcsFeatureManager mRcsFeatureManager;
+ private EabController mEabController;
+ private PublishController mPublishController;
+ private SubscribeController mSubscribeController;
+ private OptionsController mOptionsController;
+ private UceRequestManager mRequestManager;
+ // The device state to execute UCE requests.
+ private UceDeviceState mDeviceState;
+ // The cache of the capability request event triggered by ImsService
+ private final CachedCapabilityEvent mCachedCapabilityEvent;
+
+ public UceController(Context context, int subId) {
+ mSubId = subId;
+ mContext = context;
+ mCachedCapabilityEvent = new CachedCapabilityEvent();
+ mRcsConnectedState = RCS_STATE_DISCONNECTED;
+ logi("create");
+
+ initLooper();
+ initControllers();
+ initRequestManager();
+ initUceDeviceState();
+ }
+
+ @VisibleForTesting
+ public UceController(Context context, int subId, UceDeviceState deviceState,
+ ControllerFactory controllerFactory, RequestManagerFactory requestManagerFactory) {
+ mSubId = subId;
+ mContext = context;
+ mDeviceState = deviceState;
+ mControllerFactory = controllerFactory;
+ mRequestManagerFactory = requestManagerFactory;
+ mCachedCapabilityEvent = new CachedCapabilityEvent();
+ mRcsConnectedState = RCS_STATE_DISCONNECTED;
+ initLooper();
+ initControllers();
+ initRequestManager();
+ }
+
+ private void initLooper() {
+ // Init the looper, it will be passed to each controller.
+ HandlerThread handlerThread = new HandlerThread("UceControllerHandlerThread");
+ handlerThread.start();
+ mLooper = handlerThread.getLooper();
+ }
+
+ private void initControllers() {
+ mEabController = mControllerFactory.createEabController(mContext, mSubId, mCtrlCallback,
+ mLooper);
+ mPublishController = mControllerFactory.createPublishController(mContext, mSubId,
+ mCtrlCallback, mLooper);
+ mSubscribeController = mControllerFactory.createSubscribeController(mContext, mSubId);
+ mOptionsController = mControllerFactory.createOptionsController(mContext, mSubId);
+ }
+
+ private void initRequestManager() {
+ mRequestManager = mRequestManagerFactory.createRequestManager(mContext, mSubId, mLooper,
+ mCtrlCallback);
+ mRequestManager.setSubscribeController(mSubscribeController);
+ mRequestManager.setOptionsController(mOptionsController);
+ }
+
+ private void initUceDeviceState() {
+ mDeviceState = new UceDeviceState(mSubId, mContext, mCtrlCallback);
+ mDeviceState.checkSendResetDeviceStateTimer();
+ }
+
+ /**
+ * The RcsFeature has been connected to the framework. This method runs on main thread.
+ */
+ public void onRcsConnected(RcsFeatureManager manager) {
+ logi("onRcsConnected");
+ // Set the RCS is connecting flag
+ mRcsConnectedState = RCS_STATE_CONNECTING;
+
+ // Listen to the capability exchange event which is triggered by the ImsService
+ mRcsFeatureManager = manager;
+ mRcsFeatureManager.addCapabilityEventCallback(mCapabilityEventListener);
+
+ // Notify each controllers that RCS is connected.
+ mEabController.onRcsConnected(manager);
+ mPublishController.onRcsConnected(manager);
+ mSubscribeController.onRcsConnected(manager);
+ mOptionsController.onRcsConnected(manager);
+
+ // Set the RCS is connected flag and check if there is any capability event received during
+ // the connecting process.
+ mRcsConnectedState = RCS_STATE_CONNECTED;
+ handleCachedCapabilityEvent();
+ }
+
+ /**
+ * The framework has lost the binding to the RcsFeature. This method runs on main thread.
+ */
+ public void onRcsDisconnected() {
+ logi("onRcsDisconnected");
+ mRcsConnectedState = RCS_STATE_DISCONNECTED;
+ // Remove the listener because RCS is disconnected.
+ if (mRcsFeatureManager != null) {
+ mRcsFeatureManager.removeCapabilityEventCallback(mCapabilityEventListener);
+ mRcsFeatureManager = null;
+ }
+ // Notify each controllers that RCS is disconnected.
+ mEabController.onRcsDisconnected();
+ mPublishController.onRcsDisconnected();
+ mSubscribeController.onRcsDisconnected();
+ mOptionsController.onRcsDisconnected();
+ }
+
+ /**
+ * Notify to destroy this instance. This instance is unusable after destroyed.
+ */
+ public void onDestroy() {
+ logi("onDestroy");
+ mIsDestroyedFlag = true;
+ // Remove the listener because the UceController instance is destroyed.
+ if (mRcsFeatureManager != null) {
+ mRcsFeatureManager.removeCapabilityEventCallback(mCapabilityEventListener);
+ mRcsFeatureManager = null;
+ }
+ // Destroy all the controllers
+ mRequestManager.onDestroy();
+ mEabController.onDestroy();
+ mPublishController.onDestroy();
+ mSubscribeController.onDestroy();
+ mOptionsController.onDestroy();
+
+ // Execute all the existing requests before quitting the looper.
+ mLooper.quitSafely();
+ }
+
+ /**
+ * Notify all associated classes that the carrier configuration has changed for the subId.
+ */
+ public void onCarrierConfigChanged() {
+ mEabController.onCarrierConfigChanged();
+ mPublishController.onCarrierConfigChanged();
+ mSubscribeController.onCarrierConfigChanged();
+ mOptionsController.onCarrierConfigChanged();
+ }
+
+ private void handleCachedCapabilityEvent() {
+ Optional<Integer> requestPublishEvent = mCachedCapabilityEvent.getRequestPublishEvent();
+ requestPublishEvent.ifPresent(triggerType ->
+ onRequestPublishCapabilitiesFromService(triggerType));
+
+ Optional<Boolean> unpublishEvent = mCachedCapabilityEvent.getUnpublishEvent();
+ unpublishEvent.ifPresent(unpublish -> onUnpublish());
+
+ Optional<SomeArgs> remoteRequest = mCachedCapabilityEvent.getRemoteCapabilityRequestEvent();
+ remoteRequest.ifPresent(args -> {
+ Uri contactUri = (Uri) args.arg1;
+ List<String> remoteCapabilities = (List<String>) args.arg2;
+ IOptionsRequestCallback callback = (IOptionsRequestCallback) args.arg3;
+ retrieveOptionsCapabilitiesForRemote(contactUri, remoteCapabilities, callback);
+ });
+ mCachedCapabilityEvent.clear();
+ }
+
+ /*
+ * The implementation of the interface UceControllerCallback. These methods are called by other
+ * controllers.
+ */
+ private UceControllerCallback mCtrlCallback = new UceControllerCallback() {
+ @Override
+ public List<EabCapabilityResult> getCapabilitiesFromCache(List<Uri> uris) {
+ return mEabController.getCapabilities(uris);
+ }
+
+ @Override
+ public EabCapabilityResult getAvailabilityFromCache(Uri contactUri) {
+ return mEabController.getAvailability(contactUri);
+ }
+
+ @Override
+ public void saveCapabilities(List<RcsContactUceCapability> contactCapabilities) {
+ mEabController.saveCapabilities(contactCapabilities);
+ }
+
+ @Override
+ public RcsContactUceCapability getDeviceCapabilities(@CapabilityMechanism int mechanism) {
+ return mPublishController.getDeviceCapabilities(mechanism);
+ }
+
+ @Override
+ public void refreshDeviceState(int sipCode, String reason, @RequestType int type) {
+ mDeviceState.refreshDeviceState(sipCode, reason, type);
+ }
+
+ @Override
+ public void resetDeviceState() {
+ mDeviceState.resetDeviceState();
+ }
+
+ @Override
+ public DeviceStateResult getDeviceState() {
+ return mDeviceState.getCurrentState();
+ }
+
+ @Override
+ public void setupResetDeviceStateTimer(long resetAfterSec) {
+ mPublishController.setupResetDeviceStateTimer(resetAfterSec);
+ }
+
+ @Override
+ public void clearResetDeviceStateTimer() {
+ mPublishController.clearResetDeviceStateTimer();
+ }
+
+ @Override
+ public void refreshCapabilities(@NonNull List<Uri> contactNumbers,
+ @NonNull IRcsUceControllerCallback callback) throws RemoteException{
+ logd("refreshCapabilities: " + contactNumbers.size());
+ UceController.this.requestCapabilitiesInternal(contactNumbers, true, callback);
+ }
+ };
+
+ @VisibleForTesting
+ public void setUceControllerCallback(UceControllerCallback callback) {
+ mCtrlCallback = callback;
+ }
+
+ /*
+ * Setup the listener to listen to the requests and updates from ImsService.
+ */
+ private RcsFeatureManager.CapabilityExchangeEventCallback mCapabilityEventListener =
+ new RcsFeatureManager.CapabilityExchangeEventCallback() {
+ @Override
+ public void onRequestPublishCapabilities(
+ @StackPublishTriggerType int triggerType) {
+ if (isRcsConnecting()) {
+ mCachedCapabilityEvent.setRequestPublishCapabilitiesEvent(triggerType);
+ return;
+ }
+ onRequestPublishCapabilitiesFromService(triggerType);
+ }
+
+ @Override
+ public void onUnpublish() {
+ if (isRcsConnecting()) {
+ mCachedCapabilityEvent.setOnUnpublishEvent();
+ return;
+ }
+ UceController.this.onUnpublish();
+ }
+
+ @Override
+ public void onRemoteCapabilityRequest(Uri contactUri,
+ List<String> remoteCapabilities, IOptionsRequestCallback cb) {
+ if (contactUri == null || remoteCapabilities == null || cb == null) {
+ logw("onRemoteCapabilityRequest: parameter cannot be null");
+ return;
+ }
+ if (isRcsConnecting()) {
+ mCachedCapabilityEvent.setRemoteCapabilityRequestEvent(contactUri,
+ remoteCapabilities, cb);
+ return;
+ }
+ retrieveOptionsCapabilitiesForRemote(contactUri, remoteCapabilities, cb);
+ }
+ };
+
+ /**
+ * Request to get the contacts' capabilities. This method will retrieve the capabilities from
+ * the cache If the capabilities are out of date, it will trigger another request to get the
+ * latest contact's capabilities from the network.
+ */
+ public void requestCapabilities(@NonNull List<Uri> uriList,
+ @NonNull IRcsUceControllerCallback c) throws RemoteException {
+ requestCapabilitiesInternal(uriList, false, c);
+ }
+
+ private void requestCapabilitiesInternal(@NonNull List<Uri> uriList, boolean skipFromCache,
+ @NonNull IRcsUceControllerCallback c) throws RemoteException {
+ if (uriList == null || uriList.isEmpty() || c == null) {
+ logw("requestCapabilities: parameter is empty");
+ if (c != null) {
+ c.onError(RcsUceAdapter.ERROR_GENERIC_FAILURE, 0L);
+ }
+ return;
+ }
+
+ if (isUnavailable()) {
+ logw("requestCapabilities: controller is unavailable");
+ c.onError(RcsUceAdapter.ERROR_GENERIC_FAILURE, 0L);
+ return;
+ }
+
+ // Return if the device is not allowed to execute UCE requests.
+ DeviceStateResult deviceStateResult = mDeviceState.getCurrentState();
+ if (deviceStateResult.isRequestForbidden()) {
+ int deviceState = deviceStateResult.getDeviceState();
+ int errorCode = deviceStateResult.getErrorCode()
+ .orElse(RcsUceAdapter.ERROR_GENERIC_FAILURE);
+ long retryAfterMillis = deviceStateResult.getRequestRetryAfterMillis();
+ logw("requestCapabilities: The device is disallowed, deviceState= " + deviceState +
+ ", errorCode=" + errorCode + ", retryAfterMillis=" + retryAfterMillis);
+ c.onError(errorCode, retryAfterMillis);
+ return;
+ }
+
+ // Trigger the capabilities request task
+ logd("requestCapabilities: size=" + uriList.size());
+ mRequestManager.sendCapabilityRequest(uriList, skipFromCache, c);
+ }
+
+ /**
+ * Request to get the contact's capabilities. It will check the availability cache first. If
+ * the capability in the availability cache is expired then it will retrieve the capability
+ * from the network.
+ */
+ public void requestAvailability(@NonNull Uri uri, @NonNull IRcsUceControllerCallback c)
+ throws RemoteException {
+ if (uri == null || c == null) {
+ logw("requestAvailability: parameter is empty");
+ if (c != null) {
+ c.onError(RcsUceAdapter.ERROR_GENERIC_FAILURE, 0L);
+ }
+ return;
+ }
+
+ if (isUnavailable()) {
+ logw("requestAvailability: controller is unavailable");
+ c.onError(RcsUceAdapter.ERROR_GENERIC_FAILURE, 0L);
+ return;
+ }
+
+ // Return if the device is not allowed to execute UCE requests.
+ DeviceStateResult deviceStateResult = mDeviceState.getCurrentState();
+ if (deviceStateResult.isRequestForbidden()) {
+ int deviceState = deviceStateResult.getDeviceState();
+ int errorCode = deviceStateResult.getErrorCode()
+ .orElse(RcsUceAdapter.ERROR_GENERIC_FAILURE);
+ long retryAfterMillis = deviceStateResult.getRequestRetryAfterMillis();
+ logw("requestAvailability: The device is disallowed, deviceState= " + deviceState +
+ ", errorCode=" + errorCode + ", retryAfterMillis=" + retryAfterMillis);
+ c.onError(errorCode, retryAfterMillis);
+ return;
+ }
+
+ // Trigger the availability request task
+ logd("requestAvailability");
+ mRequestManager.sendAvailabilityRequest(uri, c);
+ }
+
+ /**
+ * Publish the device's capabilities. This request is triggered from the ImsService.
+ */
+ public void onRequestPublishCapabilitiesFromService(@StackPublishTriggerType int triggerType) {
+ logd("onRequestPublishCapabilitiesFromService: " + triggerType);
+ // Reset the device state when the service triggers to publish the device's capabilities
+ mDeviceState.resetDeviceState();
+ // Send the publish request.
+ mPublishController.requestPublishCapabilitiesFromService(triggerType);
+ }
+
+ /**
+ * This method is triggered by the ImsService to notify framework that the device's
+ * capabilities has been unpublished from the network.
+ */
+ public void onUnpublish() {
+ logi("onUnpublish");
+ mPublishController.onUnpublish();
+ }
+
+ /**
+ * Request publish the device's capabilities. This request is from the ImsService to send the
+ * capabilities to the remote side.
+ */
+ public void retrieveOptionsCapabilitiesForRemote(@NonNull Uri contactUri,
+ @NonNull List<String> remoteCapabilities, @NonNull IOptionsRequestCallback c) {
+ logi("retrieveOptionsCapabilitiesForRemote");
+ mRequestManager.retrieveCapabilitiesForRemote(contactUri, remoteCapabilities, c);
+ }
+
+ /**
+ * Register a {@link PublishStateCallback} to receive the published state changed.
+ */
+ public void registerPublishStateCallback(@NonNull IRcsUcePublishStateCallback c) {
+ mPublishController.registerPublishStateCallback(c);
+ }
+
+ /**
+ * Removes an existing {@link PublishStateCallback}.
+ */
+ public void unregisterPublishStateCallback(@NonNull IRcsUcePublishStateCallback c) {
+ mPublishController.unregisterPublishStateCallback(c);
+ }
+
+ /**
+ * Get the UCE publish state if the PUBLISH is supported by the carrier.
+ */
+ public @PublishState int getUcePublishState() {
+ return mPublishController.getUcePublishState();
+ }
+
+ /**
+ * Add new feature tags to the Set used to calculate the capabilities in PUBLISH.
+ * <p>
+ * Used for testing ONLY.
+ * @return the new capabilities that will be used for PUBLISH.
+ */
+ public RcsContactUceCapability addRegistrationOverrideCapabilities(Set<String> featureTags) {
+ return mPublishController.addRegistrationOverrideCapabilities(featureTags);
+ }
+
+ /**
+ * Remove existing feature tags to the Set used to calculate the capabilities in PUBLISH.
+ * <p>
+ * Used for testing ONLY.
+ * @return the new capabilities that will be used for PUBLISH.
+ */
+ public RcsContactUceCapability removeRegistrationOverrideCapabilities(Set<String> featureTags) {
+ return mPublishController.removeRegistrationOverrideCapabilities(featureTags);
+ }
+
+ /**
+ * Clear all overrides in the Set used to calculate the capabilities in PUBLISH.
+ * <p>
+ * Used for testing ONLY.
+ * @return the new capabilities that will be used for PUBLISH.
+ */
+ public RcsContactUceCapability clearRegistrationOverrideCapabilities() {
+ return mPublishController.clearRegistrationOverrideCapabilities();
+ }
+
+ /**
+ * @return current RcsContactUceCapability instance that will be used for PUBLISH.
+ */
+ public RcsContactUceCapability getLatestRcsContactUceCapability() {
+ return mPublishController.getLatestRcsContactUceCapability();
+ }
+
+ /**
+ * Get the PIDF XML associated with the last successful publish or null if not PUBLISHed to the
+ * network.
+ */
+ public String getLastPidfXml() {
+ return mPublishController.getLastPidfXml();
+ }
+
+ /**
+ * Remove the device disallowed state.
+ * <p>
+ * Used for testing ONLY.
+ */
+ public void removeRequestDisallowedStatus() {
+ logd("removeRequestDisallowedStatus");
+ mDeviceState.resetDeviceState();
+ }
+
+ /**
+ * Set the milliseconds of capabilities request timeout.
+ * <p>
+ * Used for testing ONLY.
+ */
+ public void setCapabilitiesRequestTimeout(long timeoutAfterMs) {
+ logd("setCapabilitiesRequestTimeout: " + timeoutAfterMs);
+ UceUtils.setCapRequestTimeoutAfterMillis(timeoutAfterMs);
+ }
+
+ /**
+ * Get the subscription ID.
+ */
+ public int getSubId() {
+ return mSubId;
+ }
+
+ /**
+ * Check if the UceController is available.
+ * @return true if RCS is connected without destroyed.
+ */
+ public boolean isUnavailable() {
+ if (!isRcsConnected() || mIsDestroyedFlag) {
+ return true;
+ }
+ return false;
+ }
+
+ private boolean isRcsConnecting() {
+ return mRcsConnectedState == RCS_STATE_CONNECTING;
+ }
+
+ private boolean isRcsConnected() {
+ return mRcsConnectedState == RCS_STATE_CONNECTED;
+ }
+
+ public void dump(PrintWriter printWriter) {
+ IndentingPrintWriter pw = new IndentingPrintWriter(printWriter, " ");
+ pw.println("UceController" + "[subId: " + mSubId + "]:");
+ pw.increaseIndent();
+
+ pw.println("Log:");
+ pw.increaseIndent();
+ mLocalLog.dump(pw);
+ pw.decreaseIndent();
+ pw.println("---");
+
+ mPublishController.dump(pw);
+
+ pw.decreaseIndent();
+ }
+
+ private void logd(String log) {
+ Log.d(LOG_TAG, getLogPrefix().append(log).toString());
+ mLocalLog.log("[D] " + log);
+ }
+
+ private void logi(String log) {
+ Log.i(LOG_TAG, getLogPrefix().append(log).toString());
+ mLocalLog.log("[I] " + log);
+ }
+
+ private void logw(String log) {
+ Log.w(LOG_TAG, getLogPrefix().append(log).toString());
+ mLocalLog.log("[W] " + log);
+ }
+
+ private StringBuilder getLogPrefix() {
+ StringBuilder builder = new StringBuilder("[");
+ builder.append(mSubId);
+ builder.append("] ");
+ return builder;
+ }
+}
diff --git a/src/java/com/android/ims/rcs/uce/UceDeviceState.java b/src/java/com/android/ims/rcs/uce/UceDeviceState.java
new file mode 100644
index 00000000..773726a6
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/UceDeviceState.java
@@ -0,0 +1,410 @@
+/*
+ * Copyright (C) 2021 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.ims.rcs.uce;
+
+import android.annotation.IntDef;
+import android.content.Context;
+import android.telephony.ims.RcsUceAdapter.ErrorCode;
+import android.util.Log;
+
+import com.android.ims.rcs.uce.UceController.RequestType;
+import com.android.ims.rcs.uce.UceController.UceControllerCallback;
+import com.android.ims.rcs.uce.util.NetworkSipCode;
+import com.android.ims.rcs.uce.util.UceUtils;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * Manager the device state to determine whether the device is allowed to execute UCE requests or
+ * not.
+ */
+public class UceDeviceState {
+
+ private static final String LOG_TAG = UceUtils.getLogPrefix() + "UceDeviceState";
+
+ /**
+ * The device is allowed to execute UCE requests.
+ */
+ private static final int DEVICE_STATE_OK = 0;
+
+ /**
+ * The device will be in the forbidden state when the network response SIP code is 403
+ */
+ private static final int DEVICE_STATE_FORBIDDEN = 1;
+
+ /**
+ * The device will be in the PROVISION error state when the PUBLISH request fails and the
+ * SIP code is 404 NOT FOUND.
+ */
+ private static final int DEVICE_STATE_PROVISION_ERROR = 2;
+
+ /**
+ * When the network response SIP code is 489 and the carrier config also indicates that needs
+ * to handle the SIP code 489, the device will be in the BAD EVENT state.
+ */
+ private static final int DEVICE_STATE_BAD_EVENT = 3;
+
+ @IntDef(value = {
+ DEVICE_STATE_OK,
+ DEVICE_STATE_FORBIDDEN,
+ DEVICE_STATE_PROVISION_ERROR,
+ DEVICE_STATE_BAD_EVENT,
+ }, prefix="DEVICE_STATE_")
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface DeviceStateType {}
+
+ private static final Map<Integer, String> DEVICE_STATE_DESCRIPTION = new HashMap<>();
+ static {
+ DEVICE_STATE_DESCRIPTION.put(DEVICE_STATE_OK, "DEVICE_STATE_OK");
+ DEVICE_STATE_DESCRIPTION.put(DEVICE_STATE_FORBIDDEN, "DEVICE_STATE_FORBIDDEN");
+ DEVICE_STATE_DESCRIPTION.put(DEVICE_STATE_PROVISION_ERROR, "DEVICE_STATE_PROVISION_ERROR");
+ DEVICE_STATE_DESCRIPTION.put(DEVICE_STATE_BAD_EVENT, "DEVICE_STATE_BAD_EVENT");
+ }
+
+ /**
+ * The result of the current device state.
+ */
+ public static class DeviceStateResult {
+ final @DeviceStateType int mDeviceState;
+ final @ErrorCode Optional<Integer> mErrorCode;
+ final Optional<Instant> mRequestRetryTime;
+ final Optional<Instant> mExitStateTime;
+
+ public DeviceStateResult(int deviceState, Optional<Integer> errorCode,
+ Optional<Instant> requestRetryTime, Optional<Instant> exitStateTime) {
+ mDeviceState = deviceState;
+ mErrorCode = errorCode;
+ mRequestRetryTime = requestRetryTime;
+ mExitStateTime = exitStateTime;
+ }
+
+ /**
+ * Check current state to see if the UCE request is allowed to be executed.
+ */
+ public boolean isRequestForbidden() {
+ switch(mDeviceState) {
+ case DEVICE_STATE_FORBIDDEN:
+ case DEVICE_STATE_PROVISION_ERROR:
+ case DEVICE_STATE_BAD_EVENT:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ public int getDeviceState() {
+ return mDeviceState;
+ }
+
+ public Optional<Integer> getErrorCode() {
+ return mErrorCode;
+ }
+
+ public Optional<Instant> getRequestRetryTime() {
+ return mRequestRetryTime;
+ }
+
+ public long getRequestRetryAfterMillis() {
+ if (!mRequestRetryTime.isPresent()) {
+ return 0L;
+ }
+ long retryAfter = ChronoUnit.MILLIS.between(Instant.now(), mRequestRetryTime.get());
+ return (retryAfter < 0L) ? 0L : retryAfter;
+ }
+
+ public Optional<Instant> getExitStateTime() {
+ return mExitStateTime;
+ }
+
+ /**
+ * Check if the given DeviceStateResult is equal to current DeviceStateResult instance.
+ */
+ public boolean isDeviceStateEqual(DeviceStateResult otherDeviceState) {
+ if ((mDeviceState == otherDeviceState.getDeviceState()) &&
+ mErrorCode.equals(otherDeviceState.getErrorCode()) &&
+ mRequestRetryTime.equals(otherDeviceState.getRequestRetryTime()) &&
+ mExitStateTime.equals(otherDeviceState.getExitStateTime())) {
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ builder.append("DeviceState=").append(DEVICE_STATE_DESCRIPTION.get(getDeviceState()))
+ .append(", ErrorCode=").append(getErrorCode())
+ .append(", RetryTime=").append(getRequestRetryTime())
+ .append(", retryAfterMillis=").append(getRequestRetryAfterMillis())
+ .append(", ExitStateTime=").append(getExitStateTime());
+ return builder.toString();
+ }
+ }
+
+ private final int mSubId;
+ private final Context mContext;
+ private final UceControllerCallback mUceCtrlCallback;
+
+ private @DeviceStateType int mDeviceState;
+ private @ErrorCode Optional<Integer> mErrorCode;
+ private Optional<Instant> mRequestRetryTime;
+ private Optional<Instant> mExitStateTime;
+
+ public UceDeviceState(int subId, Context context, UceControllerCallback uceCtrlCallback) {
+ mSubId = subId;
+ mContext = context;
+ mUceCtrlCallback = uceCtrlCallback;
+
+ // Try to restore the device state from the shared preference.
+ boolean restoreFromPref = false;
+ Optional<DeviceStateResult> deviceState = UceUtils.restoreDeviceState(mContext, mSubId);
+ if (deviceState.isPresent()) {
+ restoreFromPref = true;
+ mDeviceState = deviceState.get().getDeviceState();
+ mErrorCode = deviceState.get().getErrorCode();
+ mRequestRetryTime = deviceState.get().getRequestRetryTime();
+ mExitStateTime = deviceState.get().getExitStateTime();
+ } else {
+ mDeviceState = DEVICE_STATE_OK;
+ mErrorCode = Optional.empty();
+ mRequestRetryTime = Optional.empty();
+ mExitStateTime = Optional.empty();
+ }
+ logd("UceDeviceState: restore from sharedPref=" + restoreFromPref + ", " +
+ getCurrentState());
+ }
+
+ /**
+ * Check and setup the timer to exit the request disallowed state. This method is called when
+ * the DeviceState has been initialized completed and need to restore the timer.
+ */
+ public synchronized void checkSendResetDeviceStateTimer() {
+ logd("checkSendResetDeviceStateTimer: time=" + mExitStateTime);
+ if (!mExitStateTime.isPresent()) {
+ return;
+ }
+ long expirySec = ChronoUnit.SECONDS.between(Instant.now(), mExitStateTime.get());
+ if (expirySec < 0) {
+ expirySec = 0;
+ }
+ // Setup timer to exit the request disallowed state.
+ mUceCtrlCallback.setupResetDeviceStateTimer(expirySec);
+ }
+
+ /**
+ * @return The current device state.
+ */
+ public synchronized DeviceStateResult getCurrentState() {
+ return new DeviceStateResult(mDeviceState, mErrorCode, mRequestRetryTime, mExitStateTime);
+ }
+
+ /**
+ * Update the device state to determine whether the device is allowed to send requests or not.
+ * @param sipCode The SIP CODE of the request result.
+ * @param reason The reason from the network response.
+ * @param requestType The type of the request.
+ */
+ public synchronized void refreshDeviceState(int sipCode, String reason,
+ @RequestType int requestType) {
+ logd("refreshDeviceState: sipCode=" + sipCode + ", reason=" + reason +
+ ", requestResponseType=" + UceController.REQUEST_TYPE_DESCRIPTION.get(requestType));
+
+ // Get the current device status before updating the state.
+ DeviceStateResult previousState = getCurrentState();
+
+ // Update the device state based on the given sip code.
+ switch (sipCode) {
+ case NetworkSipCode.SIP_CODE_FORBIDDEN: // sip 403
+ if (requestType == UceController.REQUEST_TYPE_PUBLISH) {
+ // Provisioning error for publish request.
+ setDeviceState(DEVICE_STATE_PROVISION_ERROR);
+ } else {
+ setDeviceState(DEVICE_STATE_FORBIDDEN);
+ }
+ updateErrorCode(sipCode, reason, requestType);
+ // There is no request retry time for SIP code 403
+ removeRequestRetryTime();
+ // No timer to exit the forbidden state.
+ removeExitStateTimer();
+ break;
+
+ case NetworkSipCode.SIP_CODE_NOT_FOUND: // sip 404
+ // DeviceState only handles 404 NOT FOUND error for PUBLISH request.
+ if (requestType == UceController.REQUEST_TYPE_PUBLISH) {
+ setDeviceState(DEVICE_STATE_PROVISION_ERROR);
+ updateErrorCode(sipCode, reason, requestType);
+ // There is no request retry time for SIP code 404
+ removeRequestRetryTime();
+ // No timer to exit this state.
+ removeExitStateTimer();
+ }
+ break;
+
+ case NetworkSipCode.SIP_CODE_BAD_EVENT: // sip 489
+ if (UceUtils.isRequestForbiddenBySip489(mContext, mSubId)) {
+ setDeviceState(DEVICE_STATE_BAD_EVENT);
+ updateErrorCode(sipCode, reason, requestType);
+ // Setup the request retry time.
+ setupRequestRetryTime();
+ // Setup the timer to exit the BAD EVENT state.
+ setupExitStateTimer();
+ }
+ break;
+
+ case NetworkSipCode.SIP_CODE_OK:
+ case NetworkSipCode.SIP_CODE_ACCEPTED:
+ // Reset the device state when the network response is OK.
+ resetInternal();
+ break;
+ }
+
+ // Get the updated device state.
+ DeviceStateResult currentState = getCurrentState();
+
+ // Remove the device state from the shared preference if the device is allowed to execute
+ // UCE requests. Otherwise, save the new state into the shared preference when the device
+ // state has changed.
+ if (!currentState.isRequestForbidden()) {
+ removeDeviceStateFromPreference();
+ } else if (!currentState.isDeviceStateEqual(previousState)) {
+ saveDeviceStateToPreference(currentState);
+ }
+
+ logd("refreshDeviceState: previous: " + previousState + ", current: " + currentState);
+ }
+
+ /**
+ * Reset the device state. This method is called when the ImsService triggers to send the
+ * PUBLISH request.
+ */
+ public synchronized void resetDeviceState() {
+ DeviceStateResult previousState = getCurrentState();
+ resetInternal();
+ DeviceStateResult currentState = getCurrentState();
+
+ // Remove the device state from shared preference because the device state has been reset.
+ removeDeviceStateFromPreference();
+
+ logd("resetDeviceState: previous=" + previousState + ", current=" + currentState);
+ }
+
+ /**
+ * The internal method to reset the device state. This method doesn't
+ */
+ private void resetInternal() {
+ setDeviceState(DEVICE_STATE_OK);
+ resetErrorCode();
+ removeRequestRetryTime();
+ removeExitStateTimer();
+ }
+
+ private void setDeviceState(@DeviceStateType int latestState) {
+ if (mDeviceState != latestState) {
+ mDeviceState = latestState;
+ }
+ }
+
+ private void updateErrorCode(int sipCode, String reason, @RequestType int requestType) {
+ Optional<Integer> newErrorCode = Optional.of(NetworkSipCode.getCapabilityErrorFromSipCode(
+ sipCode, reason, requestType));
+ if (!mErrorCode.equals(newErrorCode)) {
+ mErrorCode = newErrorCode;
+ }
+ }
+
+ private void resetErrorCode() {
+ if (mErrorCode.isPresent()) {
+ mErrorCode = Optional.empty();
+ }
+ }
+
+ private void setupRequestRetryTime() {
+ /*
+ * Update the request retry time when A) it has not been assigned yet or B) it has past the
+ * current time and need to be re-assigned a new retry time.
+ */
+ if (!mRequestRetryTime.isPresent() || mRequestRetryTime.get().isAfter(Instant.now())) {
+ long retryInterval = UceUtils.getRequestRetryInterval(mContext, mSubId);
+ mRequestRetryTime = Optional.of(Instant.now().plusMillis(retryInterval));
+ }
+ }
+
+ private void removeRequestRetryTime() {
+ if (mRequestRetryTime.isPresent()) {
+ mRequestRetryTime = Optional.empty();
+ }
+ }
+
+ /**
+ * Set the timer to exit the device disallowed state and then trigger a PUBLISH request.
+ */
+ private void setupExitStateTimer() {
+ if (!mExitStateTime.isPresent()) {
+ long expirySec = UceUtils.getNonRcsCapabilitiesCacheExpiration(mContext, mSubId);
+ mExitStateTime = Optional.of(Instant.now().plusSeconds(expirySec));
+ logd("setupExitStateTimer: expirationSec=" + expirySec + ", time=" + mExitStateTime);
+
+ // Setup timer to exit the request disallowed state.
+ mUceCtrlCallback.setupResetDeviceStateTimer(expirySec);
+ }
+ }
+
+ /**
+ * Remove the exit state timer.
+ */
+ private void removeExitStateTimer() {
+ if (mExitStateTime.isPresent()) {
+ mExitStateTime = Optional.empty();
+ mUceCtrlCallback.clearResetDeviceStateTimer();
+ }
+ }
+
+ /**
+ * Save the given device sate to the shared preference.
+ * @param deviceState
+ */
+ private void saveDeviceStateToPreference(DeviceStateResult deviceState) {
+ boolean result = UceUtils.saveDeviceStateToPreference(mContext, mSubId, deviceState);
+ logd("saveDeviceStateToPreference: result=" + result + ", state= " + deviceState);
+ }
+
+ /**
+ * Remove the device state information from the shared preference because the device is allowed
+ * execute UCE requests.
+ */
+ private void removeDeviceStateFromPreference() {
+ boolean result = UceUtils.removeDeviceStateFromPreference(mContext, mSubId);
+ logd("removeDeviceStateFromPreference: result=" + result);
+ }
+
+ private void logd(String log) {
+ Log.d(LOG_TAG, getLogPrefix().append(log).toString());
+ }
+
+ private StringBuilder getLogPrefix() {
+ StringBuilder builder = new StringBuilder("[");
+ builder.append(mSubId);
+ builder.append("] ");
+ return builder;
+ }
+}
diff --git a/src/java/com/android/ims/rcs/uce/eab/EabBulkCapabilityUpdater.java b/src/java/com/android/ims/rcs/uce/eab/EabBulkCapabilityUpdater.java
new file mode 100644
index 00000000..5f2a5653
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/eab/EabBulkCapabilityUpdater.java
@@ -0,0 +1,482 @@
+/*
+ * 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.ims.rcs.uce.eab;
+
+import static android.telephony.ims.RcsContactUceCapability.CAPABILITY_MECHANISM_OPTIONS;
+import static android.telephony.ims.RcsContactUceCapability.CAPABILITY_MECHANISM_PRESENCE;
+
+import static com.android.ims.rcs.uce.eab.EabControllerImpl.getCapabilityCacheExpiration;
+
+import android.app.AlarmManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.PersistableBundle;
+import android.os.RemoteException;
+import android.provider.ContactsContract;
+import android.provider.Telephony;
+import android.telephony.CarrierConfigManager;
+import android.telephony.ims.ImsManager;
+import android.telephony.ims.ImsRcsManager;
+import android.telephony.ims.RcsContactUceCapability;
+import android.telephony.ims.aidl.IRcsUceControllerCallback;
+import android.util.Log;
+
+import com.android.ims.rcs.uce.UceController;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public final class EabBulkCapabilityUpdater {
+ private final String TAG = this.getClass().getSimpleName();
+
+ private static final Uri USER_EAB_SETTING = Uri.withAppendedPath(Telephony.SimInfo.CONTENT_URI,
+ Telephony.SimInfo.COLUMN_IMS_RCS_UCE_ENABLED);
+ private static final int NUM_SECS_IN_DAY = 86400;
+
+ private final int mSubId;
+ private final Context mContext;
+ private final Handler mHandler;
+
+ private final AlarmManager.OnAlarmListener mCapabilityExpiredListener;
+ private final ContactChangedListener mContactProviderListener;
+ private final EabSettingsListener mEabSettingListener;
+ private final BroadcastReceiver mCarrierConfigChangedListener;
+ private final EabControllerImpl mEabControllerImpl;
+ private final EabContactSyncController mEabContactSyncController;
+
+ private UceController.UceControllerCallback mUceControllerCallback;
+ private List<Uri> mRefreshContactList;
+
+ private boolean mIsContactProviderListenerRegistered = false;
+ private boolean mIsEabSettingListenerRegistered = false;
+ private boolean mIsCarrierConfigListenerRegistered = false;
+ private boolean mIsCarrierConfigEnabled = false;
+
+ /**
+ * Listen capability expired intent. Only registered when
+ * {@link CarrierConfigManager.Ims#KEY_RCS_BULK_CAPABILITY_EXCHANGE_BOOL} has enabled bulk
+ * capability exchange.
+ */
+ private class CapabilityExpiredListener implements AlarmManager.OnAlarmListener {
+ @Override
+ public void onAlarm() {
+ Log.d(TAG, "Capability expired.");
+ try {
+ List<Uri> expiredContactList = getExpiredContactList();
+ if (expiredContactList.size() > 0) {
+ mUceControllerCallback.refreshCapabilities(
+ getExpiredContactList(),
+ mRcsUceControllerCallback);
+ } else {
+ Log.d(TAG, "expiredContactList is empty.");
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "CapabilityExpiredListener RemoteException", e);
+ }
+ }
+ }
+
+ /**
+ * Listen contact provider change. Only registered when
+ * {@link CarrierConfigManager.Ims#KEY_RCS_BULK_CAPABILITY_EXCHANGE_BOOL} has enabled bulk
+ * capability exchange.
+ */
+ private class ContactChangedListener extends ContentObserver {
+ public ContactChangedListener(Handler handler) {
+ super(handler);
+ }
+
+ @Override
+ public void onChange(boolean selfChange) {
+ Log.d(TAG, "Contact changed");
+ syncContactAndRefreshCapabilities();
+ }
+ }
+
+ /**
+ * Listen EAB settings change. Only registered when
+ * {@link CarrierConfigManager.Ims#KEY_RCS_BULK_CAPABILITY_EXCHANGE_BOOL} has enabled bulk
+ * capability exchange.
+ */
+ private class EabSettingsListener extends ContentObserver {
+ public EabSettingsListener(Handler handler) {
+ super(handler);
+ }
+
+ @Override
+ public void onChange(boolean selfChange) {
+ boolean isUserEnableUce = isUserEnableUce();
+ Log.d(TAG, "EAB user setting changed: " + isUserEnableUce);
+ if (isUserEnableUce) {
+ mHandler.post(new SyncContactRunnable());
+ } else {
+ unRegisterContactProviderListener();
+ cancelTimeAlert(mContext);
+ }
+ }
+ }
+
+ /**
+ * Listen carrier config changed to prevent this instance created before carrier config loaded.
+ */
+ private class CarrierConfigChangedListener extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ boolean isSupportBulkCapabilityExchange = getBooleanCarrierConfig(
+ CarrierConfigManager.Ims.KEY_RCS_BULK_CAPABILITY_EXCHANGE_BOOL, mSubId);
+
+ Log.d(TAG, "Carrier config changed. "
+ + "isCarrierConfigEnabled: " + mIsCarrierConfigEnabled
+ + ", isSupportBulkCapabilityExchange: " + isSupportBulkCapabilityExchange);
+ if (!mIsCarrierConfigEnabled && isSupportBulkCapabilityExchange) {
+ enableBulkCapability();
+ updateExpiredTimeAlert();
+ mIsCarrierConfigEnabled = true;
+ } else if (mIsCarrierConfigEnabled && !isSupportBulkCapabilityExchange) {
+ onDestroy();
+ }
+ }
+ }
+
+ private IRcsUceControllerCallback mRcsUceControllerCallback = new IRcsUceControllerCallback() {
+ @Override
+ public void onCapabilitiesReceived(List<RcsContactUceCapability> contactCapabilities) {
+ Log.d(TAG, "onCapabilitiesReceived");
+ mEabControllerImpl.saveCapabilities(contactCapabilities);
+ }
+
+ @Override
+ public void onComplete() {
+ Log.d(TAG, "onComplete");
+ }
+
+ @Override
+ public void onError(int errorCode, long retryAfterMilliseconds) {
+ Log.d(TAG, "Refresh capabilities failed. Error code: " + errorCode
+ + ", retryAfterMilliseconds: " + retryAfterMilliseconds);
+ if (retryAfterMilliseconds != 0) {
+ mHandler.postDelayed(new retryRunnable(), retryAfterMilliseconds);
+ }
+ }
+
+ @Override
+ public IBinder asBinder() {
+ return null;
+ }
+ };
+
+ private class SyncContactRunnable implements Runnable {
+ @Override
+ public void run() {
+ Log.d(TAG, "Sync contact from contact provider");
+ syncContactAndRefreshCapabilities();
+ registerContactProviderListener();
+ registerEabUserSettingsListener();
+ }
+ }
+
+ /**
+ * Re-refresh capability if error happened.
+ */
+ private class retryRunnable implements Runnable {
+ @Override
+ public void run() {
+ Log.d(TAG, "Retry refreshCapabilities()");
+
+ try {
+ mUceControllerCallback.refreshCapabilities(
+ mRefreshContactList, mRcsUceControllerCallback);
+ } catch (RemoteException e) {
+ Log.e(TAG, "refreshCapabilities RemoteException" , e);
+ }
+ }
+ }
+
+ public EabBulkCapabilityUpdater(Context context,
+ int subId,
+ EabControllerImpl eabControllerImpl,
+ EabContactSyncController eabContactSyncController,
+ UceController.UceControllerCallback uceControllerCallback,
+ Handler handler) {
+ mContext = context;
+ mSubId = subId;
+ mEabControllerImpl = eabControllerImpl;
+ mEabContactSyncController = eabContactSyncController;
+ mUceControllerCallback = uceControllerCallback;
+
+ mHandler = handler;
+ mContactProviderListener = new ContactChangedListener(mHandler);
+ mEabSettingListener = new EabSettingsListener(mHandler);
+ mCapabilityExpiredListener = new CapabilityExpiredListener();
+ mCarrierConfigChangedListener = new CarrierConfigChangedListener();
+
+ Log.d(TAG, "create EabBulkCapabilityUpdater() subId: " + mSubId);
+
+ enableBulkCapability();
+ updateExpiredTimeAlert();
+ }
+
+ private void enableBulkCapability() {
+ boolean isUserEnableUce = isUserEnableUce();
+ boolean isSupportBulkCapabilityExchange = getBooleanCarrierConfig(
+ CarrierConfigManager.Ims.KEY_RCS_BULK_CAPABILITY_EXCHANGE_BOOL, mSubId);
+
+ Log.d(TAG, "isUserEnableUce: " + isUserEnableUce
+ + ", isSupportBulkCapabilityExchange: " + isSupportBulkCapabilityExchange);
+
+ if (isUserEnableUce && isSupportBulkCapabilityExchange) {
+ mHandler.post(new SyncContactRunnable());
+ mIsCarrierConfigEnabled = true;
+ } else if (!isUserEnableUce && isSupportBulkCapabilityExchange) {
+ registerEabUserSettingsListener();
+ mIsCarrierConfigEnabled = false;
+ } else {
+ registerCarrierConfigChanged();
+ Log.d(TAG, "Not support bulk capability exchange.");
+ }
+ }
+
+ private void syncContactAndRefreshCapabilities() {
+ mRefreshContactList = mEabContactSyncController.syncContactToEabProvider(mContext);
+ Log.d(TAG, "refresh contacts number: " + mRefreshContactList.size());
+
+ if (mUceControllerCallback == null) {
+ Log.d(TAG, "mUceControllerCallback is null.");
+ return;
+ }
+
+ try {
+ if (mRefreshContactList.size() > 0) {
+ mUceControllerCallback.refreshCapabilities(
+ mRefreshContactList, mRcsUceControllerCallback);
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "mUceControllerCallback RemoteException.", e);
+ }
+ }
+
+ protected void updateExpiredTimeAlert() {
+ boolean isUserEnableUce = isUserEnableUce();
+ boolean isSupportBulkCapabilityExchange = getBooleanCarrierConfig(
+ CarrierConfigManager.Ims.KEY_RCS_BULK_CAPABILITY_EXCHANGE_BOOL, mSubId);
+
+ Log.d(TAG, " updateExpiredTimeAlert(), isUserEnableUce: " + isUserEnableUce
+ + ", isSupportBulkCapabilityExchange: " + isSupportBulkCapabilityExchange);
+
+ if (isUserEnableUce && isSupportBulkCapabilityExchange) {
+ long expiredTimestamp = getLeastExpiredTimestamp();
+ if (expiredTimestamp == Long.MAX_VALUE) {
+ Log.d(TAG, "Can't find min timestamp in eab provider");
+ return;
+ }
+ expiredTimestamp += getCapabilityCacheExpiration(mSubId);
+ Log.d(TAG, "set time alert at " + expiredTimestamp);
+ cancelTimeAlert(mContext);
+ setTimeAlert(mContext, expiredTimestamp);
+ }
+ }
+
+ private long getLeastExpiredTimestamp() {
+ String selection = "("
+ // Query presence timestamp
+ + EabProvider.EabCommonColumns.MECHANISM + "=" + CAPABILITY_MECHANISM_PRESENCE
+ + " AND "
+ + EabProvider.PresenceTupleColumns.REQUEST_TIMESTAMP + " IS NOT NULL) "
+
+ // Query options timestamp
+ + " OR " + "(" + EabProvider.EabCommonColumns.MECHANISM + "="
+ + CAPABILITY_MECHANISM_OPTIONS + " AND "
+ + EabProvider.OptionsColumns.REQUEST_TIMESTAMP + " IS NOT NULL) "
+
+ // filter by sub id
+ + " AND " + EabProvider.EabCommonColumns.SUBSCRIPTION_ID + "=" + mSubId
+
+ // filter the contact that not come from contact provider
+ + " AND " + EabProvider.ContactColumns.RAW_CONTACT_ID + " IS NOT NULL "
+ + " AND " + EabProvider.ContactColumns.DATA_ID + " IS NOT NULL ";
+
+ long minTimestamp = Long.MAX_VALUE;
+ Cursor result = mContext.getContentResolver().query(EabProvider.ALL_DATA_URI, null,
+ selection,
+ null, null);
+
+ if (result != null) {
+ while (result.moveToNext()) {
+ int mechanism = result.getInt(
+ result.getColumnIndex(EabProvider.EabCommonColumns.MECHANISM));
+ long timestamp;
+ if (mechanism == CAPABILITY_MECHANISM_PRESENCE) {
+ timestamp = result.getLong(result.getColumnIndex(
+ EabProvider.PresenceTupleColumns.REQUEST_TIMESTAMP));
+ } else {
+ timestamp = result.getLong(result.getColumnIndex(
+ EabProvider.OptionsColumns.REQUEST_TIMESTAMP));
+ }
+
+ if (timestamp < minTimestamp) {
+ minTimestamp = timestamp;
+ }
+ }
+ result.close();
+ } else {
+ Log.d(TAG, "getLeastExpiredTimestamp() cursor is null");
+ }
+ return minTimestamp;
+ }
+
+ private void setTimeAlert(Context context, long wakeupTimeMs) {
+ AlarmManager am = context.getSystemService(AlarmManager.class);
+
+ // To prevent all devices from sending requests to the server at the same time, add a jitter
+ // time (0 sec ~ 2 days) randomly.
+ int jitterTimeSec = (int) (Math.random() * (NUM_SECS_IN_DAY * 2));
+ Log.d(TAG, " setTimeAlert: " + wakeupTimeMs + ", jitterTimeSec: " + jitterTimeSec);
+ am.set(AlarmManager.RTC_WAKEUP,
+ (wakeupTimeMs * 1000) + jitterTimeSec,
+ TAG,
+ mCapabilityExpiredListener,
+ mHandler);
+ }
+
+ private void cancelTimeAlert(Context context) {
+ Log.d(TAG, "cancelTimeAlert.");
+ AlarmManager am = context.getSystemService(AlarmManager.class);
+ am.cancel(mCapabilityExpiredListener);
+ }
+
+ private boolean getBooleanCarrierConfig(String key, int subId) {
+ CarrierConfigManager mConfigManager = mContext.getSystemService(CarrierConfigManager.class);
+ PersistableBundle b = null;
+ if (mConfigManager != null) {
+ b = mConfigManager.getConfigForSubId(subId);
+ }
+ if (b != null) {
+ return b.getBoolean(key);
+ } else {
+ Log.w(TAG, "getConfigForSubId(subId) is null. Return the default value of " + key);
+ return CarrierConfigManager.getDefaultConfig().getBoolean(key);
+ }
+ }
+
+ private boolean isUserEnableUce() {
+ ImsManager manager = mContext.getSystemService(ImsManager.class);
+ if (manager == null) {
+ Log.e(TAG, "ImsManager is null");
+ return false;
+ }
+ try {
+ ImsRcsManager rcsManager = manager.getImsRcsManager(mSubId);
+ return (rcsManager != null) && rcsManager.getUceAdapter().isUceSettingEnabled();
+ } catch (Exception e) {
+ Log.e(TAG, "hasUserEnabledUce: exception = " + e.getMessage());
+ }
+ return false;
+ }
+
+ private List<Uri> getExpiredContactList() {
+ List<Uri> refreshList = new ArrayList<>();
+ long expiredTime = (System.currentTimeMillis() / 1000)
+ + getCapabilityCacheExpiration(mSubId);
+ String selection = "("
+ + EabProvider.EabCommonColumns.MECHANISM + "=" + CAPABILITY_MECHANISM_PRESENCE
+ + " AND " + EabProvider.PresenceTupleColumns.REQUEST_TIMESTAMP + "<"
+ + expiredTime + ")";
+ selection += " OR " + "(" + EabProvider.EabCommonColumns.MECHANISM + "="
+ + CAPABILITY_MECHANISM_OPTIONS + " AND "
+ + EabProvider.OptionsColumns.REQUEST_TIMESTAMP + "<" + expiredTime + ")";
+
+ Cursor result = mContext.getContentResolver().query(EabProvider.ALL_DATA_URI, null,
+ selection,
+ null, null);
+ while (result.moveToNext()) {
+ String phoneNumber = result.getString(
+ result.getColumnIndex(EabProvider.ContactColumns.PHONE_NUMBER));
+ refreshList.add(Uri.parse(phoneNumber));
+ }
+ result.close();
+ return refreshList;
+ }
+
+ protected void onDestroy() {
+ Log.d(TAG, "onDestroy");
+ cancelTimeAlert(mContext);
+ unRegisterContactProviderListener();
+ unRegisterEabUserSettings();
+ unRegisterCarrierConfigChanged();
+ }
+
+ private void registerContactProviderListener() {
+ Log.d(TAG, "registerContactProviderListener");
+ mIsContactProviderListenerRegistered = true;
+ mContext.getContentResolver().registerContentObserver(
+ ContactsContract.Contacts.CONTENT_URI,
+ true,
+ mContactProviderListener);
+ }
+
+ private void registerEabUserSettingsListener() {
+ Log.d(TAG, "registerEabUserSettingsListener");
+ mIsEabSettingListenerRegistered = true;
+ mContext.getContentResolver().registerContentObserver(
+ USER_EAB_SETTING,
+ true,
+ mEabSettingListener);
+ }
+
+ private void registerCarrierConfigChanged() {
+ Log.d(TAG, "registerCarrierConfigChanged");
+ mIsCarrierConfigListenerRegistered = true;
+ IntentFilter FILTER_CARRIER_CONFIG_CHANGED =
+ new IntentFilter(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED);
+ mContext.registerReceiver(mCarrierConfigChangedListener, FILTER_CARRIER_CONFIG_CHANGED);
+ }
+
+ private void unRegisterContactProviderListener() {
+ Log.d(TAG, "unRegisterContactProviderListener");
+ if (mIsContactProviderListenerRegistered) {
+ mIsContactProviderListenerRegistered = false;
+ mContext.getContentResolver().unregisterContentObserver(mContactProviderListener);
+ }
+ }
+
+ private void unRegisterEabUserSettings() {
+ Log.d(TAG, "unRegisterEabUserSettings");
+ if (mIsEabSettingListenerRegistered) {
+ mIsEabSettingListenerRegistered = false;
+ mContext.getContentResolver().unregisterContentObserver(mEabSettingListener);
+ }
+ }
+
+ private void unRegisterCarrierConfigChanged() {
+ Log.d(TAG, "unregisterCarrierConfigChanged");
+ if (mIsCarrierConfigListenerRegistered) {
+ mIsCarrierConfigListenerRegistered = false;
+ mContext.unregisterReceiver(mCarrierConfigChangedListener);
+ }
+ }
+
+ public void setUceRequestCallback(UceController.UceControllerCallback uceControllerCallback) {
+ mUceControllerCallback = uceControllerCallback;
+ }
+}
diff --git a/src/java/com/android/ims/rcs/uce/eab/EabCapabilityResult.java b/src/java/com/android/ims/rcs/uce/eab/EabCapabilityResult.java
new file mode 100644
index 00000000..0e5e01f3
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/eab/EabCapabilityResult.java
@@ -0,0 +1,100 @@
+/*
+ * 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.ims.rcs.uce.eab;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.Uri;
+import android.telephony.ims.RcsContactUceCapability;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * The result class of retrieving capabilities from cache.
+ */
+public class EabCapabilityResult {
+
+ /**
+ * Query successful.
+ */
+ public static final int EAB_QUERY_SUCCESSFUL = 0;
+
+ /**
+ * The {@link EabControllerImpl} has been destroyed.
+ */
+ public static final int EAB_CONTROLLER_DESTROYED_FAILURE = 1;
+
+ /**
+ * The contact's capabilities expired.
+ */
+ public static final int EAB_CONTACT_EXPIRED_FAILURE = 2;
+
+ /**
+ * The contact cannot be found in the contact provider.
+ */
+ public static final int EAB_CONTACT_NOT_FOUND_FAILURE = 3;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = "EAB_", value = {
+ EAB_QUERY_SUCCESSFUL,
+ EAB_CONTROLLER_DESTROYED_FAILURE,
+ EAB_CONTACT_EXPIRED_FAILURE,
+ EAB_CONTACT_NOT_FOUND_FAILURE
+ })
+ public @interface QueryResult {}
+
+ private final @QueryResult int mStatus;
+ private final Uri mContactUri;
+ private final RcsContactUceCapability mContactCapabilities;
+
+ public EabCapabilityResult(@QueryResult Uri contactUri, int status,
+ RcsContactUceCapability capabilities) {
+ mStatus = status;
+ mContactUri = contactUri;
+ mContactCapabilities = capabilities;
+ }
+
+ /**
+ * Return the status of query. The possible values are
+ * {@link EabCapabilityResult#EAB_QUERY_SUCCESSFUL},
+ * {@link EabCapabilityResult#EAB_CONTROLLER_DESTROYED_FAILURE},
+ * {@link EabCapabilityResult#EAB_CONTACT_EXPIRED_FAILURE},
+ * {@link EabCapabilityResult#EAB_CONTACT_NOT_FOUND_FAILURE}.
+ *
+ */
+ public @NonNull int getStatus() {
+ return mStatus;
+ }
+
+ /**
+ * Return the contact uri.
+ */
+ public @NonNull Uri getContact() {
+ return mContactUri;
+ }
+
+ /**
+ * Return the contacts capabilities which are cached in the EAB database and
+ * are not expired.
+ */
+ public @Nullable RcsContactUceCapability getContactCapabilities() {
+ return mContactCapabilities;
+ }
+}
diff --git a/src/java/com/android/ims/rcs/uce/eab/EabContactSyncController.java b/src/java/com/android/ims/rcs/uce/eab/EabContactSyncController.java
new file mode 100644
index 00000000..6f148a8a
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/eab/EabContactSyncController.java
@@ -0,0 +1,357 @@
+/*
+ * 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.ims.rcs.uce.eab;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.database.Cursor;
+import android.net.Uri;
+import android.preference.PreferenceManager;
+import android.provider.ContactsContract;
+import android.telephony.TelephonyManager;
+import android.util.Log;
+
+import com.android.i18n.phonenumbers.NumberParseException;
+import com.android.i18n.phonenumbers.PhoneNumberUtil;
+import com.android.i18n.phonenumbers.Phonenumber;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * Sync the contacts from Contact Provider to EAB Provider
+ */
+public class EabContactSyncController {
+ private final String TAG = this.getClass().getSimpleName();
+
+ private static final int NOT_INIT_LAST_UPDATED_TIME = -1;
+ private static final String LAST_UPDATED_TIME_KEY = "eab_last_updated_time";
+
+ /**
+ * Sync contact from Contact provider to EAB provider. There are 4 kinds of cases need to be
+ * handled when received the contact db changed:
+ *
+ * 1. Contact deleted
+ * 2. Delete the phone number in the contact
+ * 3. Update the phone number
+ * 4. Add a new contact and add phone number
+ *
+ * @return The contacts that need to refresh
+ */
+ @VisibleForTesting
+ public List<Uri> syncContactToEabProvider(Context context) {
+ Log.d(TAG, "syncContactToEabProvider");
+ List<Uri> refreshContacts = null;
+ StringBuilder selection = new StringBuilder();
+ String[] selectionArgs = null;
+
+ // Get the last update timestamp from shared preference.
+ long lastUpdatedTimeStamp = getLastUpdatedTime(context);
+ if (lastUpdatedTimeStamp != -1) {
+ selection.append(ContactsContract.Contacts.CONTACT_LAST_UPDATED_TIMESTAMP + ">?");
+ selectionArgs = new String[]{String.valueOf(lastUpdatedTimeStamp)};
+ }
+
+ // Contact deleted cases (case 1)
+ handleContactDeletedCase(context, lastUpdatedTimeStamp);
+
+ // Query the contacts that have not been synchronized to eab contact table.
+ Cursor updatedContact = context.getContentResolver().query(
+ ContactsContract.Data.CONTENT_URI,
+ null,
+ selection.toString(),
+ selectionArgs,
+ null);
+
+ if (updatedContact != null) {
+ Log.d(TAG, "Contact changed count: " + updatedContact.getCount());
+
+ if (updatedContact.getCount() == 0) {
+ return new ArrayList<>();
+ }
+
+ // Delete the EAB phone number that not in contact provider (case 2). Updated phone
+ // number(case 3) also delete in here and re-insert in next step.
+ handlePhoneNumberDeletedCase(context, updatedContact);
+
+ // Insert the phone number that not in EAB provider (case 3 and case 4)
+ refreshContacts = handlePhoneNumberInsertedCase(context, updatedContact);
+
+ // Update the last update time in shared preference
+ if (updatedContact.getCount() > 0) {
+ long maxTimestamp = findMaxTimestamp(updatedContact);
+ if (maxTimestamp != Long.MIN_VALUE) {
+ setLastUpdatedTime(context, maxTimestamp);
+ }
+ }
+ updatedContact.close();
+ } else {
+ Log.e(TAG, "Cursor is null.");
+ }
+ return refreshContacts;
+ }
+
+ /**
+ * Delete the phone numbers that contact has been deleted in contact provider. Query based on
+ * {@link ContactsContract.DeletedContacts#CONTENT_URI} to know which contact has been removed.
+ *
+ * @param timeStamp last updated timestamp
+ */
+ private void handleContactDeletedCase(Context context, long timeStamp) {
+ String selection = "";
+ if (timeStamp != -1) {
+ selection =
+ ContactsContract.DeletedContacts.CONTACT_DELETED_TIMESTAMP + ">" + timeStamp;
+ }
+
+ Cursor cursor = context.getContentResolver().query(
+ ContactsContract.DeletedContacts.CONTENT_URI,
+ new String[]{ContactsContract.DeletedContacts.CONTACT_ID,
+ ContactsContract.DeletedContacts.CONTACT_DELETED_TIMESTAMP},
+ selection,
+ null,
+ null);
+
+ if (cursor == null) {
+ Log.d(TAG, "handleContactDeletedCase() cursor is null.");
+ return;
+ }
+
+ Log.d(TAG, "(Case 1) The count of contact that need to be deleted: "
+ + cursor.getCount());
+
+ StringBuilder deleteClause = new StringBuilder();
+ while (cursor.moveToNext()) {
+ if (deleteClause.length() > 0) {
+ deleteClause.append(" OR ");
+ }
+
+ String contactId = cursor.getString(cursor.getColumnIndex(
+ ContactsContract.DeletedContacts.CONTACT_ID));
+ deleteClause.append(EabProvider.ContactColumns.CONTACT_ID + "=" + contactId);
+ }
+
+ if (deleteClause.toString().length() > 0) {
+ int number = context.getContentResolver().delete(
+ EabProvider.CONTACT_URI,
+ deleteClause.toString(),
+ null);
+ Log.d(TAG, "(Case 1) Deleted contact count=" + number);
+ }
+ }
+
+ /**
+ * Delete phone numbers that have been deleted in the contact provider. There is no API to get
+ * deleted phone numbers easily, so check all updated contact's phone number and delete the
+ * phone number. It will also delete the phone number that has been changed.
+ */
+ private void handlePhoneNumberDeletedCase(Context context, Cursor cursor) {
+ // The map represent which contacts have which numbers.
+ Map<String, List<String>> phoneNumberMap = new HashMap<>();
+ cursor.moveToPosition(-1);
+ while (cursor.moveToNext()) {
+ String mimeType = cursor.getString(
+ cursor.getColumnIndex(ContactsContract.Data.MIMETYPE));
+ if (!mimeType.equals(ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE)) {
+ continue;
+ }
+
+ String rawContactId = cursor.getString(
+ cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.RAW_CONTACT_ID));
+ String number = formatNumber(context, cursor.getString(
+ cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER)));
+
+ if (phoneNumberMap.containsKey(rawContactId)) {
+ phoneNumberMap.get(rawContactId).add(number);
+ } else {
+ List<String> phoneNumberList = new ArrayList<>();
+ phoneNumberList.add(number);
+ phoneNumberMap.put(rawContactId, phoneNumberList);
+ }
+ }
+
+ // Build a SQL statement that delete the phone number not exist in contact provider.
+ // For example:
+ // raw_contact_id = 1 AND phone_number NOT IN (12345, 23456)
+ StringBuilder deleteClause = new StringBuilder();
+ List<String> deleteClauseArgs = new ArrayList<>();
+ for (Map.Entry<String, List<String>> entry : phoneNumberMap.entrySet()) {
+ String rawContactId = entry.getKey();
+ List<String> phoneNumberList = entry.getValue();
+
+ if (deleteClause.length() > 0) {
+ deleteClause.append(" OR ");
+ }
+
+ deleteClause.append("(" + EabProvider.ContactColumns.RAW_CONTACT_ID + "=? ");
+ deleteClauseArgs.add(rawContactId);
+
+ if (phoneNumberList.size() > 0) {
+ String argsList = phoneNumberList.stream()
+ .map(s -> "?")
+ .collect(Collectors.joining(", "));
+ deleteClause.append(" AND "
+ + EabProvider.ContactColumns.PHONE_NUMBER
+ + " NOT IN (" + argsList + "))");
+ deleteClauseArgs.addAll(phoneNumberList);
+ } else {
+ deleteClause.append(")");
+ }
+ }
+
+ int number = context.getContentResolver().delete(
+ EabProvider.CONTACT_URI,
+ deleteClause.toString(),
+ deleteClauseArgs.toArray(new String[0]));
+ Log.d(TAG, "(Case 2, 3) handlePhoneNumberDeletedCase number count= " + number);
+ }
+
+ /**
+ * Insert new phone number.
+ *
+ * @param contactCursor the result of updated contact
+ * @return the contacts that need to refresh
+ */
+ private List<Uri> handlePhoneNumberInsertedCase(Context context,
+ Cursor contactCursor) {
+ List<Uri> refreshContacts = new ArrayList<>();
+ List<ContentValues> allContactData = new ArrayList<>();
+ contactCursor.moveToPosition(-1);
+
+ // Query all of contacts that store in eab provider
+ Cursor eabContact = context.getContentResolver().query(
+ EabProvider.CONTACT_URI,
+ null,
+ EabProvider.ContactColumns.DATA_ID + " IS NOT NULL",
+ null,
+ EabProvider.ContactColumns.DATA_ID);
+
+ while (contactCursor.moveToNext()) {
+ String contactId = contactCursor.getString(contactCursor.getColumnIndex(
+ ContactsContract.Data.CONTACT_ID));
+ String rawContactId = contactCursor.getString(contactCursor.getColumnIndex(
+ ContactsContract.CommonDataKinds.Phone.RAW_CONTACT_ID));
+ String dataId = contactCursor.getString(
+ contactCursor.getColumnIndex(ContactsContract.Data._ID));
+ String mimeType = contactCursor.getString(
+ contactCursor.getColumnIndex(ContactsContract.Data.MIMETYPE));
+
+ if (!mimeType.equals(ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE)) {
+ continue;
+ }
+
+ String number = formatNumber(context, contactCursor.getString(
+ contactCursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER)));
+
+ int index = searchDataIdIndex(eabContact, Integer.parseInt(dataId));
+ if (index == -1) {
+ Log.d(TAG, "Data id does not exist. Insert phone number into EAB db.");
+ refreshContacts.add(Uri.parse(number));
+ ContentValues data = new ContentValues();
+ data.put(EabProvider.ContactColumns.CONTACT_ID, contactId);
+ data.put(EabProvider.ContactColumns.DATA_ID, dataId);
+ data.put(EabProvider.ContactColumns.RAW_CONTACT_ID, rawContactId);
+ data.put(EabProvider.ContactColumns.PHONE_NUMBER, number);
+ allContactData.add(data);
+ }
+ }
+
+ // Insert contacts at once
+ int result = context.getContentResolver().bulkInsert(
+ EabProvider.CONTACT_URI,
+ allContactData.toArray(new ContentValues[0]));
+ Log.d(TAG, "(Case 3, 4) Phone number insert count: " + result);
+ return refreshContacts;
+ }
+
+ /**
+ * Binary search the target data_id in the cursor.
+ *
+ * @param cursor EabProvider contact which sorted by
+ * {@link EabProvider.ContactColumns#DATA_ID}
+ * @param targetDataId the data_id to search for
+ * @return the index of cursor
+ */
+ private int searchDataIdIndex(Cursor cursor, int targetDataId) {
+ int start = 0;
+ int end = cursor.getCount() - 1;
+
+ while (start <= end) {
+ int position = (start + end) >>> 1;
+ cursor.moveToPosition(position);
+ int dataId = cursor.getInt(cursor.getColumnIndex(EabProvider.ContactColumns.DATA_ID));
+
+ if (dataId > targetDataId) {
+ end = position - 1;
+ } else if (dataId < targetDataId) {
+ start = position + 1;
+ } else {
+ return position;
+ }
+ }
+ return -1;
+ }
+
+
+ private long findMaxTimestamp(Cursor cursor) {
+ long maxTimestamp = Long.MIN_VALUE;
+ cursor.moveToPosition(-1);
+ while(cursor.moveToNext()) {
+ long lastUpdatedTimeStamp = cursor.getLong(cursor.getColumnIndex(
+ ContactsContract.CommonDataKinds.Phone.CONTACT_LAST_UPDATED_TIMESTAMP));
+ Log.d(TAG, lastUpdatedTimeStamp + " " + maxTimestamp);
+ if (lastUpdatedTimeStamp > maxTimestamp) {
+ maxTimestamp = lastUpdatedTimeStamp;
+ }
+ }
+ return maxTimestamp;
+ }
+
+ private void setLastUpdatedTime(Context context, long timestamp) {
+ Log.d(TAG, "setLastUpdatedTime: " + timestamp);
+ SharedPreferences sharedPreferences =
+ PreferenceManager.getDefaultSharedPreferences(context);
+ sharedPreferences.edit().putLong(LAST_UPDATED_TIME_KEY, timestamp).apply();
+ }
+
+ private long getLastUpdatedTime(Context context) {
+ SharedPreferences sharedPreferences =
+ PreferenceManager.getDefaultSharedPreferences(context);
+ return sharedPreferences.getLong(LAST_UPDATED_TIME_KEY, NOT_INIT_LAST_UPDATED_TIME);
+ }
+
+ private String formatNumber(Context context, String number) {
+ TelephonyManager manager = context.getSystemService(TelephonyManager.class);
+ String simCountryIso = manager.getSimCountryIso();
+ if (simCountryIso != null) {
+ simCountryIso = simCountryIso.toUpperCase();
+ PhoneNumberUtil util = PhoneNumberUtil.getInstance();
+ try {
+ Phonenumber.PhoneNumber phoneNumber = util.parse(number, simCountryIso);
+ return util.format(phoneNumber, PhoneNumberUtil.PhoneNumberFormat.E164);
+ } catch (NumberParseException e) {
+ Log.w(TAG, "formatNumber: could not format " + number + ", error: " + e);
+ }
+ }
+ return number;
+ }
+}
diff --git a/src/java/com/android/ims/rcs/uce/eab/EabController.java b/src/java/com/android/ims/rcs/uce/eab/EabController.java
new file mode 100644
index 00000000..903a19df
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/eab/EabController.java
@@ -0,0 +1,51 @@
+/*
+ * 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.ims.rcs.uce.eab;
+
+import android.annotation.NonNull;
+import android.net.Uri;
+import android.telephony.ims.RcsContactUceCapability;
+
+import com.android.ims.rcs.uce.ControllerBase;
+import com.android.ims.rcs.uce.UceController.UceControllerCallback;
+
+import java.util.List;
+
+/**
+ * The interface related to the Enhanced Address Book.
+ */
+public interface EabController extends ControllerBase {
+ /**
+ * Retrieve the contacts' capabilities from the EAB database.
+ */
+ @NonNull List<EabCapabilityResult> getCapabilities(@NonNull List<Uri> uris);
+
+ /**
+ * Retrieve the contact's capabilities from the availability cache.
+ */
+ @NonNull EabCapabilityResult getAvailability(@NonNull Uri contactUri);
+
+ /**
+ * Save the capabilities to the EAB database.
+ */
+ void saveCapabilities(@NonNull List<RcsContactUceCapability> contactCapabilities);
+
+ /**
+ * Set the UceRequestCallback for sending the request to UceController.
+ */
+ void setUceRequestCallback(@NonNull UceControllerCallback c);
+}
diff --git a/src/java/com/android/ims/rcs/uce/eab/EabControllerImpl.java b/src/java/com/android/ims/rcs/uce/eab/EabControllerImpl.java
new file mode 100644
index 00000000..f59171f3
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/eab/EabControllerImpl.java
@@ -0,0 +1,793 @@
+/*
+ * 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.ims.rcs.uce.eab;
+
+import static android.telephony.ims.RcsContactUceCapability.CAPABILITY_MECHANISM_OPTIONS;
+import static android.telephony.ims.RcsContactUceCapability.CAPABILITY_MECHANISM_PRESENCE;
+import static android.telephony.ims.RcsContactUceCapability.REQUEST_RESULT_NOT_FOUND;
+import static android.telephony.ims.RcsContactUceCapability.SOURCE_TYPE_CACHED;
+
+import static com.android.ims.rcs.uce.eab.EabProvider.EAB_OPTIONS_TABLE_NAME;
+import static com.android.ims.rcs.uce.eab.EabProvider.EAB_PRESENCE_TUPLE_TABLE_NAME;
+
+import android.annotation.NonNull;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.PersistableBundle;
+import android.telephony.CarrierConfigManager;
+import android.telephony.TelephonyManager;
+import android.telephony.ims.ProvisioningManager;
+import android.telephony.ims.RcsContactPresenceTuple;
+import android.telephony.ims.RcsContactPresenceTuple.ServiceCapabilities;
+import android.telephony.ims.RcsContactUceCapability;
+import android.telephony.ims.RcsContactUceCapability.OptionsBuilder;
+import android.telephony.ims.RcsContactUceCapability.PresenceBuilder;
+import android.text.TextUtils;
+import android.text.format.Time;
+import android.util.Log;
+import android.util.TimeFormatException;
+
+import com.android.i18n.phonenumbers.NumberParseException;
+import com.android.i18n.phonenumbers.PhoneNumberUtil;
+import com.android.i18n.phonenumbers.Phonenumber;
+import com.android.ims.RcsFeatureManager;
+import com.android.ims.rcs.uce.UceController.UceControllerCallback;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.time.Instant;
+import java.time.format.DateTimeParseException;
+import java.time.temporal.ChronoUnit;
+import java.util.ArrayList;
+import java.util.GregorianCalendar;
+import java.util.List;
+import java.util.Objects;
+import java.util.TimeZone;
+import java.util.function.Predicate;
+
+/**
+ * The implementation of EabController.
+ */
+public class EabControllerImpl implements EabController {
+ private static final String TAG = "EabControllerImpl";
+
+ // 90 days
+ private static final int DEFAULT_CAPABILITY_CACHE_EXPIRATION_SEC = 90 * 24 * 60 * 60;
+ private static final int DEFAULT_AVAILABILITY_CACHE_EXPIRATION_SEC = 60;
+
+ // 1 week
+ private static final int CLEAN_UP_LEGACY_CAPABILITY_SEC = 7 * 24 * 60 * 60;
+ private static final int CLEAN_UP_LEGACY_CAPABILITY_DELAY_MILLI_SEC = 30 * 1000;
+
+ private final Context mContext;
+ private final int mSubId;
+ private final EabBulkCapabilityUpdater mEabBulkCapabilityUpdater;
+ private final Handler mHandler;
+
+ private UceControllerCallback mUceControllerCallback;
+ private volatile boolean mIsSetDestroyedFlag = false;
+
+ @VisibleForTesting
+ public final Runnable mCapabilityCleanupRunnable = () -> {
+ Log.d(TAG, "Cleanup Capabilities");
+ cleanupExpiredCapabilities();
+ };
+
+ public EabControllerImpl(Context context, int subId, UceControllerCallback c, Looper looper) {
+ mContext = context;
+ mSubId = subId;
+ mUceControllerCallback = c;
+ mHandler = new Handler(looper);
+ mEabBulkCapabilityUpdater = new EabBulkCapabilityUpdater(mContext, mSubId,
+ this,
+ new EabContactSyncController(),
+ mUceControllerCallback,
+ mHandler);
+ }
+
+ @Override
+ public void onRcsConnected(RcsFeatureManager manager) {
+ }
+
+ @Override
+ public void onRcsDisconnected() {
+ }
+
+ @Override
+ public void onDestroy() {
+ Log.d(TAG, "onDestroy");
+ mIsSetDestroyedFlag = true;
+ mEabBulkCapabilityUpdater.onDestroy();
+ }
+
+ @Override
+ public void onCarrierConfigChanged() {
+ // Pick up changes to CarrierConfig and run any applicable cleanup tasks associated with
+ // that configuration.
+ mCapabilityCleanupRunnable.run();
+ }
+
+ /**
+ * Set the callback for sending the request to UceController.
+ */
+ @Override
+ public void setUceRequestCallback(UceControllerCallback c) {
+ Objects.requireNonNull(c);
+ if (mIsSetDestroyedFlag) {
+ Log.d(TAG, "EabController destroyed.");
+ return;
+ }
+ mUceControllerCallback = c;
+ mEabBulkCapabilityUpdater.setUceRequestCallback(c);
+ }
+
+ /**
+ * Retrieve the contacts' capabilities from the EAB database.
+ */
+ @Override
+ public @NonNull List<EabCapabilityResult> getCapabilities(@NonNull List<Uri> uris) {
+ Objects.requireNonNull(uris);
+ if (mIsSetDestroyedFlag) {
+ Log.d(TAG, "EabController destroyed.");
+ return generateDestroyedResult(uris);
+ }
+
+ Log.d(TAG, "getCapabilities uri size=" + uris.size());
+ List<EabCapabilityResult> capabilityResultList = new ArrayList();
+
+ for (Uri uri : uris) {
+ EabCapabilityResult result = generateEabResult(uri, this::isCapabilityExpired);
+ capabilityResultList.add(result);
+ }
+ return capabilityResultList;
+ }
+
+ /**
+ * Retrieve the contact's capabilities from the availability cache.
+ */
+ @Override
+ public @NonNull EabCapabilityResult getAvailability(@NonNull Uri contactUri) {
+ Objects.requireNonNull(contactUri);
+ if (mIsSetDestroyedFlag) {
+ Log.d(TAG, "EabController destroyed.");
+ return new EabCapabilityResult(
+ contactUri,
+ EabCapabilityResult.EAB_CONTROLLER_DESTROYED_FAILURE,
+ null);
+ }
+ return generateEabResult(contactUri, this::isAvailabilityExpired);
+ }
+
+ /**
+ * Update the availability catch and save the capabilities to the EAB database.
+ */
+ @Override
+ public void saveCapabilities(@NonNull List<RcsContactUceCapability> contactCapabilities) {
+ Objects.requireNonNull(contactCapabilities);
+ if (mIsSetDestroyedFlag) {
+ Log.d(TAG, "EabController destroyed.");
+ return;
+ }
+
+ Log.d(TAG, "Save capabilities: " + contactCapabilities.size());
+
+ // Update the capabilities
+ for (RcsContactUceCapability capability : contactCapabilities) {
+ String phoneNumber = getNumberFromUri(mContext, capability.getContactUri());
+ Cursor c = mContext.getContentResolver().query(
+ EabProvider.CONTACT_URI, null,
+ EabProvider.ContactColumns.PHONE_NUMBER + "=?",
+ new String[]{phoneNumber}, null);
+
+ if (c != null && c.moveToNext()) {
+ int contactId = getIntValue(c, EabProvider.ContactColumns._ID);
+ if (capability.getCapabilityMechanism() == CAPABILITY_MECHANISM_PRESENCE) {
+ Log.d(TAG, "Insert presence capability");
+ deleteOldPresenceCapability(contactId);
+ insertNewPresenceCapability(contactId, capability);
+ } else if (capability.getCapabilityMechanism() == CAPABILITY_MECHANISM_OPTIONS) {
+ Log.d(TAG, "Insert options capability");
+ deleteOldOptionCapability(contactId);
+ insertNewOptionCapability(contactId, capability);
+ }
+ } else {
+ Log.e(TAG, "The phone number can't find in contact table. ");
+ int contactId = insertNewContact(phoneNumber);
+ if (capability.getCapabilityMechanism() == CAPABILITY_MECHANISM_PRESENCE) {
+ insertNewPresenceCapability(contactId, capability);
+ } else if (capability.getCapabilityMechanism() == CAPABILITY_MECHANISM_OPTIONS) {
+ insertNewOptionCapability(contactId, capability);
+ }
+ }
+
+ if (c != null) {
+ c.close();
+ }
+ }
+
+ mEabBulkCapabilityUpdater.updateExpiredTimeAlert();
+
+ if (mHandler.hasCallbacks(mCapabilityCleanupRunnable)) {
+ mHandler.removeCallbacks(mCapabilityCleanupRunnable);
+ }
+ mHandler.postDelayed(mCapabilityCleanupRunnable,
+ CLEAN_UP_LEGACY_CAPABILITY_DELAY_MILLI_SEC);
+ }
+
+ private List<EabCapabilityResult> generateDestroyedResult(List<Uri> contactUri) {
+ List<EabCapabilityResult> destroyedResult = new ArrayList<>();
+ for (Uri uri : contactUri) {
+ destroyedResult.add(new EabCapabilityResult(
+ uri,
+ EabCapabilityResult.EAB_CONTROLLER_DESTROYED_FAILURE,
+ null));
+ }
+ return destroyedResult;
+ }
+
+ private EabCapabilityResult generateEabResult(Uri contactUri,
+ Predicate<Cursor> isExpiredMethod) {
+ RcsUceCapabilityBuilderWrapper builder = null;
+ EabCapabilityResult result;
+
+ // query EAB provider
+ Uri queryUri = Uri.withAppendedPath(
+ Uri.withAppendedPath(EabProvider.ALL_DATA_URI, String.valueOf(mSubId)),
+ getNumberFromUri(mContext, contactUri));
+ Cursor cursor = mContext.getContentResolver().query(
+ queryUri, null, null, null, null);
+
+ if (cursor != null && cursor.getCount() != 0) {
+ while (cursor.moveToNext()) {
+ if (isExpiredMethod.test(cursor)) {
+ continue;
+ }
+
+ if (builder == null) {
+ builder = createNewBuilder(contactUri, cursor);
+ } else {
+ updateCapability(contactUri, cursor, builder);
+ }
+ }
+ cursor.close();
+
+ if (builder == null) {
+ result = new EabCapabilityResult(contactUri,
+ EabCapabilityResult.EAB_CONTACT_EXPIRED_FAILURE,
+ null);
+ } else {
+ if (builder.getMechanism() == CAPABILITY_MECHANISM_PRESENCE) {
+ PresenceBuilder presenceBuilder = builder.getPresenceBuilder();
+ result = new EabCapabilityResult(contactUri,
+ EabCapabilityResult.EAB_QUERY_SUCCESSFUL,
+ presenceBuilder.build());
+ } else {
+ OptionsBuilder optionsBuilder = builder.getOptionsBuilder();
+ result = new EabCapabilityResult(contactUri,
+ EabCapabilityResult.EAB_QUERY_SUCCESSFUL,
+ optionsBuilder.build());
+ }
+
+ }
+ } else {
+ result = new EabCapabilityResult(contactUri,
+ EabCapabilityResult.EAB_CONTACT_NOT_FOUND_FAILURE, null);
+ }
+ return result;
+ }
+
+ private void updateCapability(Uri contactUri, Cursor cursor,
+ RcsUceCapabilityBuilderWrapper builderWrapper) {
+ if (builderWrapper.getMechanism() == CAPABILITY_MECHANISM_PRESENCE) {
+ PresenceBuilder builder = builderWrapper.getPresenceBuilder();
+ if (builder != null) {
+ builder.addCapabilityTuple(createPresenceTuple(contactUri, cursor));
+ }
+ } else {
+ OptionsBuilder builder = builderWrapper.getOptionsBuilder();
+ if (builder != null) {
+ builder.addFeatureTag(createOptionTuple(cursor));
+ }
+ }
+ }
+
+ private RcsUceCapabilityBuilderWrapper createNewBuilder(Uri contactUri, Cursor cursor) {
+ int mechanism = getIntValue(cursor, EabProvider.EabCommonColumns.MECHANISM);
+ int result = getIntValue(cursor, EabProvider.EabCommonColumns.REQUEST_RESULT);
+ RcsUceCapabilityBuilderWrapper builderWrapper =
+ new RcsUceCapabilityBuilderWrapper(mechanism);
+
+ if (mechanism == CAPABILITY_MECHANISM_PRESENCE) {
+ PresenceBuilder builder = new PresenceBuilder(
+ contactUri, SOURCE_TYPE_CACHED, result);
+ builder.addCapabilityTuple(createPresenceTuple(contactUri, cursor));
+ builderWrapper.setPresenceBuilder(builder);
+ } else {
+ OptionsBuilder builder = new OptionsBuilder(contactUri, SOURCE_TYPE_CACHED);
+ builder.setRequestResult(result);
+ builder.addFeatureTag(createOptionTuple(cursor));
+ builderWrapper.setOptionsBuilder(builder);
+ }
+ return builderWrapper;
+ }
+
+ private String createOptionTuple(Cursor cursor) {
+ return getStringValue(cursor, EabProvider.OptionsColumns.FEATURE_TAG);
+ }
+
+ private RcsContactPresenceTuple createPresenceTuple(Uri contactUri, Cursor cursor) {
+ // RcsContactPresenceTuple fields
+ String status = getStringValue(cursor, EabProvider.PresenceTupleColumns.BASIC_STATUS);
+ String serviceId = getStringValue(cursor, EabProvider.PresenceTupleColumns.SERVICE_ID);
+ String version = getStringValue(cursor, EabProvider.PresenceTupleColumns.SERVICE_VERSION);
+ String description = getStringValue(cursor, EabProvider.PresenceTupleColumns.DESCRIPTION);
+ String timeStamp = getStringValue(cursor,
+ EabProvider.PresenceTupleColumns.REQUEST_TIMESTAMP);
+
+ // ServiceCapabilities fields
+ boolean audioCapable = getIntValue(cursor,
+ EabProvider.PresenceTupleColumns.AUDIO_CAPABLE) == 1;
+ boolean videoCapable = getIntValue(cursor,
+ EabProvider.PresenceTupleColumns.VIDEO_CAPABLE) == 1;
+ String duplexModes = getStringValue(cursor,
+ EabProvider.PresenceTupleColumns.DUPLEX_MODE);
+ String unsupportedDuplexModes = getStringValue(cursor,
+ EabProvider.PresenceTupleColumns.UNSUPPORTED_DUPLEX_MODE);
+ String[] duplexModeList, unsupportedDuplexModeList;
+
+ if (!TextUtils.isEmpty(duplexModes)) {
+ duplexModeList = duplexModes.split(",");
+ } else {
+ duplexModeList = new String[0];
+ }
+ if (!TextUtils.isEmpty(unsupportedDuplexModes)) {
+ unsupportedDuplexModeList = unsupportedDuplexModes.split(",");
+ } else {
+ unsupportedDuplexModeList = new String[0];
+ }
+
+ // Create ServiceCapabilities
+ ServiceCapabilities serviceCapabilities;
+ ServiceCapabilities.Builder serviceCapabilitiesBuilder =
+ new ServiceCapabilities.Builder(audioCapable, videoCapable);
+ if (!TextUtils.isEmpty(duplexModes)
+ || !TextUtils.isEmpty(unsupportedDuplexModes)) {
+ for (String duplexMode : duplexModeList) {
+ serviceCapabilitiesBuilder.addSupportedDuplexMode(duplexMode);
+ }
+ for (String unsupportedDuplex : unsupportedDuplexModeList) {
+ serviceCapabilitiesBuilder.addUnsupportedDuplexMode(unsupportedDuplex);
+ }
+ }
+ serviceCapabilities = serviceCapabilitiesBuilder.build();
+
+ // Create RcsContactPresenceTuple
+ RcsContactPresenceTuple.Builder rcsContactPresenceTupleBuilder =
+ new RcsContactPresenceTuple.Builder(status, serviceId, version);
+ if (description != null) {
+ rcsContactPresenceTupleBuilder.setServiceDescription(description);
+ }
+ if (contactUri != null) {
+ rcsContactPresenceTupleBuilder.setContactUri(contactUri);
+ }
+ if (serviceCapabilities != null) {
+ rcsContactPresenceTupleBuilder.setServiceCapabilities(serviceCapabilities);
+ }
+ if (timeStamp != null) {
+ try {
+ Instant instant = Instant.ofEpochSecond(Long.parseLong(timeStamp));
+ rcsContactPresenceTupleBuilder.setTime(instant);
+ } catch (NumberFormatException ex) {
+ Log.w(TAG, "Create presence tuple: NumberFormatException");
+ } catch (DateTimeParseException e) {
+ Log.w(TAG, "Create presence tuple: parse timestamp failed");
+ }
+ }
+
+ return rcsContactPresenceTupleBuilder.build();
+ }
+
+ private boolean isCapabilityExpired(Cursor cursor) {
+ boolean expired = false;
+ String requestTimeStamp = getRequestTimestamp(cursor);
+ int capabilityCacheExpiration;
+
+ if (isNonRcsCapability(cursor)) {
+ capabilityCacheExpiration = getNonRcsCapabilityCacheExpiration(mSubId);
+ } else {
+ capabilityCacheExpiration = getCapabilityCacheExpiration(mSubId);
+ }
+
+ if (requestTimeStamp != null) {
+ Instant expiredTimestamp = Instant
+ .ofEpochSecond(Long.parseLong(requestTimeStamp))
+ .plus(capabilityCacheExpiration, ChronoUnit.SECONDS);
+ expired = expiredTimestamp.isBefore(Instant.now());
+ Log.d(TAG, "Capability expiredTimestamp: " + expiredTimestamp.getEpochSecond() +
+ ", isNonRcsCapability: " + isNonRcsCapability(cursor) +
+ ", capabilityCacheExpiration: " + capabilityCacheExpiration +
+ ", expired:" + expired);
+ } else {
+ Log.d(TAG, "Capability requestTimeStamp is null");
+ }
+ return expired;
+ }
+
+ private boolean isNonRcsCapability(Cursor cursor) {
+ int result = getIntValue(cursor, EabProvider.EabCommonColumns.REQUEST_RESULT);
+ return result == REQUEST_RESULT_NOT_FOUND;
+ }
+
+ private boolean isAvailabilityExpired(Cursor cursor) {
+ boolean expired = false;
+ String requestTimeStamp = getRequestTimestamp(cursor);
+
+ if (requestTimeStamp != null) {
+ Instant expiredTimestamp = Instant
+ .ofEpochSecond(Long.parseLong(requestTimeStamp))
+ .plus(getAvailabilityCacheExpiration(mSubId), ChronoUnit.SECONDS);
+ expired = expiredTimestamp.isBefore(Instant.now());
+ Log.d(TAG, "Availability insertedTimestamp: "
+ + expiredTimestamp.getEpochSecond() + ", expired:" + expired);
+ } else {
+ Log.d(TAG, "Capability requestTimeStamp is null");
+ }
+ return expired;
+ }
+
+ private String getRequestTimestamp(Cursor cursor) {
+ String expiredTimestamp = null;
+ int mechanism = getIntValue(cursor, EabProvider.EabCommonColumns.MECHANISM);
+ if (mechanism == CAPABILITY_MECHANISM_PRESENCE) {
+ expiredTimestamp = getStringValue(cursor,
+ EabProvider.PresenceTupleColumns.REQUEST_TIMESTAMP);
+
+ } else if (mechanism == CAPABILITY_MECHANISM_OPTIONS) {
+ expiredTimestamp = getStringValue(cursor, EabProvider.OptionsColumns.REQUEST_TIMESTAMP);
+ }
+ return expiredTimestamp;
+ }
+
+ private int getNonRcsCapabilityCacheExpiration(int subId) {
+ int value;
+ PersistableBundle carrierConfig =
+ mContext.getSystemService(CarrierConfigManager.class).getConfigForSubId(subId);
+
+ if (carrierConfig != null) {
+ value = carrierConfig.getInt(
+ CarrierConfigManager.Ims.KEY_NON_RCS_CAPABILITIES_CACHE_EXPIRATION_SEC_INT);
+ } else {
+ value = DEFAULT_CAPABILITY_CACHE_EXPIRATION_SEC;
+ Log.e(TAG, "getNonRcsCapabilityCacheExpiration: " +
+ "CarrierConfig is null, returning default");
+ }
+ return value;
+ }
+
+ protected static int getCapabilityCacheExpiration(int subId) {
+ int value = -1;
+ try {
+ ProvisioningManager pm = ProvisioningManager.createForSubscriptionId(subId);
+ value = pm.getProvisioningIntValue(
+ ProvisioningManager.KEY_RCS_CAPABILITIES_CACHE_EXPIRATION_SEC);
+ } catch (Exception ex) {
+ Log.e(TAG, "Exception in getCapabilityCacheExpiration(): " + ex);
+ }
+
+ if (value <= 0) {
+ value = DEFAULT_CAPABILITY_CACHE_EXPIRATION_SEC;
+ Log.e(TAG, "The capability expiration cannot be less than 0.");
+ }
+ return value;
+ }
+
+ protected static long getAvailabilityCacheExpiration(int subId) {
+ long value = -1;
+ try {
+ ProvisioningManager pm = ProvisioningManager.createForSubscriptionId(subId);
+ value = pm.getProvisioningIntValue(
+ ProvisioningManager.KEY_RCS_AVAILABILITY_CACHE_EXPIRATION_SEC);
+ } catch (Exception ex) {
+ Log.e(TAG, "Exception in getAvailabilityCacheExpiration(): " + ex);
+ }
+
+ if (value <= 0) {
+ value = DEFAULT_AVAILABILITY_CACHE_EXPIRATION_SEC;
+ Log.e(TAG, "The Availability expiration cannot be less than 0.");
+ }
+ return value;
+ }
+
+ private int insertNewContact(String phoneNumber) {
+ ContentValues contentValues = new ContentValues();
+ contentValues.put(EabProvider.ContactColumns.PHONE_NUMBER, phoneNumber);
+ Uri result = mContext.getContentResolver().insert(EabProvider.CONTACT_URI, contentValues);
+ return Integer.parseInt(result.getLastPathSegment());
+ }
+
+ private void deleteOldPresenceCapability(int id) {
+ Cursor c = mContext.getContentResolver().query(
+ EabProvider.COMMON_URI,
+ new String[]{EabProvider.EabCommonColumns._ID},
+ EabProvider.EabCommonColumns.EAB_CONTACT_ID + "=?",
+ new String[]{String.valueOf(id)}, null);
+
+ if (c != null && c.getCount() > 0) {
+ while(c.moveToNext()) {
+ int commonId = c.getInt(c.getColumnIndex(EabProvider.EabCommonColumns._ID));
+ mContext.getContentResolver().delete(
+ EabProvider.PRESENCE_URI,
+ EabProvider.PresenceTupleColumns.EAB_COMMON_ID + "=?",
+ new String[]{String.valueOf(commonId)});
+ }
+ }
+
+ if (c != null) {
+ c.close();
+ }
+ }
+
+ private void insertNewPresenceCapability(int contactId, RcsContactUceCapability capability) {
+ ContentValues contentValues = new ContentValues();
+ contentValues.put(EabProvider.EabCommonColumns.EAB_CONTACT_ID, contactId);
+ contentValues.put(EabProvider.EabCommonColumns.MECHANISM, CAPABILITY_MECHANISM_PRESENCE);
+ contentValues.put(EabProvider.EabCommonColumns.SUBSCRIPTION_ID, mSubId);
+ contentValues.put(EabProvider.EabCommonColumns.REQUEST_RESULT,
+ capability.getRequestResult());
+ Uri result = mContext.getContentResolver().insert(EabProvider.COMMON_URI, contentValues);
+ int commonId = Integer.parseInt(result.getLastPathSegment());
+ Log.d(TAG, "Insert into common table. Id: " + commonId);
+
+ ContentValues[] presenceContent =
+ new ContentValues[capability.getCapabilityTuples().size()];
+ for (int i = 0; i < presenceContent.length; i++) {
+ RcsContactPresenceTuple tuple = capability.getCapabilityTuples().get(i);
+
+ // Create new ServiceCapabilities
+ ServiceCapabilities serviceCapabilities = tuple.getServiceCapabilities();
+ String duplexMode = null, unsupportedDuplexMode = null;
+ if (serviceCapabilities != null) {
+ List<String> duplexModes = serviceCapabilities.getSupportedDuplexModes();
+ if (duplexModes.size() != 0) {
+ duplexMode = TextUtils.join(",", duplexModes);
+ }
+
+ List<String> unsupportedDuplexModes =
+ serviceCapabilities.getUnsupportedDuplexModes();
+ if (unsupportedDuplexModes.size() != 0) {
+ unsupportedDuplexMode =
+ TextUtils.join(",", unsupportedDuplexModes);
+ }
+ }
+
+ // Using the current timestamp if the timestamp doesn't populate
+ Long timestamp;
+ if (tuple.getTime() != null) {
+ timestamp = tuple.getTime().getEpochSecond();
+ } else {
+ timestamp = Instant.now().getEpochSecond();
+ }
+
+ contentValues = new ContentValues();
+ contentValues.put(EabProvider.PresenceTupleColumns.EAB_COMMON_ID, commonId);
+ contentValues.put(EabProvider.PresenceTupleColumns.BASIC_STATUS, tuple.getStatus());
+ contentValues.put(EabProvider.PresenceTupleColumns.SERVICE_ID, tuple.getServiceId());
+ contentValues.put(EabProvider.PresenceTupleColumns.SERVICE_VERSION,
+ tuple.getServiceVersion());
+ contentValues.put(EabProvider.PresenceTupleColumns.DESCRIPTION,
+ tuple.getServiceDescription());
+ contentValues.put(EabProvider.PresenceTupleColumns.REQUEST_TIMESTAMP, timestamp);
+ contentValues.put(EabProvider.PresenceTupleColumns.CONTACT_URI,
+ tuple.getContactUri().toString());
+ if (serviceCapabilities != null) {
+ contentValues.put(EabProvider.PresenceTupleColumns.DUPLEX_MODE, duplexMode);
+ contentValues.put(EabProvider.PresenceTupleColumns.UNSUPPORTED_DUPLEX_MODE,
+ unsupportedDuplexMode);
+
+ contentValues.put(EabProvider.PresenceTupleColumns.AUDIO_CAPABLE,
+ serviceCapabilities.isAudioCapable());
+ contentValues.put(EabProvider.PresenceTupleColumns.VIDEO_CAPABLE,
+ serviceCapabilities.isVideoCapable());
+ }
+ presenceContent[i] = contentValues;
+ }
+ Log.d(TAG, "Insert into presence table. count: " + presenceContent.length);
+ mContext.getContentResolver().bulkInsert(EabProvider.PRESENCE_URI, presenceContent);
+ }
+
+ private void deleteOldOptionCapability(int contactId) {
+ Cursor c = mContext.getContentResolver().query(
+ EabProvider.COMMON_URI,
+ new String[]{EabProvider.EabCommonColumns._ID},
+ EabProvider.EabCommonColumns.EAB_CONTACT_ID + "=?",
+ new String[]{String.valueOf(contactId)}, null);
+
+ if (c != null && c.getCount() > 0) {
+ while(c.moveToNext()) {
+ int commonId = c.getInt(c.getColumnIndex(EabProvider.EabCommonColumns._ID));
+ mContext.getContentResolver().delete(
+ EabProvider.OPTIONS_URI,
+ EabProvider.OptionsColumns.EAB_COMMON_ID + "=?",
+ new String[]{String.valueOf(commonId)});
+ }
+ }
+
+ if (c != null) {
+ c.close();
+ }
+ }
+
+ private void insertNewOptionCapability(int contactId, RcsContactUceCapability capability) {
+ ContentValues contentValues = new ContentValues();
+ contentValues.put(EabProvider.EabCommonColumns.EAB_CONTACT_ID, contactId);
+ contentValues.put(EabProvider.EabCommonColumns.MECHANISM, CAPABILITY_MECHANISM_OPTIONS);
+ contentValues.put(EabProvider.EabCommonColumns.SUBSCRIPTION_ID, mSubId);
+ contentValues.put(EabProvider.EabCommonColumns.REQUEST_RESULT,
+ capability.getRequestResult());
+ Uri result = mContext.getContentResolver().insert(EabProvider.COMMON_URI, contentValues);
+
+ int commonId = Integer.valueOf(result.getLastPathSegment());
+ List<ContentValues> optionContentList = new ArrayList<>();
+ for (String feature : capability.getFeatureTags()) {
+ contentValues = new ContentValues();
+ contentValues.put(EabProvider.OptionsColumns.EAB_COMMON_ID, commonId);
+ contentValues.put(EabProvider.OptionsColumns.FEATURE_TAG, feature);
+ contentValues.put(EabProvider.OptionsColumns.REQUEST_TIMESTAMP,
+ Instant.now().getEpochSecond());
+ optionContentList.add(contentValues);
+ }
+
+ ContentValues[] optionContent = new ContentValues[optionContentList.size()];
+ optionContent = optionContentList.toArray(optionContent);
+ mContext.getContentResolver().bulkInsert(EabProvider.OPTIONS_URI, optionContent);
+ }
+
+ private void cleanupExpiredCapabilities() {
+ // Cleanup the capabilities that expired more than 1 week
+ long rcsCapabilitiesExpiredTime = Instant.now().getEpochSecond() -
+ getCapabilityCacheExpiration(mSubId) -
+ CLEAN_UP_LEGACY_CAPABILITY_SEC;
+
+ // Cleanup the capabilities that expired more than 1 week
+ long nonRcsCapabilitiesExpiredTime = Instant.now().getEpochSecond() -
+ getNonRcsCapabilityCacheExpiration(mSubId) -
+ CLEAN_UP_LEGACY_CAPABILITY_SEC;
+
+ cleanupCapabilities(rcsCapabilitiesExpiredTime, getRcsCommonIdList());
+ cleanupCapabilities(nonRcsCapabilitiesExpiredTime, getNonRcsCommonIdList());
+ cleanupOrphanedRows();
+ }
+
+ private void cleanupCapabilities(long rcsCapabilitiesExpiredTime, List<Integer> commonIdList) {
+ if (commonIdList.size() > 0) {
+ String presenceClause =
+ EabProvider.PresenceTupleColumns.EAB_COMMON_ID +
+ " IN (" + TextUtils.join(",", commonIdList) + ") " + " AND " +
+ EabProvider.PresenceTupleColumns.REQUEST_TIMESTAMP + "<?";
+
+ String optionClause =
+ EabProvider.PresenceTupleColumns.EAB_COMMON_ID +
+ " IN (" + TextUtils.join(",", commonIdList) + ") " + " AND " +
+ EabProvider.OptionsColumns.REQUEST_TIMESTAMP + "<?";
+
+ int deletePresenceCount = mContext.getContentResolver().delete(
+ EabProvider.PRESENCE_URI,
+ presenceClause,
+ new String[]{String.valueOf(rcsCapabilitiesExpiredTime)});
+
+ int deleteOptionsCount = mContext.getContentResolver().delete(
+ EabProvider.OPTIONS_URI,
+ optionClause,
+ new String[]{String.valueOf(rcsCapabilitiesExpiredTime)});
+
+ Log.d(TAG, "Cleanup capabilities. deletePresenceCount: " + deletePresenceCount +
+ ",deleteOptionsCount: " + deleteOptionsCount);
+ }
+ }
+
+ private List<Integer> getRcsCommonIdList() {
+ ArrayList<Integer> list = new ArrayList<>();
+ Cursor cursor = mContext.getContentResolver().query(
+ EabProvider.COMMON_URI,
+ null,
+ EabProvider.EabCommonColumns.REQUEST_RESULT + "<>?",
+ new String[]{String.valueOf(REQUEST_RESULT_NOT_FOUND)},
+ null);
+
+ if (cursor == null) return list;
+
+ while (cursor.moveToNext()) {
+ list.add(cursor.getInt(cursor.getColumnIndex(EabProvider.EabCommonColumns._ID)));
+ }
+ cursor.close();
+
+ return list;
+ }
+
+ private List<Integer> getNonRcsCommonIdList() {
+ ArrayList<Integer> list = new ArrayList<>();
+ Cursor cursor = mContext.getContentResolver().query(
+ EabProvider.COMMON_URI,
+ null,
+ EabProvider.EabCommonColumns.REQUEST_RESULT + "=?",
+ new String[]{String.valueOf(REQUEST_RESULT_NOT_FOUND)},
+ null);
+
+ if (cursor == null) return list;
+
+ while (cursor.moveToNext()) {
+ list.add(cursor.getInt(cursor.getColumnIndex(EabProvider.EabCommonColumns._ID)));
+ }
+ cursor.close();
+
+ return list;
+ }
+
+ /**
+ * Cleanup the entry of common table that can't map to presence or option table
+ */
+ private void cleanupOrphanedRows() {
+ String presenceSelection =
+ " (SELECT " + EabProvider.PresenceTupleColumns.EAB_COMMON_ID +
+ " FROM " + EAB_PRESENCE_TUPLE_TABLE_NAME + ") ";
+ String optionSelection =
+ " (SELECT " + EabProvider.OptionsColumns.EAB_COMMON_ID +
+ " FROM " + EAB_OPTIONS_TABLE_NAME + ") ";
+
+ mContext.getContentResolver().delete(
+ EabProvider.COMMON_URI,
+ EabProvider.EabCommonColumns._ID + " NOT IN " + presenceSelection +
+ " AND " + EabProvider.EabCommonColumns._ID+ " NOT IN " + optionSelection,
+ null);
+ }
+
+ private String getStringValue(Cursor cursor, String column) {
+ return cursor.getString(cursor.getColumnIndex(column));
+ }
+
+ private int getIntValue(Cursor cursor, String column) {
+ return cursor.getInt(cursor.getColumnIndex(column));
+ }
+
+ private static String getNumberFromUri(Context context, Uri uri) {
+ String number = uri.getSchemeSpecificPart();
+ String[] numberParts = number.split("[@;:]");
+ if (numberParts.length == 0) {
+ return null;
+ }
+ return formatNumber(context, numberParts[0]);
+ }
+
+ static String formatNumber(Context context, String number) {
+ TelephonyManager manager = context.getSystemService(TelephonyManager.class);
+ String simCountryIso = manager.getSimCountryIso();
+ if (simCountryIso != null) {
+ simCountryIso = simCountryIso.toUpperCase();
+ PhoneNumberUtil util = PhoneNumberUtil.getInstance();
+ try {
+ Phonenumber.PhoneNumber phoneNumber = util.parse(number, simCountryIso);
+ return util.format(phoneNumber, PhoneNumberUtil.PhoneNumberFormat.E164);
+ } catch (NumberParseException e) {
+ Log.w(TAG, "formatNumber: could not format " + number + ", error: " + e);
+ }
+ }
+ return number;
+ }
+}
diff --git a/src/java/com/android/ims/rcs/uce/eab/EabProvider.java b/src/java/com/android/ims/rcs/uce/eab/EabProvider.java
new file mode 100644
index 00000000..60283c22
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/eab/EabProvider.java
@@ -0,0 +1,667 @@
+/*
+ * 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.ims.rcs.uce.eab;
+
+import static android.content.ContentResolver.NOTIFY_DELETE;
+import static android.content.ContentResolver.NOTIFY_INSERT;
+import static android.content.ContentResolver.NOTIFY_UPDATE;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.UriMatcher;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.database.sqlite.SQLiteQueryBuilder;
+import android.net.Uri;
+import android.provider.BaseColumns;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * This class provides the ability to query the enhanced address book databases(A.K.A. EAB) based on
+ * both SIP options and UCE presence server data.
+ *
+ * <p>
+ * There are 4 tables in EAB DB:
+ * <ul>
+ * <li><em>Contact:</em> It stores the name and phone number of the contact.
+ *
+ * <li><em>Common:</em> It's a general table for storing the query results and the mechanisms of
+ * querying UCE capabilities. It should be 1:1 mapped to the contact table and has a foreign
+ * key(eab_contact_id) that refers to the id of contact table. If the value of mechanism is
+ * 1 ({@link android.telephony.ims.RcsContactUceCapability#CAPABILITY_MECHANISM_PRESENCE}), the
+ * capability information should be stored in presence table, if the value of mechanism is
+ * 2({@link android.telephony.ims.RcsContactUceCapability#CAPABILITY_MECHANISM_OPTIONS}), it
+ * should be stored in options table.
+ *
+ * <li><em>Presence:</em> It stores the information
+ * ({@link android.telephony.ims.RcsContactUceCapability}) that queried through presence server.
+ * It should be *:1 mapped to the common table and has a foreign key(eab_common_id) that refers
+ * to the id of common table.
+ *
+ * <li><em>Options:</em> It stores the information
+ * ({@link android.telephony.ims.RcsContactUceCapability}) that queried through SIP OPTIONS. It
+ * should be *:1 mapped to the common table and it has a foreign key(eab_common_id) that refers
+ * to the id of common table.
+ * </ul>
+ * </p>
+ */
+public class EabProvider extends ContentProvider {
+ // The public URI for operating Eab DB. They support query, insert, delete and update.
+ public static final Uri CONTACT_URI = Uri.parse("content://eab/contact");
+ public static final Uri COMMON_URI = Uri.parse("content://eab/common");
+ public static final Uri PRESENCE_URI = Uri.parse("content://eab/presence");
+ public static final Uri OPTIONS_URI = Uri.parse("content://eab/options");
+
+ // The public URI for querying EAB DB. Only support query.
+ public static final Uri ALL_DATA_URI = Uri.parse("content://eab/all");
+
+ @VisibleForTesting
+ public static final String AUTHORITY = "eab";
+
+ private static final String TAG = "EabProvider";
+ private static final int DATABASE_VERSION = 2;
+
+ public static final String EAB_CONTACT_TABLE_NAME = "eab_contact";
+ public static final String EAB_COMMON_TABLE_NAME = "eab_common";
+ public static final String EAB_PRESENCE_TUPLE_TABLE_NAME = "eab_presence";
+ public static final String EAB_OPTIONS_TABLE_NAME = "eab_options";
+
+ private static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
+ private static final int URL_CONTACT = 1;
+ private static final int URL_COMMON = 2;
+ private static final int URL_PRESENCE = 3;
+ private static final int URL_OPTIONS = 4;
+ private static final int URL_ALL = 5;
+ private static final int URL_ALL_WITH_SUB_ID_AND_PHONE_NUMBER = 6;
+
+ static {
+ URI_MATCHER.addURI(AUTHORITY, "contact", URL_CONTACT);
+ URI_MATCHER.addURI(AUTHORITY, "common", URL_COMMON);
+ URI_MATCHER.addURI(AUTHORITY, "presence", URL_PRESENCE);
+ URI_MATCHER.addURI(AUTHORITY, "options", URL_OPTIONS);
+ URI_MATCHER.addURI(AUTHORITY, "all", URL_ALL);
+ URI_MATCHER.addURI(AUTHORITY, "all/#/*", URL_ALL_WITH_SUB_ID_AND_PHONE_NUMBER);
+ }
+
+ private static final String QUERY_CONTACT_TABLE =
+ " SELECT * FROM " + EAB_CONTACT_TABLE_NAME;
+
+ private static final String JOIN_ALL_TABLES =
+ // join common table
+ " INNER JOIN " + EAB_COMMON_TABLE_NAME
+ + " ON " + EAB_CONTACT_TABLE_NAME + "." + ContactColumns._ID
+ + "=" + EAB_COMMON_TABLE_NAME + "." + EabCommonColumns.EAB_CONTACT_ID
+
+ // join options table
+ + " LEFT JOIN " + EAB_OPTIONS_TABLE_NAME
+ + " ON " + EAB_COMMON_TABLE_NAME + "." + EabCommonColumns._ID
+ + "=" + EAB_OPTIONS_TABLE_NAME + "." + OptionsColumns.EAB_COMMON_ID
+
+ // join presence table
+ + " LEFT JOIN " + EAB_PRESENCE_TUPLE_TABLE_NAME
+ + " ON " + EAB_COMMON_TABLE_NAME + "." + EabCommonColumns._ID
+ + "=" + EAB_PRESENCE_TUPLE_TABLE_NAME + "."
+ + PresenceTupleColumns.EAB_COMMON_ID;
+
+ /**
+ * The contact table's columns.
+ */
+ public static class ContactColumns implements BaseColumns {
+
+ /**
+ * The contact's phone number. It may come from contact provider or someone via
+ * {@link EabControllerImpl#saveCapabilities(List)} to save the capability but the phone
+ * number not in contact provider.
+ *
+ * <P>Type: TEXT</P>
+ */
+ public static final String PHONE_NUMBER = "phone_number";
+
+ /**
+ * The ID of contact that store in contact provider. It refer to the
+ * {@link android.provider.ContactsContract.Data#CONTACT_ID}. If the phone number not in
+ * contact provider, the value should be null.
+ *
+ * <P>Type: INTEGER</P>
+ */
+ public static final String CONTACT_ID = "contact_id";
+
+ /**
+ * The ID of contact that store in contact provider. It refer to the
+ * {@link android.provider.ContactsContract.Data#RAW_CONTACT_ID}. If the phone number not in
+ * contact provider, the value should be null.
+ *
+ * <P>Type: INTEGER</P>
+ */
+ public static final String RAW_CONTACT_ID = "raw_contact_id";
+
+ /**
+ * The ID of phone number that store in contact provider. It refer to the
+ * {@link android.provider.ContactsContract.Data#_ID}. If the phone number not in
+ * contact provider, the value should be null.
+ *
+ * <P>Type: INTEGER</P>
+ */
+ public static final String DATA_ID = "data_id";
+ }
+
+ /**
+ * The common table's columns. The eab_contact_id should refer to the id of contact table.
+ */
+ public static class EabCommonColumns implements BaseColumns {
+
+ /**
+ * A reference to the {@link ContactColumns#_ID} that this data belongs to.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String EAB_CONTACT_ID = "eab_contact_id";
+
+ /**
+ * The mechanism of querying UCE capability. Possible values are
+ * {@link android.telephony.ims.RcsContactUceCapability#CAPABILITY_MECHANISM_OPTIONS }
+ * and
+ * {@link android.telephony.ims.RcsContactUceCapability#CAPABILITY_MECHANISM_PRESENCE }
+ * <P>Type: INTEGER</P>
+ */
+ public static final String MECHANISM = "mechanism";
+
+ /**
+ * The result of querying UCE capability. Possible values are
+ * {@link android.telephony.ims.RcsContactUceCapability#REQUEST_RESULT_NOT_ONLINE }
+ * and
+ * {@link android.telephony.ims.RcsContactUceCapability#REQUEST_RESULT_NOT_FOUND }
+ * and
+ * {@link android.telephony.ims.RcsContactUceCapability#REQUEST_RESULT_FOUND }
+ * and
+ * {@link android.telephony.ims.RcsContactUceCapability#REQUEST_RESULT_UNKNOWN }
+ * <P>Type: INTEGER</P>
+ */
+ public static final String REQUEST_RESULT = "request_result";
+
+ /**
+ * The subscription id.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String SUBSCRIPTION_ID = "subscription_id";
+ }
+
+ /**
+ * This is used to generate a instance of {@link android.telephony.ims.RcsContactPresenceTuple}.
+ * See that class for more information on each of these parameters.
+ */
+ public static class PresenceTupleColumns implements BaseColumns {
+
+ /**
+ * A reference to the {@link ContactColumns#_ID} that this data belongs to.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String EAB_COMMON_ID = "eab_common_id";
+
+ /**
+ * The basic status of service capabilities. Possible values are
+ * {@link android.telephony.ims.RcsContactPresenceTuple#TUPLE_BASIC_STATUS_OPEN}
+ * and
+ * {@link android.telephony.ims.RcsContactPresenceTuple#TUPLE_BASIC_STATUS_CLOSED}
+ * <P>Type: TEXT</P>
+ */
+ public static final String BASIC_STATUS = "basic_status";
+
+ /**
+ * The OMA Presence service-id associated with this capability. See the OMA Presence SIMPLE
+ * specification v1.1, section 10.5.1.
+ * <P>Type: TEXT</P>
+ */
+ public static final String SERVICE_ID = "service_id";
+
+ /**
+ * The contact uri of service capabilities.
+ * <P>Type: TEXT</P>
+ */
+ public static final String CONTACT_URI = "contact_uri";
+
+ /**
+ * The service version of service capabilities.
+ * <P>Type: TEXT</P>
+ */
+ public static final String SERVICE_VERSION = "service_version";
+
+ /**
+ * The description of service capabilities.
+ * <P>Type: TEXT</P>
+ */
+ public static final String DESCRIPTION = "description";
+
+ /**
+ * The supported duplex mode of service capabilities. Possible values are
+ * {@link android.telephony.ims.RcsContactPresenceTuple.ServiceCapabilities#DUPLEX_MODE_FULL}
+ * and
+ * {@link android.telephony.ims.RcsContactPresenceTuple.ServiceCapabilities#DUPLEX_MODE_HALF}
+ * and
+ * {@link android.telephony.ims.RcsContactPresenceTuple.ServiceCapabilities#DUPLEX_MODE_RECEIVE_ONLY}
+ * and
+ * {@link android.telephony.ims.RcsContactPresenceTuple.ServiceCapabilities#DUPLEX_MODE_SEND_ONLY}
+ * <P>Type: TEXT</P>
+ */
+ public static final String DUPLEX_MODE = "duplex_mode";
+
+ /**
+ * The unsupported duplex mode of service capabilities. Possible values are
+ * {@link android.telephony.ims.RcsContactPresenceTuple.ServiceCapabilities#DUPLEX_MODE_FULL}
+ * and
+ * {@link android.telephony.ims.RcsContactPresenceTuple.ServiceCapabilities#DUPLEX_MODE_HALF}
+ * and
+ * {@link android.telephony.ims.RcsContactPresenceTuple.ServiceCapabilities#DUPLEX_MODE_RECEIVE_ONLY}
+ * and
+ * {@link android.telephony.ims.RcsContactPresenceTuple.ServiceCapabilities#DUPLEX_MODE_SEND_ONLY}
+ * <P>Type: TEXT</P>
+ */
+ public static final String UNSUPPORTED_DUPLEX_MODE = "unsupported_duplex_mode";
+
+ /**
+ * The presence request timestamp. Represents seconds of UTC time since Unix epoch
+ * 1970-01-01 00:00:00.
+ * <P>Type: LONG</P>
+ */
+ public static final String REQUEST_TIMESTAMP = "presence_request_timestamp";
+
+ /**
+ * The audio capable.
+ * <P>Type: BOOLEAN </P>
+ */
+ public static final String AUDIO_CAPABLE = "audio_capable";
+
+ /**
+ * The video capable.
+ * <P>Type: BOOLEAN </P>
+ */
+ public static final String VIDEO_CAPABLE = "video_capable";
+ }
+
+ /**
+ * This is used to generate a instance of {@link android.telephony.ims.RcsContactPresenceTuple}.
+ * See that class for more information on each of these parameters.
+ */
+ public static class OptionsColumns implements BaseColumns {
+
+ /**
+ * A reference to the {@link ContactColumns#_ID} that this data belongs to.
+ * <P>Type: INTEGER</P>
+ */
+ public static final String EAB_COMMON_ID = "eab_common_id";
+
+ /**
+ * An IMS feature tag indicating the capabilities of the contact. See RFC3840 #section-9.
+ * <P>Type: TEXT</P>
+ */
+ public static final String FEATURE_TAG = "feature_tag";
+
+ /**
+ * The request timestamp of options capabilities.
+ * <P>Type: LONG</P>
+ */
+ public static final String REQUEST_TIMESTAMP = "options_request_timestamp";
+ }
+
+ @VisibleForTesting
+ public static final class EabDatabaseHelper extends SQLiteOpenHelper {
+ private static final String DB_NAME = "EabDatabase";
+ private static final List<String> CONTACT_UNIQUE_FIELDS = new ArrayList<>();
+ private static final List<String> COMMON_UNIQUE_FIELDS = new ArrayList<>();
+
+ static {
+ CONTACT_UNIQUE_FIELDS.add(ContactColumns.PHONE_NUMBER);
+
+ COMMON_UNIQUE_FIELDS.add(EabCommonColumns.EAB_CONTACT_ID);
+ }
+
+ @VisibleForTesting
+ public static final String SQL_CREATE_CONTACT_TABLE = "CREATE TABLE "
+ + EAB_CONTACT_TABLE_NAME
+ + " ("
+ + ContactColumns._ID + " INTEGER PRIMARY KEY, "
+ + ContactColumns.PHONE_NUMBER + " Text DEFAULT NULL, "
+ + ContactColumns.CONTACT_ID + " INTEGER DEFAULT -1, "
+ + ContactColumns.RAW_CONTACT_ID + " INTEGER DEFAULT -1, "
+ + ContactColumns.DATA_ID + " INTEGER DEFAULT -1, "
+ + "UNIQUE (" + TextUtils.join(", ", CONTACT_UNIQUE_FIELDS) + ")"
+ + ");";
+
+ @VisibleForTesting
+ public static final String SQL_CREATE_COMMON_TABLE = "CREATE TABLE "
+ + EAB_COMMON_TABLE_NAME
+ + " ("
+ + EabCommonColumns._ID + " INTEGER PRIMARY KEY, "
+ + EabCommonColumns.EAB_CONTACT_ID + " INTEGER DEFAULT -1, "
+ + EabCommonColumns.MECHANISM + " INTEGER DEFAULT NULL, "
+ + EabCommonColumns.REQUEST_RESULT + " INTEGER DEFAULT -1, "
+ + EabCommonColumns.SUBSCRIPTION_ID + " INTEGER DEFAULT -1, "
+ + "UNIQUE (" + TextUtils.join(", ", COMMON_UNIQUE_FIELDS) + ")"
+ + ");";
+
+ @VisibleForTesting
+ public static final String SQL_CREATE_PRESENCE_TUPLE_TABLE = "CREATE TABLE "
+ + EAB_PRESENCE_TUPLE_TABLE_NAME
+ + " ("
+ + PresenceTupleColumns._ID + " INTEGER PRIMARY KEY, "
+ + PresenceTupleColumns.EAB_COMMON_ID + " INTEGER DEFAULT -1, "
+ + PresenceTupleColumns.BASIC_STATUS + " TEXT DEFAULT NULL, "
+ + PresenceTupleColumns.SERVICE_ID + " TEXT DEFAULT NULL, "
+ + PresenceTupleColumns.SERVICE_VERSION + " TEXT DEFAULT NULL, "
+ + PresenceTupleColumns.DESCRIPTION + " TEXT DEFAULT NULL, "
+ + PresenceTupleColumns.REQUEST_TIMESTAMP + " LONG DEFAULT NULL, "
+ + PresenceTupleColumns.CONTACT_URI + " TEXT DEFAULT NULL, "
+
+ // For ServiceCapabilities
+ + PresenceTupleColumns.DUPLEX_MODE + " TEXT DEFAULT NULL, "
+ + PresenceTupleColumns.UNSUPPORTED_DUPLEX_MODE + " TEXT DEFAULT NULL, "
+ + PresenceTupleColumns.AUDIO_CAPABLE + " BOOLEAN DEFAULT NULL, "
+ + PresenceTupleColumns.VIDEO_CAPABLE + " BOOLEAN DEFAULT NULL"
+ + ");";
+
+ @VisibleForTesting
+ public static final String SQL_CREATE_OPTIONS_TABLE = "CREATE TABLE "
+ + EAB_OPTIONS_TABLE_NAME
+ + " ("
+ + OptionsColumns._ID + " INTEGER PRIMARY KEY, "
+ + OptionsColumns.EAB_COMMON_ID + " INTEGER DEFAULT -1, "
+ + OptionsColumns.REQUEST_TIMESTAMP + " LONG DEFAULT NULL, "
+ + OptionsColumns.FEATURE_TAG + " TEXT DEFAULT NULL "
+ + ");";
+
+ EabDatabaseHelper(Context context) {
+ super(context, DB_NAME, null, DATABASE_VERSION);
+ }
+
+ public void onCreate(SQLiteDatabase db) {
+ db.execSQL(SQL_CREATE_CONTACT_TABLE);
+ db.execSQL(SQL_CREATE_COMMON_TABLE);
+ db.execSQL(SQL_CREATE_PRESENCE_TUPLE_TABLE);
+ db.execSQL(SQL_CREATE_OPTIONS_TABLE);
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase sqLiteDatabase, int oldVersion, int newVersion) {
+ Log.d(TAG, "DB upgrade from " + oldVersion + " to " + newVersion);
+
+ if (oldVersion < 2) {
+ sqLiteDatabase.execSQL("ALTER TABLE " + EAB_CONTACT_TABLE_NAME + " ADD COLUMN "
+ + ContactColumns.CONTACT_ID + " INTEGER DEFAULT -1;");
+ oldVersion = 2;
+ }
+ }
+ }
+
+ private EabDatabaseHelper mOpenHelper;
+
+ @Override
+ public boolean onCreate() {
+ mOpenHelper = new EabDatabaseHelper(getContext());
+ return true;
+ }
+
+ /**
+ * Support 6 URLs for querying:
+ *
+ * <ul>
+ * <li>{@link #URL_CONTACT}: query contact table.
+ *
+ * <li>{@link #URL_COMMON}: query common table.
+ *
+ * <li>{@link #URL_PRESENCE}: query presence capability table.
+ *
+ * <li>{@link #URL_OPTIONS}: query options capability table.
+ *
+ * <li>{@link #URL_ALL_WITH_SUB_ID_AND_PHONE_NUMBER}: To provide more efficient query way,
+ * filter by the {@link ContactColumns#PHONE_NUMBER} first and join with others tables. The
+ * format is like content://eab/all/[sub_id]/[phone_number]
+ *
+ * <li> {@link #URL_ALL}: Join all of tables at once
+ * </ul>
+ */
+ @Override
+ public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+ String sortOrder) {
+ SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
+ SQLiteDatabase db = getReadableDatabase();
+ int match = URI_MATCHER.match(uri);
+ int subId;
+ String subIdString;
+
+ Log.d(TAG, "Query URI: " + match);
+
+ switch (match) {
+ case URL_CONTACT:
+ qb.setTables(EAB_CONTACT_TABLE_NAME);
+ break;
+
+ case URL_COMMON:
+ qb.setTables(EAB_COMMON_TABLE_NAME);
+ break;
+
+ case URL_PRESENCE:
+ qb.setTables(EAB_PRESENCE_TUPLE_TABLE_NAME);
+ break;
+
+ case URL_OPTIONS:
+ qb.setTables(EAB_OPTIONS_TABLE_NAME);
+ break;
+
+ case URL_ALL_WITH_SUB_ID_AND_PHONE_NUMBER:
+ List<String> pathSegment = uri.getPathSegments();
+
+ subIdString = pathSegment.get(1);
+ try {
+ subId = Integer.parseInt(subIdString);
+ } catch (NumberFormatException e) {
+ Log.e(TAG, "NumberFormatException" + e);
+ return null;
+ }
+ qb.appendWhereStandalone(EabCommonColumns.SUBSCRIPTION_ID + "=" + subId);
+
+ String phoneNumber = pathSegment.get(2);
+ String whereClause;
+ if (TextUtils.isEmpty(phoneNumber)) {
+ Log.e(TAG, "phone number is null");
+ return null;
+ }
+ whereClause = " where " + ContactColumns.PHONE_NUMBER + "='" + phoneNumber + "' ";
+ qb.setTables(
+ "((" + QUERY_CONTACT_TABLE + whereClause + ") AS " + EAB_CONTACT_TABLE_NAME
+ + JOIN_ALL_TABLES + ")");
+ break;
+
+ case URL_ALL:
+ qb.setTables("(" + QUERY_CONTACT_TABLE + JOIN_ALL_TABLES + ")");
+ break;
+
+ default:
+ Log.d(TAG, "Query failed. Not support URL.");
+ return null;
+ }
+ return qb.query(db, projection, selection, selectionArgs, null, null, sortOrder);
+ }
+
+ @Override
+ public Uri insert(Uri uri, ContentValues contentValues) {
+ SQLiteDatabase db = getWritableDatabase();
+ int match = URI_MATCHER.match(uri);
+ long result = 0;
+ String tableName = "";
+ switch (match) {
+ case URL_CONTACT:
+ tableName = EAB_CONTACT_TABLE_NAME;
+ break;
+ case URL_COMMON:
+ tableName = EAB_COMMON_TABLE_NAME;
+ break;
+ case URL_PRESENCE:
+ tableName = EAB_PRESENCE_TUPLE_TABLE_NAME;
+ break;
+ case URL_OPTIONS:
+ tableName = EAB_OPTIONS_TABLE_NAME;
+ break;
+ }
+ if (!TextUtils.isEmpty(tableName)) {
+ result = db.insertWithOnConflict(tableName, null, contentValues,
+ SQLiteDatabase.CONFLICT_REPLACE);
+ Log.d(TAG, "Insert uri: " + match + " ID: " + result);
+ if (result > 0) {
+ getContext().getContentResolver().notifyChange(uri, null, NOTIFY_INSERT);
+ }
+ } else {
+ Log.d(TAG, "Insert. Not support URI.");
+ }
+
+ return Uri.withAppendedPath(uri, String.valueOf(result));
+ }
+
+ @Override
+ public int bulkInsert(Uri uri, ContentValues[] values) {
+ SQLiteDatabase db = getWritableDatabase();
+ int match = URI_MATCHER.match(uri);
+ int result = 0;
+ String tableName = "";
+ switch (match) {
+ case URL_CONTACT:
+ tableName = EAB_CONTACT_TABLE_NAME;
+ break;
+ case URL_COMMON:
+ tableName = EAB_COMMON_TABLE_NAME;
+ break;
+ case URL_PRESENCE:
+ tableName = EAB_PRESENCE_TUPLE_TABLE_NAME;
+ break;
+ case URL_OPTIONS:
+ tableName = EAB_OPTIONS_TABLE_NAME;
+ break;
+ }
+
+ if (TextUtils.isEmpty(tableName)) {
+ Log.d(TAG, "bulkInsert. Not support URI.");
+ return 0;
+ }
+
+ try {
+ // Batch all insertions in a single transaction to improve efficiency.
+ db.beginTransaction();
+ for (ContentValues contentValue : values) {
+ if (contentValue != null) {
+ db.insertWithOnConflict(tableName, null, contentValue,
+ SQLiteDatabase.CONFLICT_REPLACE);
+ result++;
+ }
+ }
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+ if (result > 0) {
+ getContext().getContentResolver().notifyChange(uri, null, NOTIFY_INSERT);
+ }
+ Log.d(TAG, "bulkInsert uri: " + match + " count: " + result);
+ return result;
+ }
+
+ @Override
+ public int delete(Uri uri, String selection, String[] selectionArgs) {
+ SQLiteDatabase db = getWritableDatabase();
+ int match = URI_MATCHER.match(uri);
+ int result = 0;
+ String tableName = "";
+ switch (match) {
+ case URL_CONTACT:
+ tableName = EAB_CONTACT_TABLE_NAME;
+ break;
+ case URL_COMMON:
+ tableName = EAB_COMMON_TABLE_NAME;
+ break;
+ case URL_PRESENCE:
+ tableName = EAB_PRESENCE_TUPLE_TABLE_NAME;
+ break;
+ case URL_OPTIONS:
+ tableName = EAB_OPTIONS_TABLE_NAME;
+ break;
+ }
+ if (!TextUtils.isEmpty(tableName)) {
+ result = db.delete(tableName, selection, selectionArgs);
+ if (result > 0) {
+ getContext().getContentResolver().notifyChange(uri, null, NOTIFY_DELETE);
+ }
+ Log.d(TAG, "Delete uri: " + match + " result: " + result);
+ } else {
+ Log.d(TAG, "Delete. Not support URI.");
+ }
+ return result;
+ }
+
+ @Override
+ public int update(Uri uri, ContentValues contentValues, String selection,
+ String[] selectionArgs) {
+ SQLiteDatabase db = getWritableDatabase();
+ int match = URI_MATCHER.match(uri);
+ int result = 0;
+ String tableName = "";
+ switch (match) {
+ case URL_CONTACT:
+ tableName = EAB_CONTACT_TABLE_NAME;
+ break;
+ case URL_COMMON:
+ tableName = EAB_COMMON_TABLE_NAME;
+ break;
+ case URL_PRESENCE:
+ tableName = EAB_PRESENCE_TUPLE_TABLE_NAME;
+ break;
+ case URL_OPTIONS:
+ tableName = EAB_OPTIONS_TABLE_NAME;
+ break;
+ }
+ if (!TextUtils.isEmpty(tableName)) {
+ result = db.updateWithOnConflict(tableName, contentValues,
+ selection, selectionArgs, SQLiteDatabase.CONFLICT_REPLACE);
+ if (result > 0) {
+ getContext().getContentResolver().notifyChange(uri, null, NOTIFY_UPDATE);
+ }
+ Log.d(TAG, "Update uri: " + match + " result: " + result);
+ } else {
+ Log.d(TAG, "Update. Not support URI.");
+ }
+ return result;
+ }
+
+ @Override
+ public String getType(Uri uri) {
+ return null;
+ }
+
+ @VisibleForTesting
+ public SQLiteDatabase getWritableDatabase() {
+ return mOpenHelper.getWritableDatabase();
+ }
+
+ @VisibleForTesting
+ public SQLiteDatabase getReadableDatabase() {
+ return mOpenHelper.getReadableDatabase();
+ }
+}
diff --git a/src/java/com/android/ims/rcs/uce/eab/EabUtil.java b/src/java/com/android/ims/rcs/uce/eab/EabUtil.java
new file mode 100644
index 00000000..de738067
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/eab/EabUtil.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2021 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.ims.rcs.uce.eab;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.ims.rcs.uce.eab.EabProvider.ContactColumns;
+import com.android.ims.rcs.uce.eab.EabProvider.EabCommonColumns;
+import com.android.ims.rcs.uce.eab.EabProvider.OptionsColumns;
+import com.android.ims.rcs.uce.eab.EabProvider.PresenceTupleColumns;
+import com.android.ims.rcs.uce.util.UceUtils;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * The util to modify the EAB database.
+ */
+public class EabUtil {
+
+ private static final String LOG_TAG = UceUtils.getLogPrefix() + "EabUtil";
+
+ /**
+ * Get the given EAB contacts from the EAB database.
+ *
+ * Output format:
+ * [PHONE_NUMBER], [RAW_CONTACT_ID], [CONTACT_ID], [DATA_ID]
+ */
+ public static String getContactFromEab(Context context, String contact) {
+ StringBuilder result = new StringBuilder();
+ try (Cursor cursor = context.getContentResolver().query(
+ EabProvider.CONTACT_URI,
+ new String[]{ContactColumns.PHONE_NUMBER,
+ ContactColumns.RAW_CONTACT_ID,
+ ContactColumns.CONTACT_ID,
+ ContactColumns.DATA_ID},
+ ContactColumns.PHONE_NUMBER + "=?",
+ new String[]{contact}, null)) {
+ if (cursor != null && cursor.moveToFirst()) {
+ result.append(cursor.getString(cursor.getColumnIndex(
+ ContactColumns.PHONE_NUMBER)));
+ result.append(",");
+ result.append(cursor.getString(cursor.getColumnIndex(
+ ContactColumns.RAW_CONTACT_ID)));
+ result.append(",");
+ result.append(cursor.getString(cursor.getColumnIndex(
+ ContactColumns.CONTACT_ID)));
+ result.append(",");
+ result.append(cursor.getString(cursor.getColumnIndex(
+ ContactColumns.DATA_ID)));
+ }
+ } catch (Exception e) {
+ Log.w(LOG_TAG, "getEabContactId exception " + e);
+ }
+ Log.d(LOG_TAG, "getContactFromEab() result: " + result);
+ return result.toString();
+ }
+
+ /**
+ * Remove the given EAB contacts from the EAB database.
+ */
+ public static int removeContactFromEab(int subId, String contacts, Context context) {
+ if (TextUtils.isEmpty(contacts)) {
+ return -1;
+ }
+ List<String> contactList = Arrays.stream(contacts.split(",")).collect(Collectors.toList());
+ if (contactList == null || contactList.isEmpty()) {
+ return -1;
+ }
+ int count = 0;
+ for (String contact : contactList) {
+ int contactId = getEabContactId(contact, context);
+ if (contactId == -1) {
+ continue;
+ }
+ int commonId = getEabCommonId(contactId, context);
+ count += removeContactCapabilities(contactId, commonId, context);
+ }
+ return count;
+ }
+
+ private static int getEabContactId(String contactNumber, Context context) {
+ int contactId = -1;
+ Cursor cursor = null;
+ String formattedNumber = EabControllerImpl.formatNumber(context, contactNumber);
+ try {
+ cursor = context.getContentResolver().query(
+ EabProvider.CONTACT_URI,
+ new String[] { EabProvider.EabCommonColumns._ID },
+ EabProvider.ContactColumns.PHONE_NUMBER + "=?",
+ new String[] { formattedNumber }, null);
+ if (cursor != null && cursor.moveToFirst()) {
+ contactId = cursor.getInt(cursor.getColumnIndex(EabProvider.ContactColumns._ID));
+ }
+ } catch (Exception e) {
+ Log.w(LOG_TAG, "getEabContactId exception " + e);
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ return contactId;
+ }
+
+ private static int getEabCommonId(int contactId, Context context) {
+ int commonId = -1;
+ Cursor cursor = null;
+ try {
+ cursor = context.getContentResolver().query(
+ EabProvider.COMMON_URI,
+ new String[] { EabProvider.EabCommonColumns._ID },
+ EabProvider.EabCommonColumns.EAB_CONTACT_ID + "=?",
+ new String[] { String.valueOf(contactId) }, null);
+ if (cursor != null && cursor.moveToFirst()) {
+ commonId = cursor.getInt(cursor.getColumnIndex(EabProvider.EabCommonColumns._ID));
+ }
+ } catch (Exception e) {
+ Log.w(LOG_TAG, "getEabCommonId exception " + e);
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ return commonId;
+ }
+
+ private static int removeContactCapabilities(int contactId, int commonId, Context context) {
+ int count = 0;
+ count = context.getContentResolver().delete(EabProvider.PRESENCE_URI,
+ PresenceTupleColumns.EAB_COMMON_ID + "=?", new String[]{String.valueOf(commonId)});
+ context.getContentResolver().delete(EabProvider.OPTIONS_URI,
+ OptionsColumns.EAB_COMMON_ID + "=?", new String[]{String.valueOf(commonId)});
+ context.getContentResolver().delete(EabProvider.COMMON_URI,
+ EabCommonColumns.EAB_CONTACT_ID + "=?", new String[]{String.valueOf(contactId)});
+ context.getContentResolver().delete(EabProvider.CONTACT_URI,
+ ContactColumns._ID + "=?", new String[]{String.valueOf(contactId)});
+ return count;
+ }
+}
diff --git a/src/java/com/android/ims/rcs/uce/eab/RcsUceCapabilityBuilderWrapper.java b/src/java/com/android/ims/rcs/uce/eab/RcsUceCapabilityBuilderWrapper.java
new file mode 100644
index 00000000..8e42b613
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/eab/RcsUceCapabilityBuilderWrapper.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2021 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.ims.rcs.uce.eab;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.telephony.ims.RcsContactUceCapability.OptionsBuilder;
+import android.telephony.ims.RcsContactUceCapability.PresenceBuilder;
+
+/**
+ * The wrapper class of the PresenceBuilder and the OptionsBuilder.
+ */
+public class RcsUceCapabilityBuilderWrapper {
+ private final int mMechanism;
+ private PresenceBuilder mPresenceBuilder;
+ private OptionsBuilder mOptionsBuilder;
+
+ public RcsUceCapabilityBuilderWrapper(int mechanism) {
+ mMechanism = mechanism;
+ }
+
+ public int getMechanism() {
+ return mMechanism;
+ }
+
+ public void setPresenceBuilder(@NonNull PresenceBuilder presenceBuilder) {
+ mPresenceBuilder = presenceBuilder;
+ }
+
+ public @Nullable PresenceBuilder getPresenceBuilder() {
+ return mPresenceBuilder;
+ }
+
+ public void setOptionsBuilder(@NonNull OptionsBuilder optionsBuilder) {
+ mOptionsBuilder = optionsBuilder;
+ }
+
+ public @Nullable OptionsBuilder getOptionsBuilder() {
+ return mOptionsBuilder;
+ }
+}
diff --git a/src/java/com/android/ims/rcs/uce/options/OptionsController.java b/src/java/com/android/ims/rcs/uce/options/OptionsController.java
new file mode 100644
index 00000000..b4b3260d
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/options/OptionsController.java
@@ -0,0 +1,40 @@
+/*
+ * 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.ims.rcs.uce.options;
+
+import android.annotation.NonNull;
+import android.net.Uri;
+import android.os.RemoteException;
+import android.telephony.ims.aidl.IOptionsResponseCallback;
+
+import com.android.ims.rcs.uce.ControllerBase;
+
+import java.util.Set;
+
+/**
+ * The interface to define the operations of the SIP OPTIONS
+ */
+public interface OptionsController extends ControllerBase {
+ /**
+ * Request the contact's capabilities of the given contact.
+ * @param contactUri The contact of the capabilities is being requested for.
+ * @param deviceFeatureTags The feature tags of the device's capabilities.
+ * @param c The response callback of the OPTIONS capabilities request.
+ */
+ void sendCapabilitiesRequest(@NonNull Uri contactUri, @NonNull Set<String> deviceFeatureTags,
+ @NonNull IOptionsResponseCallback c) throws RemoteException;
+}
diff --git a/src/java/com/android/ims/rcs/uce/options/OptionsControllerImpl.java b/src/java/com/android/ims/rcs/uce/options/OptionsControllerImpl.java
new file mode 100644
index 00000000..e3b708f7
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/options/OptionsControllerImpl.java
@@ -0,0 +1,88 @@
+/*
+ * 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.ims.rcs.uce.options;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.net.Uri;
+import android.os.RemoteException;
+import android.telephony.ims.aidl.IOptionsResponseCallback;
+import android.telephony.ims.stub.RcsCapabilityExchangeImplBase;
+import android.util.Log;
+
+import com.android.ims.RcsFeatureManager;
+import com.android.ims.rcs.uce.util.UceUtils;
+
+import java.util.ArrayList;
+import java.util.Set;
+
+/**
+ * The implementation of OptionsController.
+ */
+public class OptionsControllerImpl implements OptionsController {
+
+ private static final String LOG_TAG = UceUtils.getLogPrefix() + "OptionsController";
+
+ private final int mSubId;
+ private final Context mContext;
+ private volatile boolean mIsDestroyedFlag;
+ private volatile RcsFeatureManager mRcsFeatureManager;
+
+ public OptionsControllerImpl(Context context, int subId) {
+ mSubId = subId;
+ mContext = context;
+ }
+
+ @Override
+ public void onRcsConnected(RcsFeatureManager manager) {
+ mRcsFeatureManager = manager;
+ }
+
+ @Override
+ public void onRcsDisconnected() {
+ mRcsFeatureManager = null;
+ }
+
+ @Override
+ public void onDestroy() {
+ mIsDestroyedFlag = true;
+ mRcsFeatureManager = null;
+ }
+
+ @Override
+ public void onCarrierConfigChanged() {
+ // Nothing required here.
+ }
+
+ public void sendCapabilitiesRequest(Uri contactUri, @NonNull Set<String> deviceFeatureTags,
+ IOptionsResponseCallback c) throws RemoteException {
+
+ if (mIsDestroyedFlag) {
+ throw new RemoteException("OPTIONS controller is destroyed");
+ }
+
+ RcsFeatureManager featureManager = mRcsFeatureManager;
+ if (featureManager == null) {
+ Log.w(LOG_TAG, "sendCapabilitiesRequest: Service is unavailable");
+ c.onCommandError(RcsCapabilityExchangeImplBase.COMMAND_CODE_SERVICE_UNAVAILABLE);
+ return;
+ }
+
+ featureManager.sendOptionsCapabilityRequest(contactUri, new ArrayList<>(deviceFeatureTags),
+ c);
+ }
+}
diff --git a/src/java/com/android/ims/rcs/uce/presence/pidfparser/ElementBase.java b/src/java/com/android/ims/rcs/uce/presence/pidfparser/ElementBase.java
new file mode 100644
index 00000000..6a03f69d
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/presence/pidfparser/ElementBase.java
@@ -0,0 +1,84 @@
+/*
+ * 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.ims.rcs.uce.presence.pidfparser;
+
+import android.util.Log;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.IOException;
+
+/**
+ * The base class of the pidf element.
+ */
+public abstract class ElementBase {
+ private String mNamespace;
+ private String mElementName;
+
+ public ElementBase() {
+ mNamespace = initNamespace();
+ mElementName = initElementName();
+ }
+
+ protected abstract String initNamespace();
+ protected abstract String initElementName();
+
+ /**
+ * @return The namespace of this element
+ */
+ public String getNamespace() {
+ return mNamespace;
+ }
+
+ /**
+ * @return The name of this element.
+ */
+ public String getElementName() {
+ return mElementName;
+ }
+
+ public abstract void serialize(XmlSerializer serializer) throws IOException;
+
+ public abstract void parse(XmlPullParser parser) throws IOException, XmlPullParserException;
+
+ protected boolean verifyParsingElement(String namespace, String elementName) {
+ if (!getNamespace().equals(namespace) || !getElementName().equals(elementName)) {
+ return false;
+ } else {
+ return true;
+ }
+ }
+
+ // Move to the end tag of this element
+ protected void moveToElementEndTag(XmlPullParser parser, int type)
+ throws IOException, XmlPullParserException {
+ int eventType = type;
+
+ // Move to the end tag of this element.
+ while(!(eventType == XmlPullParser.END_TAG
+ && getNamespace().equals(parser.getNamespace())
+ && getElementName().equals(parser.getName()))) {
+ eventType = parser.next();
+
+ // Leave directly if the event type is the end of the document.
+ if (eventType == XmlPullParser.END_DOCUMENT) {
+ return;
+ }
+ }
+ }
+}
diff --git a/src/java/com/android/ims/rcs/uce/presence/pidfparser/PidfParser.java b/src/java/com/android/ims/rcs/uce/presence/pidfparser/PidfParser.java
new file mode 100644
index 00000000..2660f1d9
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/presence/pidfparser/PidfParser.java
@@ -0,0 +1,274 @@
+/*
+ * 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.ims.rcs.uce.presence.pidfparser;
+
+import android.annotation.Nullable;
+import android.net.Uri;
+import android.telephony.ims.RcsContactPresenceTuple;
+import android.telephony.ims.RcsContactPresenceTuple.ServiceCapabilities;
+import android.telephony.ims.RcsContactUceCapability;
+import android.telephony.ims.RcsContactUceCapability.PresenceBuilder;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.ims.rcs.uce.presence.pidfparser.capabilities.Audio;
+import com.android.ims.rcs.uce.presence.pidfparser.capabilities.CapsConstant;
+import com.android.ims.rcs.uce.presence.pidfparser.capabilities.Duplex;
+import com.android.ims.rcs.uce.presence.pidfparser.capabilities.ServiceCaps;
+import com.android.ims.rcs.uce.presence.pidfparser.capabilities.Video;
+import com.android.ims.rcs.uce.presence.pidfparser.omapres.OmaPresConstant;
+import com.android.ims.rcs.uce.presence.pidfparser.pidf.Basic;
+import com.android.ims.rcs.uce.presence.pidfparser.pidf.PidfConstant;
+import com.android.ims.rcs.uce.presence.pidfparser.pidf.Presence;
+import com.android.ims.rcs.uce.presence.pidfparser.pidf.Tuple;
+import com.android.ims.rcs.uce.util.UceUtils;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.time.Instant;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+import org.xmlpull.v1.XmlSerializer;
+
+/**
+ * Convert between the class RcsContactUceCapability and the pidf format.
+ */
+public class PidfParser {
+
+ private static final String LOG_TAG = UceUtils.getLogPrefix() + "PidfParser";
+
+ private static final Pattern PIDF_PATTERN = Pattern.compile("\t|\r|\n");
+
+ /**
+ * Convert the RcsContactUceCapability to the string of pidf.
+ */
+ public static String convertToPidf(RcsContactUceCapability capabilities) {
+ StringWriter pidfWriter = new StringWriter();
+ try {
+ // Init the instance of the XmlSerializer.
+ XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
+ XmlSerializer serializer = factory.newSerializer();
+
+ // setup output and namespace
+ serializer.setOutput(pidfWriter);
+ serializer.setPrefix("", PidfConstant.NAMESPACE);
+ serializer.setPrefix("op", OmaPresConstant.NAMESPACE);
+ serializer.setPrefix("caps", CapsConstant.NAMESPACE);
+
+ // Get the Presence element
+ Presence presence = PidfParserUtils.getPresence(capabilities);
+
+ // Start serializing.
+ serializer.startDocument(PidfParserConstant.ENCODING_UTF_8, true);
+ presence.serialize(serializer);
+ serializer.endDocument();
+ serializer.flush();
+
+ } catch (XmlPullParserException parserEx) {
+ parserEx.printStackTrace();
+ return null;
+ } catch (IOException ioException) {
+ ioException.printStackTrace();
+ return null;
+ }
+ return pidfWriter.toString();
+ }
+
+ /**
+ * Get the RcsContactUceCapability from the given PIDF xml format.
+ */
+ public static @Nullable RcsContactUceCapability getRcsContactUceCapability(String pidf) {
+ if (TextUtils.isEmpty(pidf)) {
+ Log.w(LOG_TAG, "getRcsContactUceCapability: The given pidf is empty");
+ return null;
+ }
+
+ // Filter the newline characters
+ Matcher matcher = PIDF_PATTERN.matcher(pidf);
+ String formattedPidf = matcher.replaceAll("");
+ if (TextUtils.isEmpty(formattedPidf)) {
+ Log.w(LOG_TAG, "getRcsContactUceCapability: The formatted pidf is empty");
+ return null;
+ }
+
+ Reader reader = null;
+ try {
+ // Init the instance of the parser
+ XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser();
+ parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
+ reader = new StringReader(formattedPidf);
+ parser.setInput(reader);
+
+ // Start parsing
+ Presence presence = parsePidf(parser);
+
+ // Convert from the Presence to the RcsContactUceCapability
+ return convertToRcsContactUceCapability(presence);
+
+ } catch (XmlPullParserException | IOException e) {
+ e.printStackTrace();
+ } finally {
+ if (reader != null) {
+ try {
+ reader.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ return null;
+ }
+
+ private static Presence parsePidf(XmlPullParser parser) throws IOException,
+ XmlPullParserException {
+ Presence presence = null;
+ int nextType = parser.next();
+ do {
+ // Find the Presence start tag
+ if (nextType == XmlPullParser.START_TAG
+ && Presence.ELEMENT_NAME.equals(parser.getName())) {
+ presence = new Presence();
+ presence.parse(parser);
+ break;
+ }
+ nextType = parser.next();
+ } while(nextType != XmlPullParser.END_DOCUMENT);
+
+ return presence;
+ }
+
+ /*
+ * Convert the given Presence to the RcsContactUceCapability
+ */
+ private static RcsContactUceCapability convertToRcsContactUceCapability(Presence presence) {
+ if (presence == null) {
+ Log.w(LOG_TAG, "convertToRcsContactUceCapability: The presence is null");
+ return null;
+ }
+ if (TextUtils.isEmpty(presence.getEntity())) {
+ Log.w(LOG_TAG, "convertToRcsContactUceCapability: The entity is empty");
+ return null;
+ }
+
+ PresenceBuilder presenceBuilder = new PresenceBuilder(Uri.parse(presence.getEntity()),
+ RcsContactUceCapability.SOURCE_TYPE_NETWORK,
+ RcsContactUceCapability.REQUEST_RESULT_FOUND);
+
+ // Add all the capability tuples of this contact
+ presence.getTupleList().forEach(tuple -> {
+ RcsContactPresenceTuple capabilityTuple = getRcsContactPresenceTuple(tuple);
+ if (capabilityTuple != null) {
+ presenceBuilder.addCapabilityTuple(capabilityTuple);
+ }
+ });
+
+ return presenceBuilder.build();
+ }
+
+ /*
+ * Get the RcsContactPresenceTuple from the giving tuple element.
+ */
+ private static RcsContactPresenceTuple getRcsContactPresenceTuple(Tuple tuple) {
+ if (tuple == null) {
+ return null;
+ }
+
+ String status = RcsContactPresenceTuple.TUPLE_BASIC_STATUS_CLOSED;
+ if (Basic.OPEN.equals(PidfParserUtils.getTupleStatus(tuple))) {
+ status = RcsContactPresenceTuple.TUPLE_BASIC_STATUS_OPEN;
+ }
+
+ String serviceId = PidfParserUtils.getTupleServiceId(tuple);
+ String serviceVersion = PidfParserUtils.getTupleServiceVersion(tuple);
+ String serviceDescription = PidfParserUtils.getTupleServiceDescription(tuple);
+
+ RcsContactPresenceTuple.Builder builder = new RcsContactPresenceTuple.Builder(status,
+ serviceId, serviceVersion);
+
+ // Set contact uri
+ String contact = PidfParserUtils.getTupleContact(tuple);
+ if (!TextUtils.isEmpty(contact)) {
+ builder.setContactUri(Uri.parse(contact));
+ }
+
+ // Timestamp
+ String timestamp = PidfParserUtils.getTupleTimestamp(tuple);
+ if (!TextUtils.isEmpty(timestamp)) {
+ try {
+ Instant instant = DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(
+ timestamp, Instant::from);
+ builder.setTime(instant);
+ } catch (DateTimeParseException e) {
+ Log.w(LOG_TAG, "getRcsContactPresenceTuple: Parse timestamp failed " + e);
+ }
+ }
+
+ // Set service description
+ if (!TextUtils.isEmpty(serviceDescription)) {
+ builder.setServiceDescription(serviceDescription);
+ }
+
+ // Set service capabilities
+ ServiceCaps serviceCaps = tuple.getServiceCaps();
+ if (serviceCaps != null) {
+ List<ElementBase> serviceCapsList = serviceCaps.getElements();
+ if (serviceCapsList != null && !serviceCapsList.isEmpty()) {
+ boolean isAudioSupported = false;
+ boolean isVideoSupported = false;
+ List<String> supportedTypes = null;
+ List<String> notSupportedTypes = null;
+
+ for (ElementBase element : serviceCapsList) {
+ if (element instanceof Audio) {
+ isAudioSupported = ((Audio) element).isAudioSupported();
+ } else if (element instanceof Video) {
+ isVideoSupported = ((Video) element).isVideoSupported();
+ } else if (element instanceof Duplex) {
+ supportedTypes = ((Duplex) element).getSupportedTypes();
+ notSupportedTypes = ((Duplex) element).getNotSupportedTypes();
+ }
+ }
+
+ ServiceCapabilities.Builder capabilitiesBuilder
+ = new ServiceCapabilities.Builder(isAudioSupported, isVideoSupported);
+
+ if (supportedTypes != null && !supportedTypes.isEmpty()) {
+ for (String supportedType : supportedTypes) {
+ capabilitiesBuilder.addSupportedDuplexMode(supportedType);
+ }
+ }
+
+ if (notSupportedTypes != null && !notSupportedTypes.isEmpty()) {
+ for (String notSupportedType : notSupportedTypes) {
+ capabilitiesBuilder.addUnsupportedDuplexMode(notSupportedType);
+ }
+ }
+ builder.setServiceCapabilities(capabilitiesBuilder.build());
+ }
+ }
+ return builder.build();
+ }
+}
diff --git a/src/java/com/android/ims/rcs/uce/presence/pidfparser/PidfParserConstant.java b/src/java/com/android/ims/rcs/uce/presence/pidfparser/PidfParserConstant.java
new file mode 100644
index 00000000..07fde38d
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/presence/pidfparser/PidfParserConstant.java
@@ -0,0 +1,39 @@
+/*
+ * 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.ims.rcs.uce.presence.pidfparser;
+
+/**
+ * The constant of the pidf
+ */
+public class PidfParserConstant {
+ /**
+ * The UTF-8 encoding format
+ */
+ public static final String ENCODING_UTF_8 = "utf-8";
+
+ /**
+ * The service id of the capabilities discovery via presence.
+ */
+ public static final String SERVICE_ID_CAPS_DISCOVERY =
+ "org.3gpp.urn:urn-7:3gpp-application.ims.iari.rcse.dp";
+
+ /**
+ * The service id of the VoLTE voice and video call.
+ */
+ public static final String SERVICE_ID_IpCall =
+ "org.3gpp.urn:urn-7:3gpp-service.ims.icsi.mmtel";
+}
diff --git a/src/java/com/android/ims/rcs/uce/presence/pidfparser/PidfParserUtils.java b/src/java/com/android/ims/rcs/uce/presence/pidfparser/PidfParserUtils.java
new file mode 100644
index 00000000..f2b21bd0
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/presence/pidfparser/PidfParserUtils.java
@@ -0,0 +1,312 @@
+/*
+ * 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.ims.rcs.uce.presence.pidfparser;
+
+import android.net.Uri;
+import android.telephony.ims.RcsContactPresenceTuple;
+import android.telephony.ims.RcsContactPresenceTuple.BasicStatus;
+import android.telephony.ims.RcsContactPresenceTuple.ServiceCapabilities;
+import android.telephony.ims.RcsContactUceCapability;
+import android.text.TextUtils;
+
+import com.android.ims.rcs.uce.presence.pidfparser.capabilities.Audio;
+import com.android.ims.rcs.uce.presence.pidfparser.capabilities.Duplex;
+import com.android.ims.rcs.uce.presence.pidfparser.capabilities.ServiceCaps;
+import com.android.ims.rcs.uce.presence.pidfparser.capabilities.Video;
+import com.android.ims.rcs.uce.presence.pidfparser.omapres.Description;
+import com.android.ims.rcs.uce.presence.pidfparser.omapres.ServiceDescription;
+import com.android.ims.rcs.uce.presence.pidfparser.omapres.ServiceId;
+import com.android.ims.rcs.uce.presence.pidfparser.omapres.Version;
+import com.android.ims.rcs.uce.presence.pidfparser.pidf.Basic;
+import com.android.ims.rcs.uce.presence.pidfparser.pidf.Contact;
+import com.android.ims.rcs.uce.presence.pidfparser.pidf.Presence;
+import com.android.ims.rcs.uce.presence.pidfparser.pidf.Status;
+import com.android.ims.rcs.uce.presence.pidfparser.pidf.Timestamp;
+import com.android.ims.rcs.uce.presence.pidfparser.pidf.Tuple;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * The utils to help the PIDF parsing process.
+ */
+public class PidfParserUtils {
+
+ /*
+ * The resource terminated reason with NOT FOUND
+ */
+ private static String[] REQUEST_RESULT_REASON_NOT_FOUND = { "noresource", "rejected" };
+
+ /**
+ * Convert the given class RcsContactUceCapability to the class Presence.
+ */
+ static Presence getPresence(RcsContactUceCapability capabilities) {
+ // Create "presence" element which is the root element of the pidf
+ Presence presence = new Presence(capabilities.getContactUri());
+
+ List<RcsContactPresenceTuple> tupleList = capabilities.getCapabilityTuples();
+ if (tupleList == null || tupleList.isEmpty()) {
+ return presence;
+ }
+
+ for (RcsContactPresenceTuple presenceTuple : tupleList) {
+ Tuple tupleElement = getTupleElement(presenceTuple);
+ if (tupleElement != null) {
+ presence.addTuple(tupleElement);
+ }
+ }
+
+ return presence;
+ }
+
+ /**
+ * Convert the class from RcsContactPresenceTuple to the class Tuple
+ */
+ private static Tuple getTupleElement(RcsContactPresenceTuple presenceTuple) {
+ if (presenceTuple == null) {
+ return null;
+ }
+ Tuple tupleElement = new Tuple();
+
+ // status element
+ handleTupleStatusElement(tupleElement, presenceTuple.getStatus());
+
+ // service description element
+ handleTupleServiceDescriptionElement(tupleElement, presenceTuple.getServiceId(),
+ presenceTuple.getServiceVersion(), presenceTuple.getServiceDescription());
+
+ // service capabilities element
+ handleServiceCapsElement(tupleElement, presenceTuple.getServiceCapabilities());
+
+ // contact element
+ handleTupleContactElement(tupleElement, presenceTuple.getContactUri());
+
+ return tupleElement;
+ }
+
+ private static void handleTupleContactElement(Tuple tupleElement, Uri uri) {
+ if (uri == null) {
+ return;
+ }
+ Contact contactElement = new Contact();
+ contactElement.setContact(uri.toString());
+ tupleElement.setContact(contactElement);
+ }
+
+ private static void handleTupleStatusElement(Tuple tupleElement, @BasicStatus String status) {
+ if (TextUtils.isEmpty(status)) {
+ return;
+ }
+ Basic basicElement = new Basic(status);
+ Status statusElement = new Status();
+ statusElement.setBasic(basicElement);
+ tupleElement.setStatus(statusElement);
+ }
+
+ private static void handleTupleServiceDescriptionElement(Tuple tupleElement, String serviceId,
+ String version, String description) {
+ ServiceId serviceIdElement = null;
+ Version versionElement = null;
+ Description descriptionElement = null;
+
+ // init serviceId element
+ if (!TextUtils.isEmpty(serviceId)) {
+ serviceIdElement = new ServiceId(serviceId);
+ }
+
+ // init version element
+ if (!TextUtils.isEmpty(version)) {
+ String[] versionAry = version.split("\\.");
+ if (versionAry != null && versionAry.length == 2) {
+ int majorVersion = Integer.parseInt(versionAry[0]);
+ int minorVersion = Integer.parseInt(versionAry[1]);
+ versionElement = new Version(majorVersion, minorVersion);
+ }
+ }
+
+ // init description element
+ if (!TextUtils.isEmpty(description)) {
+ descriptionElement = new Description(description);
+ }
+
+ // Add the Service Description element into the tuple
+ if (serviceIdElement != null && versionElement != null) {
+ ServiceDescription serviceDescription = new ServiceDescription();
+ serviceDescription.setServiceId(serviceIdElement);
+ serviceDescription.setVersion(versionElement);
+ if (descriptionElement != null) {
+ serviceDescription.setDescription(descriptionElement);
+ }
+ tupleElement.setServiceDescription(serviceDescription);
+ }
+ }
+
+ private static void handleServiceCapsElement(Tuple tupleElement,
+ ServiceCapabilities serviceCaps) {
+ if (serviceCaps == null) {
+ return;
+ }
+
+ ServiceCaps servCapsElement = new ServiceCaps();
+
+ // Audio and Video element
+ Audio audioElement = new Audio(serviceCaps.isAudioCapable());
+ Video videoElement = new Video(serviceCaps.isVideoCapable());
+ servCapsElement.addElement(audioElement);
+ servCapsElement.addElement(videoElement);
+
+ // Duplex element
+ List<String> supportedDuplexModes = serviceCaps.getSupportedDuplexModes();
+ List<String> UnsupportedDuplexModes = serviceCaps.getUnsupportedDuplexModes();
+ if ((supportedDuplexModes != null && !supportedDuplexModes.isEmpty()) ||
+ (UnsupportedDuplexModes != null && !UnsupportedDuplexModes.isEmpty())) {
+ Duplex duplex = new Duplex();
+ if (!supportedDuplexModes.isEmpty()) {
+ duplex.addSupportedType(supportedDuplexModes.get(0));
+ }
+ if (!UnsupportedDuplexModes.isEmpty()) {
+ duplex.addNotSupportedType(UnsupportedDuplexModes.get(0));
+ }
+ servCapsElement.addElement(duplex);
+ }
+
+ tupleElement.setServiceCaps(servCapsElement);
+ }
+
+ /**
+ * Get the status from the given tuple.
+ */
+ public static String getTupleStatus(Tuple tuple) {
+ if (tuple == null) {
+ return null;
+ }
+ Status status = tuple.getStatus();
+ if (status != null) {
+ Basic basic = status.getBasic();
+ if (basic != null) {
+ return basic.getValue();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Get the service Id from the given tuple.
+ */
+ public static String getTupleServiceId(Tuple tuple) {
+ if (tuple == null) {
+ return null;
+ }
+ ServiceDescription servDescription = tuple.getServiceDescription();
+ if (servDescription != null) {
+ ServiceId serviceId = servDescription.getServiceId();
+ if (serviceId != null) {
+ return serviceId.getValue();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Get the service version from the given tuple.
+ */
+ public static String getTupleServiceVersion(Tuple tuple) {
+ if (tuple == null) {
+ return null;
+ }
+ ServiceDescription servDescription = tuple.getServiceDescription();
+ if (servDescription != null) {
+ Version version = servDescription.getVersion();
+ if (version != null) {
+ return version.getValue();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Get the service description from the given tuple.
+ */
+ public static String getTupleServiceDescription(Tuple tuple) {
+ if (tuple == null) {
+ return null;
+ }
+ ServiceDescription servDescription = tuple.getServiceDescription();
+ if (servDescription != null) {
+ Description description = servDescription.getDescription();
+ if (description != null) {
+ return description.getValue();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Get the contact from the given tuple.
+ */
+ public static String getTupleContact(Tuple tuple) {
+ if (tuple == null) {
+ return null;
+ }
+ Contact contact = tuple.getContact();
+ if (contact != null) {
+ return contact.getContact();
+ }
+ return null;
+ }
+
+ /**
+ * Get the timestamp from the given tuple.
+ */
+ public static String getTupleTimestamp(Tuple tuple) {
+ if (tuple == null) {
+ return null;
+ }
+ Timestamp timestamp = tuple.getTimestamp();
+ if (timestamp != null) {
+ return timestamp.getValue();
+ }
+ return null;
+ }
+
+ /**
+ * Get the terminated capability which disable all the capabilities.
+ */
+ public static RcsContactUceCapability getTerminatedCapability(Uri contact, String reason) {
+ if (reason == null) reason = "";
+ int requestResult = (Arrays.stream(REQUEST_RESULT_REASON_NOT_FOUND)
+ .anyMatch(reason::equalsIgnoreCase) == true) ?
+ RcsContactUceCapability.REQUEST_RESULT_NOT_FOUND :
+ RcsContactUceCapability.REQUEST_RESULT_UNKNOWN;
+
+ RcsContactUceCapability.PresenceBuilder builder =
+ new RcsContactUceCapability.PresenceBuilder(
+ contact, RcsContactUceCapability.SOURCE_TYPE_NETWORK, requestResult);
+ return builder.build();
+ }
+
+ /**
+ * Get the RcsContactUceCapability instance which the request result is NOT FOUND.
+ */
+ public static RcsContactUceCapability getNotFoundContactCapabilities(Uri contact) {
+ RcsContactUceCapability.PresenceBuilder builder =
+ new RcsContactUceCapability.PresenceBuilder(contact,
+ RcsContactUceCapability.SOURCE_TYPE_NETWORK,
+ RcsContactUceCapability.REQUEST_RESULT_NOT_FOUND);
+ return builder.build();
+ }
+}
diff --git a/src/java/com/android/ims/rcs/uce/presence/pidfparser/capabilities/Audio.java b/src/java/com/android/ims/rcs/uce/presence/pidfparser/capabilities/Audio.java
new file mode 100644
index 00000000..e3fe7ab5
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/presence/pidfparser/capabilities/Audio.java
@@ -0,0 +1,91 @@
+/*
+ * 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.ims.rcs.uce.presence.pidfparser.capabilities;
+
+import android.text.TextUtils;
+
+import com.android.ims.rcs.uce.presence.pidfparser.ElementBase;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.IOException;
+
+/**
+ * The "audio" element of the Capabilities namespace.
+ */
+public class Audio extends ElementBase {
+ /** The name of this element */
+ public static final String ELEMENT_NAME = "audio";
+
+ private boolean mSupported;
+
+ public Audio() {
+ }
+
+ public Audio(boolean supported) {
+ mSupported = supported;
+ }
+
+ @Override
+ protected String initNamespace() {
+ return CapsConstant.NAMESPACE;
+ }
+
+ @Override
+ protected String initElementName() {
+ return ELEMENT_NAME;
+ }
+
+ public boolean isAudioSupported() {
+ return mSupported;
+ }
+
+ @Override
+ public void serialize(XmlSerializer serializer) throws IOException {
+ String namespace = getNamespace();
+ String elementName = getElementName();
+ serializer.startTag(namespace, elementName);
+ serializer.text(String.valueOf(isAudioSupported()));
+ serializer.endTag(namespace, elementName);
+ }
+
+ @Override
+ public void parse(XmlPullParser parser) throws IOException, XmlPullParserException {
+ String namespace = parser.getNamespace();
+ String name = parser.getName();
+
+ if (!verifyParsingElement(namespace, name)) {
+ throw new XmlPullParserException("Incorrect element: " + namespace + ", " + name);
+ }
+
+ // Move to the next event to get the value.
+ int eventType = parser.next();
+
+ // Get the value if the event type is text.
+ if (eventType == XmlPullParser.TEXT) {
+ String isSupported = parser.getText();
+ if (!TextUtils.isEmpty(isSupported)) {
+ mSupported = Boolean.parseBoolean(isSupported);
+ }
+ }
+
+ // Move to the end tag.
+ moveToElementEndTag(parser, eventType);
+ }
+}
diff --git a/src/java/com/android/ims/IFeatureConnector.java b/src/java/com/android/ims/rcs/uce/presence/pidfparser/capabilities/CapsConstant.java
index 66428ce7..2b809b20 100644
--- a/src/java/com/android/ims/IFeatureConnector.java
+++ b/src/java/com/android/ims/rcs/uce/presence/pidfparser/capabilities/CapsConstant.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2019 The Android Open Source Project
+ * 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.
@@ -14,11 +14,8 @@
* limitations under the License.
*/
-package com.android.ims;
+package com.android.ims.rcs.uce.presence.pidfparser.capabilities;
-public interface IFeatureConnector<T> {
- int getImsServiceState() throws ImsException;
- void addNotifyStatusChangedCallbackIfAvailable(FeatureConnection.IFeatureUpdate callback)
- throws android.telephony.ims.ImsException;
- void removeNotifyStatusChangedCallback(FeatureConnection.IFeatureUpdate callback);
-} \ No newline at end of file
+public class CapsConstant {
+ public static final String NAMESPACE = "urn:ietf:params:xml:ns:pidf:caps";
+}
diff --git a/src/java/com/android/ims/rcs/uce/presence/pidfparser/capabilities/Duplex.java b/src/java/com/android/ims/rcs/uce/presence/pidfparser/capabilities/Duplex.java
new file mode 100644
index 00000000..af55a424
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/presence/pidfparser/capabilities/Duplex.java
@@ -0,0 +1,182 @@
+/*
+ * 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.ims.rcs.uce.presence.pidfparser.capabilities;
+
+import android.annotation.StringDef;
+import android.text.TextUtils;
+
+import com.android.ims.rcs.uce.presence.pidfparser.ElementBase;
+
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+/**
+ * The "duplex" element indicates how the communication service send and receive media. It can
+ * contain two elements: "supported" and "notsupported." The supported and
+ * nonsupported elements can contains four elements: "full", "half", "receive-only" and
+ * "send-only".
+ */
+public class Duplex extends ElementBase {
+ /** The name of the duplex element */
+ public static final String ELEMENT_NAME = "duplex";
+
+ /** The name of the supported element */
+ public static final String ELEMENT_SUPPORTED = "supported";
+
+ /** The name of the notsupported element */
+ public static final String ELEMENT_NOT_SUPPORTED = "notsupported";
+
+ /** The device can simultaneously send and receive media */
+ public static final String DUPLEX_FULL = "full";
+
+ /** The service can alternate between sending and receiving media.*/
+ public static final String DUPLEX_HALF = "half";
+
+ /** The service can only receive media */
+ public static final String DUPLEX_RECEIVE_ONLY = "receive-only";
+
+ /** The service can only send media */
+ public static final String DUPLEX_SEND_ONLY = "send-only";
+
+ @StringDef(value = {
+ DUPLEX_FULL,
+ DUPLEX_HALF,
+ DUPLEX_RECEIVE_ONLY,
+ DUPLEX_SEND_ONLY})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface DuplexType {}
+
+ private final List<String> mSupportedTypeList = new ArrayList<>();
+ private final List<String> mNotSupportedTypeList = new ArrayList<>();
+
+ public Duplex() {
+ }
+
+ @Override
+ protected String initNamespace() {
+ return CapsConstant.NAMESPACE;
+ }
+
+ @Override
+ protected String initElementName() {
+ return ELEMENT_NAME;
+ }
+
+ public void addSupportedType(@DuplexType String type) {
+ mSupportedTypeList.add(type);
+ }
+
+ public List<String> getSupportedTypes() {
+ return Collections.unmodifiableList(mSupportedTypeList);
+ }
+
+ public void addNotSupportedType(@DuplexType String type) {
+ mNotSupportedTypeList.add(type);
+ }
+
+ public List<String> getNotSupportedTypes() {
+ return Collections.unmodifiableList(mNotSupportedTypeList);
+ }
+
+ @Override
+ public void serialize(XmlSerializer serializer) throws IOException {
+ if (mSupportedTypeList.isEmpty() && mNotSupportedTypeList.isEmpty()) {
+ return;
+ }
+ String namespace = getNamespace();
+ String elementName = getElementName();
+ serializer.startTag(namespace, elementName);
+ for (String supportedType : mSupportedTypeList) {
+ serializer.startTag(namespace, ELEMENT_SUPPORTED);
+ serializer.startTag(namespace, supportedType);
+ serializer.endTag(namespace, supportedType);
+ serializer.endTag(namespace, ELEMENT_SUPPORTED);
+ }
+ for (String notSupportedType : mNotSupportedTypeList) {
+ serializer.startTag(namespace, ELEMENT_NOT_SUPPORTED);
+ serializer.startTag(namespace, notSupportedType);
+ serializer.endTag(namespace, notSupportedType);
+ serializer.endTag(namespace, ELEMENT_NOT_SUPPORTED);
+ }
+ serializer.endTag(namespace, elementName);
+ }
+
+ @Override
+ public void parse(XmlPullParser parser) throws IOException, XmlPullParserException {
+ String namespace = parser.getNamespace();
+ String name = parser.getName();
+
+ if (!verifyParsingElement(namespace, name)) {
+ throw new XmlPullParserException("Incorrect element: " + namespace + ", " + name);
+ }
+
+ // Move to the next event.
+ int eventType = parser.next();
+
+ while(!(eventType == XmlPullParser.END_TAG
+ && getNamespace().equals(parser.getNamespace())
+ && getElementName().equals(parser.getName()))) {
+
+ if (eventType == XmlPullParser.START_TAG) {
+ String tagName = parser.getName();
+
+ if (ELEMENT_SUPPORTED.equals(tagName)) {
+ String duplexType = getDuplexType(parser);
+ if (!TextUtils.isEmpty(duplexType)) {
+ addSupportedType(duplexType);
+ }
+ } else if (ELEMENT_NOT_SUPPORTED.equals(tagName)) {
+ String duplexType = getDuplexType(parser);
+ if (!TextUtils.isEmpty(duplexType)) {
+ addNotSupportedType(duplexType);
+ }
+ }
+ }
+
+ eventType = parser.next();
+
+ // Leave directly if the event type is the end of the document.
+ if (eventType == XmlPullParser.END_DOCUMENT) {
+ return;
+ }
+ }
+ }
+
+ private String getDuplexType(XmlPullParser parser) throws IOException, XmlPullParserException {
+ // Move to the next event
+ int eventType = parser.next();
+
+ String name = parser.getName();
+ if (eventType == XmlPullParser.START_TAG) {
+ if (DUPLEX_FULL.equals(name) ||
+ DUPLEX_HALF.equals(name) ||
+ DUPLEX_RECEIVE_ONLY.equals(name) ||
+ DUPLEX_SEND_ONLY.equals(name)) {
+ return name;
+ }
+ }
+ return null;
+ }
+}
diff --git a/src/java/com/android/ims/rcs/uce/presence/pidfparser/capabilities/ServiceCaps.java b/src/java/com/android/ims/rcs/uce/presence/pidfparser/capabilities/ServiceCaps.java
new file mode 100644
index 00000000..16b52cdb
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/presence/pidfparser/capabilities/ServiceCaps.java
@@ -0,0 +1,117 @@
+/*
+ * 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.ims.rcs.uce.presence.pidfparser.capabilities;
+
+import com.android.ims.rcs.uce.presence.pidfparser.ElementBase;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * The "servicecaps" element is the root element of service capabilities.
+ */
+public class ServiceCaps extends ElementBase {
+ /** The name of this element */
+ public static final String ELEMENT_NAME = "servcaps";
+
+ // The elements in the "servcaps" element.
+ private final List<ElementBase> mElements = new ArrayList<>();
+
+ public ServiceCaps() {
+ }
+
+ @Override
+ protected String initNamespace() {
+ return CapsConstant.NAMESPACE;
+ }
+
+ @Override
+ protected String initElementName() {
+ return ELEMENT_NAME;
+ }
+
+ public void addElement(ElementBase element) {
+ mElements.add(element);
+ }
+
+ public List<ElementBase> getElements() {
+ return mElements;
+ }
+
+ @Override
+ public void serialize(XmlSerializer serializer) throws IOException {
+ if (mElements.isEmpty()) {
+ return;
+ }
+ String namespace = getNamespace();
+ String elementName = getElementName();
+ serializer.startTag(namespace, elementName);
+ for (ElementBase element : mElements) {
+ element.serialize(serializer);
+ }
+ serializer.endTag(namespace, elementName);
+
+ }
+
+ @Override
+ public void parse(XmlPullParser parser) throws IOException, XmlPullParserException {
+ String namespace = parser.getNamespace();
+ String name = parser.getName();
+
+ if (!verifyParsingElement(namespace, name)) {
+ throw new XmlPullParserException("Incorrect element: " + namespace + ", " + name);
+ }
+
+ // Move to the next event.
+ int eventType = parser.next();
+
+ while(!(eventType == XmlPullParser.END_TAG
+ && getNamespace().equals(parser.getNamespace())
+ && getElementName().equals(parser.getName()))) {
+
+ if (eventType == XmlPullParser.START_TAG) {
+ String tagName = parser.getName();
+
+ if (Audio.ELEMENT_NAME.equals(tagName)) {
+ Audio audio = new Audio();
+ audio.parse(parser);
+ mElements.add(audio);
+ } else if (Video.ELEMENT_NAME.equals(tagName)) {
+ Video video = new Video();
+ video.parse(parser);
+ mElements.add(video);
+ } else if (Duplex.ELEMENT_NAME.equals(tagName)) {
+ Duplex duplex = new Duplex();
+ duplex.parse(parser);
+ mElements.add(duplex);
+ }
+ }
+
+ eventType = parser.next();
+
+ // Leave directly if the event type is the end of the document.
+ if (eventType == XmlPullParser.END_DOCUMENT) {
+ return;
+ }
+ }
+ }
+}
diff --git a/src/java/com/android/ims/rcs/uce/presence/pidfparser/capabilities/Video.java b/src/java/com/android/ims/rcs/uce/presence/pidfparser/capabilities/Video.java
new file mode 100644
index 00000000..290b6141
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/presence/pidfparser/capabilities/Video.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.ims.rcs.uce.presence.pidfparser.capabilities;
+
+import android.text.TextUtils;
+
+import com.android.ims.rcs.uce.presence.pidfparser.ElementBase;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.IOException;
+
+/**
+ * The "video" element of the Capabilities namespace.
+ */
+public class Video extends ElementBase {
+ /** The name of this element */
+ public static final String ELEMENT_NAME = "video";
+
+ private boolean mSupported;
+
+ public Video() {
+ }
+
+ public Video(boolean supported) {
+ mSupported = supported;
+ }
+
+ @Override
+ protected String initNamespace() {
+ return CapsConstant.NAMESPACE;
+ }
+
+ @Override
+ protected String initElementName() {
+ return ELEMENT_NAME;
+ }
+
+ public boolean isVideoSupported() {
+ return mSupported;
+ }
+
+ @Override
+ public void serialize(XmlSerializer serializer) throws IOException {
+ String namespace = getNamespace();
+ String elementName = getElementName();
+ serializer.startTag(namespace, elementName);
+ serializer.text(String.valueOf(isVideoSupported()));
+ serializer.endTag(namespace, elementName);
+ }
+
+ @Override
+ public void parse(XmlPullParser parser) throws IOException, XmlPullParserException {
+ String namespace = parser.getNamespace();
+ String name = parser.getName();
+
+ if (!verifyParsingElement(namespace, name)) {
+ throw new XmlPullParserException("Incorrect element: " + namespace + ", " + name);
+ }
+
+ // Move to the next event to get the value.
+ int eventType = parser.next();
+
+ // Get the value if the event type is text.
+ if (eventType == XmlPullParser.TEXT) {
+ String isSupported = parser.getText();
+ if (!TextUtils.isEmpty(isSupported)) {
+ mSupported = Boolean.parseBoolean(isSupported);
+ }
+ }
+
+ // Move to the end tag.
+ moveToElementEndTag(parser, eventType);
+ }
+
+}
diff --git a/src/java/com/android/ims/rcs/uce/presence/pidfparser/omapres/Description.java b/src/java/com/android/ims/rcs/uce/presence/pidfparser/omapres/Description.java
new file mode 100644
index 00000000..8b4b8614
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/presence/pidfparser/omapres/Description.java
@@ -0,0 +1,94 @@
+/*
+ * 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.ims.rcs.uce.presence.pidfparser.omapres;
+
+import android.text.TextUtils;
+
+import com.android.ims.rcs.uce.presence.pidfparser.ElementBase;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.IOException;
+
+/**
+ * The "description" element of the pidf
+ */
+public class Description extends ElementBase {
+ /** The name of this element */
+ public static final String ELEMENT_NAME = "description";
+
+ private String mDescription;
+
+ public Description() {
+ }
+
+ public Description(String description) {
+ mDescription = description;
+ }
+
+ @Override
+ protected String initNamespace() {
+ return OmaPresConstant.NAMESPACE;
+ }
+
+ @Override
+ protected String initElementName() {
+ return ELEMENT_NAME;
+ }
+
+ public String getValue() {
+ return mDescription;
+ }
+
+ @Override
+ public void serialize(XmlSerializer serializer) throws IOException {
+ if (mDescription == null) {
+ return;
+ }
+ String namespace = getNamespace();
+ String elementName = getElementName();
+ serializer.startTag(namespace, elementName);
+ serializer.text(mDescription);
+ serializer.endTag(namespace, elementName);
+ }
+
+ @Override
+ public void parse(XmlPullParser parser) throws IOException, XmlPullParserException {
+ String namespace = parser.getNamespace();
+ String name = parser.getName();
+
+ if (!verifyParsingElement(namespace, name)) {
+ throw new XmlPullParserException("Incorrect element: " + namespace + ", " + name);
+ }
+
+ // Move to the next event to get the value.
+ int eventType = parser.next();
+
+ // Get the value if the event type is text.
+ if (eventType == XmlPullParser.TEXT) {
+ String description = parser.getText();
+ if (!TextUtils.isEmpty(description)) {
+ mDescription = description;
+ }
+ }
+
+ // Move to the end tag.
+ moveToElementEndTag(parser, eventType);
+ }
+}
diff --git a/src/java/com/android/ims/rcs/uce/presence/pidfparser/omapres/OmaPresConstant.java b/src/java/com/android/ims/rcs/uce/presence/pidfparser/omapres/OmaPresConstant.java
new file mode 100644
index 00000000..668a1b38
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/presence/pidfparser/omapres/OmaPresConstant.java
@@ -0,0 +1,21 @@
+/*
+ * 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.ims.rcs.uce.presence.pidfparser.omapres;
+
+public class OmaPresConstant {
+ public static final String NAMESPACE = "urn:oma:xml:prs:pidf:oma-pres";
+}
diff --git a/src/java/com/android/ims/rcs/uce/presence/pidfparser/omapres/ServiceDescription.java b/src/java/com/android/ims/rcs/uce/presence/pidfparser/omapres/ServiceDescription.java
new file mode 100644
index 00000000..1a4eedad
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/presence/pidfparser/omapres/ServiceDescription.java
@@ -0,0 +1,137 @@
+/*
+ * 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.ims.rcs.uce.presence.pidfparser.omapres;
+
+import com.android.ims.rcs.uce.presence.pidfparser.ElementBase;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.IOException;
+
+/**
+ * The "service-description" element of the pidf.
+ */
+public class ServiceDescription extends ElementBase {
+ /** The name of this element */
+ public static final String ELEMENT_NAME = "service-description";
+
+ private ServiceId mServiceId;
+ private Version mVersion;
+ private Description mDescription;
+
+ public ServiceDescription() {
+ }
+
+ @Override
+ protected String initNamespace() {
+ return OmaPresConstant.NAMESPACE;
+ }
+
+ @Override
+ protected String initElementName() {
+ return ELEMENT_NAME;
+ }
+
+ public void setServiceId(ServiceId serviceId) {
+ mServiceId = serviceId;
+ }
+
+ public ServiceId getServiceId() {
+ return mServiceId;
+ }
+
+ public void setVersion(Version version) {
+ mVersion = version;
+ }
+
+ public Version getVersion() {
+ return mVersion;
+ }
+
+ public void setDescription(Description description) {
+ mDescription = description;
+ }
+
+ public Description getDescription() {
+ return mDescription;
+ }
+
+ @Override
+ public void serialize(XmlSerializer serializer) throws IOException {
+ if(mServiceId == null && mVersion == null && mDescription == null) {
+ return;
+ }
+ final String namespace = getNamespace();
+ final String element = getElementName();
+ serializer.startTag(namespace, element);
+ if (mServiceId != null) {
+ mServiceId.serialize(serializer);
+ }
+ if (mVersion != null) {
+ mVersion.serialize(serializer);
+ }
+ if (mDescription != null) {
+ mDescription.serialize(serializer);
+ }
+ serializer.endTag(namespace, element);
+ }
+
+ @Override
+ public void parse(XmlPullParser parser) throws IOException, XmlPullParserException {
+ String namespace = parser.getNamespace();
+ String name = parser.getName();
+
+ if (!verifyParsingElement(namespace, name)) {
+ throw new XmlPullParserException("Incorrect element: " + namespace + ", " + name);
+ }
+
+ // Move to the next event.
+ int eventType = parser.next();
+
+ while(!(eventType == XmlPullParser.END_TAG
+ && getNamespace().equals(parser.getNamespace())
+ && getElementName().equals(parser.getName()))) {
+
+ if (eventType == XmlPullParser.START_TAG) {
+ String tagName = parser.getName();
+
+ if (ServiceId.ELEMENT_NAME.equals(tagName)) {
+ ServiceId serviceId = new ServiceId();
+ serviceId.parse(parser);
+ mServiceId = serviceId;
+ } else if (Version.ELEMENT_NAME.equals(tagName)) {
+ Version version = new Version();
+ version.parse(parser);
+ mVersion = version;
+ } else if (Description.ELEMENT_NAME.equals(tagName)) {
+ Description description = new Description();
+ description.parse(parser);
+ mDescription = description;
+ }
+ }
+
+ eventType = parser.next();
+
+ // Leave directly if the event type is the end of the document.
+ if (eventType == XmlPullParser.END_DOCUMENT) {
+ return;
+ }
+ }
+ }
+}
diff --git a/src/java/com/android/ims/rcs/uce/presence/pidfparser/omapres/ServiceId.java b/src/java/com/android/ims/rcs/uce/presence/pidfparser/omapres/ServiceId.java
new file mode 100644
index 00000000..db821fb5
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/presence/pidfparser/omapres/ServiceId.java
@@ -0,0 +1,94 @@
+/*
+ * 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.ims.rcs.uce.presence.pidfparser.omapres;
+
+import android.text.TextUtils;
+
+import com.android.ims.rcs.uce.presence.pidfparser.ElementBase;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.IOException;
+
+/**
+ * The "service-id" element of the pidf.
+ */
+public class ServiceId extends ElementBase {
+ /** The name of this element */
+ public static final String ELEMENT_NAME = "service-id";
+
+ private String mServiceId;
+
+ public ServiceId() {
+ }
+
+ public ServiceId(String serviceId) {
+ mServiceId = serviceId;
+ }
+
+ @Override
+ protected String initNamespace() {
+ return OmaPresConstant.NAMESPACE;
+ }
+
+ @Override
+ protected String initElementName() {
+ return ELEMENT_NAME;
+ }
+
+ public String getValue() {
+ return mServiceId;
+ }
+
+ @Override
+ public void serialize(XmlSerializer serializer) throws IOException {
+ if (mServiceId == null) {
+ return;
+ }
+ String namespace = getNamespace();
+ String elementName = getElementName();
+ serializer.startTag(namespace, elementName);
+ serializer.text(mServiceId);
+ serializer.endTag(namespace, elementName);
+ }
+
+ @Override
+ public void parse(XmlPullParser parser) throws IOException, XmlPullParserException {
+ String namespace = parser.getNamespace();
+ String name = parser.getName();
+
+ if (!verifyParsingElement(namespace, name)) {
+ throw new XmlPullParserException("Incorrect element: " + namespace + ", " + name);
+ }
+
+ // Move to the next event to get the value.
+ int eventType = parser.next();
+
+ // Get the value if the event type is text.
+ if (eventType == XmlPullParser.TEXT) {
+ String serviceId = parser.getText();
+ if (!TextUtils.isEmpty(serviceId)) {
+ mServiceId = serviceId;
+ }
+ }
+
+ // Move to the end tag.
+ moveToElementEndTag(parser, eventType);
+ }
+}
diff --git a/src/java/com/android/ims/rcs/uce/presence/pidfparser/omapres/Version.java b/src/java/com/android/ims/rcs/uce/presence/pidfparser/omapres/Version.java
new file mode 100644
index 00000000..8e0a7211
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/presence/pidfparser/omapres/Version.java
@@ -0,0 +1,105 @@
+/*
+ * 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.ims.rcs.uce.presence.pidfparser.omapres;
+
+import android.text.TextUtils;
+
+import com.android.ims.rcs.uce.presence.pidfparser.ElementBase;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.IOException;
+
+/**
+ * The "version" element of the pidf.
+ */
+public class Version extends ElementBase {
+ /** The name of this element */
+ public static final String ELEMENT_NAME = "version";
+
+ private int mMajorVersion;
+ private int mMinorVersion;
+
+ public Version() {
+ }
+
+ public Version(int majorVersion, int minorVersion) {
+ mMajorVersion = majorVersion;
+ mMinorVersion = minorVersion;
+ }
+
+ @Override
+ protected String initNamespace() {
+ return OmaPresConstant.NAMESPACE;
+ }
+
+ @Override
+ protected String initElementName() {
+ return ELEMENT_NAME;
+ }
+
+ public String getValue() {
+ StringBuilder builder = new StringBuilder();
+ builder.append(mMajorVersion).append(".").append(mMinorVersion);
+ return builder.toString();
+ }
+
+ @Override
+ public void serialize(XmlSerializer serializer) throws IOException {
+ String namespace = getNamespace();
+ String elementName = getElementName();
+ serializer.startTag(namespace, elementName);
+ serializer.text(getValue());
+ serializer.endTag(namespace, elementName);
+ }
+
+ @Override
+ public void parse(XmlPullParser parser) throws IOException, XmlPullParserException {
+ String namespace = parser.getNamespace();
+ String name = parser.getName();
+
+ if (!verifyParsingElement(namespace, name)) {
+ throw new XmlPullParserException("Incorrect element: " + namespace + ", " + name);
+ }
+
+ // Move to the next event to get the value.
+ int eventType = parser.next();
+
+ // Get the value if the event type is text.
+ if (eventType == XmlPullParser.TEXT) {
+ String version = parser.getText();
+ handleParsedVersion(version);
+ }
+
+ // Move to the end tag.
+ moveToElementEndTag(parser, eventType);
+ }
+
+ private void handleParsedVersion(String version) {
+ if (TextUtils.isEmpty(version)) {
+ return;
+ }
+
+ String[] versionAry = version.split("\\.");
+ if (versionAry != null && versionAry.length == 2) {
+ mMajorVersion = Integer.parseInt(versionAry[0]);
+ mMinorVersion = Integer.parseInt(versionAry[1]);
+ }
+ }
+}
diff --git a/src/java/com/android/ims/rcs/uce/presence/pidfparser/pidf/Basic.java b/src/java/com/android/ims/rcs/uce/presence/pidfparser/pidf/Basic.java
new file mode 100644
index 00000000..a4f487a6
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/presence/pidfparser/pidf/Basic.java
@@ -0,0 +1,118 @@
+/*
+ * 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.ims.rcs.uce.presence.pidfparser.pidf;
+
+import android.annotation.StringDef;
+import android.util.Log;
+
+import com.android.ims.rcs.uce.presence.pidfparser.ElementBase;
+import com.android.ims.rcs.uce.util.UceUtils;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * The "basic" element of the pidf.
+ */
+public class Basic extends ElementBase {
+ private static final String LOG_TAG = UceUtils.getLogPrefix() + "Basic";
+
+ /** The name of this element */
+ public static final String ELEMENT_NAME = "basic";
+
+ /** The value "open" of the Basic element */
+ public static final String OPEN = "open";
+
+ /** The value "closed" of the Basic element */
+ public static final String CLOSED = "closed";
+
+ @StringDef(value = {
+ OPEN,
+ CLOSED})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface BasicValue {}
+
+ private @BasicValue String mBasic;
+
+ public Basic() {
+ }
+
+ public Basic(@BasicValue String value) {
+ mBasic = value;
+ }
+
+ @Override
+ protected String initNamespace() {
+ return PidfConstant.NAMESPACE;
+ }
+
+ @Override
+ protected String initElementName() {
+ return ELEMENT_NAME;
+ }
+
+ public String getValue() {
+ return mBasic;
+ }
+
+ @Override
+ public void serialize(XmlSerializer serializer) throws IOException {
+ if (mBasic == null) {
+ return;
+ }
+ final String namespace = getNamespace();
+ final String element = getElementName();
+ serializer.startTag(namespace, element);
+ serializer.text(mBasic);
+ serializer.endTag(namespace, element);
+ }
+
+ @Override
+ public void parse(XmlPullParser parser) throws IOException, XmlPullParserException {
+ String namespace = parser.getNamespace();
+ String name = parser.getName();
+
+ if (!verifyParsingElement(namespace, name)) {
+ throw new XmlPullParserException("Incorrect element: " + namespace + ", " + name);
+ }
+
+ // Move to the next event to get the value.
+ int eventType = parser.next();
+
+ // Get the value if the event type is text.
+ if (eventType == XmlPullParser.TEXT) {
+ String basicValue = parser.getText();
+ if (OPEN.equals(basicValue)) {
+ mBasic = OPEN;
+ } else if (CLOSED.equals(basicValue)) {
+ mBasic = CLOSED;
+ } else {
+ mBasic = null;
+ }
+ } else {
+ Log.d(LOG_TAG, "The eventType is not TEXT=" + eventType);
+ }
+
+ // Move to the end tag.
+ moveToElementEndTag(parser, eventType);
+ }
+}
diff --git a/src/java/com/android/ims/rcs/uce/presence/pidfparser/pidf/Contact.java b/src/java/com/android/ims/rcs/uce/presence/pidfparser/pidf/Contact.java
new file mode 100644
index 00000000..df5c800d
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/presence/pidfparser/pidf/Contact.java
@@ -0,0 +1,114 @@
+/*
+ * 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.ims.rcs.uce.presence.pidfparser.pidf;
+
+import android.text.TextUtils;
+
+import com.android.ims.rcs.uce.presence.pidfparser.ElementBase;
+import com.android.internal.annotations.VisibleForTesting;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.IOException;
+
+/**
+ * The "contact" element of the pidf.
+ */
+public class Contact extends ElementBase {
+ /** The name of this element */
+ public static final String ELEMENT_NAME = "contact";
+
+ private Double mPriority;
+ private String mContact;
+
+ public Contact() {
+ }
+
+ @Override
+ protected String initNamespace() {
+ return PidfConstant.NAMESPACE;
+ }
+
+ @Override
+ protected String initElementName() {
+ return ELEMENT_NAME;
+ }
+
+ public void setPriority(Double priority) {
+ mPriority = priority;
+ }
+
+ @VisibleForTesting
+ public Double getPriority() {
+ return mPriority;
+ }
+
+ public void setContact(String contact) {
+ mContact = contact;
+ }
+
+ public String getContact() {
+ return mContact;
+ }
+
+ @Override
+ public void serialize(XmlSerializer serializer) throws IOException {
+ if (mContact == null) {
+ return;
+ }
+ String noNamespace = XmlPullParser.NO_NAMESPACE;
+ String namespace = getNamespace();
+ String elementName = getElementName();
+ serializer.startTag(namespace, elementName);
+ if (mPriority != null) {
+ serializer.attribute(noNamespace, "priority", String.valueOf(mPriority));
+ }
+ serializer.text(mContact);
+ serializer.endTag(namespace, elementName);
+ }
+
+ @Override
+ public void parse(XmlPullParser parser) throws IOException, XmlPullParserException {
+ String namespace = parser.getNamespace();
+ String name = parser.getName();
+
+ if (!verifyParsingElement(namespace, name)) {
+ throw new XmlPullParserException("Incorrect element: " + namespace + ", " + name);
+ }
+
+ String priority = parser.getAttributeValue(XmlPullParser.NO_NAMESPACE, "priority");
+ if (!TextUtils.isEmpty(priority)) {
+ mPriority = Double.parseDouble(priority);
+ }
+
+ // Move to the next event to get the value.
+ int eventType = parser.next();
+
+ // Get the value if the event type is text.
+ if (eventType == XmlPullParser.TEXT) {
+ String contact = parser.getText();
+ if (!TextUtils.isEmpty(contact)) {
+ mContact = contact;
+ }
+ }
+
+ // Move to the end tag.
+ moveToElementEndTag(parser, eventType);
+ }
+}
diff --git a/src/java/com/android/ims/rcs/uce/presence/pidfparser/pidf/Note.java b/src/java/com/android/ims/rcs/uce/presence/pidfparser/pidf/Note.java
new file mode 100644
index 00000000..ef13b5bc
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/presence/pidfparser/pidf/Note.java
@@ -0,0 +1,95 @@
+/*
+ * 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.ims.rcs.uce.presence.pidfparser.pidf;
+
+import android.text.TextUtils;
+
+import com.android.ims.rcs.uce.presence.pidfparser.ElementBase;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.IOException;
+
+/**
+ * The "note" element of the pidf. This element is usually used for a human readable comment.
+ * It may appear as a child element of "presence" or as a child element of the "tuple" element.
+ */
+public class Note extends ElementBase {
+ /** The name of this element */
+ public static final String ELEMENT_NAME = "note";
+
+ private String mNote;
+
+ public Note() {
+ }
+
+ public Note(String note) {
+ mNote = note;
+ }
+
+ @Override
+ protected String initNamespace() {
+ return PidfConstant.NAMESPACE;
+ }
+
+ @Override
+ protected String initElementName() {
+ return ELEMENT_NAME;
+ }
+
+ public String getNote() {
+ return mNote;
+ }
+
+ @Override
+ public void serialize(XmlSerializer serializer) throws IOException {
+ if (mNote == null) {
+ return;
+ }
+ final String namespace = getNamespace();
+ final String element = getElementName();
+ serializer.startTag(namespace, element);
+ serializer.text(mNote);
+ serializer.endTag(namespace, element);
+ }
+
+ @Override
+ public void parse(XmlPullParser parser) throws IOException, XmlPullParserException {
+ String namespace = parser.getNamespace();
+ String name = parser.getName();
+
+ if (!verifyParsingElement(namespace, name)) {
+ throw new XmlPullParserException("Incorrect element: " + namespace + ", " + name);
+ }
+
+ // Move to the next event to get the value.
+ int eventType = parser.next();
+
+ // Get the value if the event type is text.
+ if (eventType == XmlPullParser.TEXT) {
+ String note = parser.getText();
+ if (!TextUtils.isEmpty(note)) {
+ mNote = note;
+ }
+ }
+
+ // Move to the end tag.
+ moveToElementEndTag(parser, eventType);
+ }
+}
diff --git a/src/java/com/android/ims/rcs/uce/presence/pidfparser/pidf/PidfConstant.java b/src/java/com/android/ims/rcs/uce/presence/pidfparser/pidf/PidfConstant.java
new file mode 100644
index 00000000..ac9c9da1
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/presence/pidfparser/pidf/PidfConstant.java
@@ -0,0 +1,21 @@
+/*
+ * 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.ims.rcs.uce.presence.pidfparser.pidf;
+
+public class PidfConstant {
+ public static final String NAMESPACE = "urn:ietf:params:xml:ns:pidf";
+}
diff --git a/src/java/com/android/ims/rcs/uce/presence/pidfparser/pidf/Presence.java b/src/java/com/android/ims/rcs/uce/presence/pidfparser/pidf/Presence.java
new file mode 100644
index 00000000..e9a40a84
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/presence/pidfparser/pidf/Presence.java
@@ -0,0 +1,176 @@
+/*
+ * 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.ims.rcs.uce.presence.pidfparser.pidf;
+
+import android.annotation.NonNull;
+import android.net.Uri;
+
+import com.android.ims.rcs.uce.presence.pidfparser.ElementBase;
+import com.android.internal.annotations.VisibleForTesting;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * The "present" element is the root element of an "application/pidf+xml" object.
+ */
+public class Presence extends ElementBase {
+ /**
+ * The presence element consists the following elements:
+ * 1: Any number (including 0) of <tuple> elements
+ * 2: Any number (including 0) of <note> elements
+ * 3: Any number of OPTIONAL extension elements from other namespaces.
+ */
+
+ /** The name of this element */
+ public static final String ELEMENT_NAME = "presence";
+
+ private static final String ATTRIBUTE_NAME_ENTITY = "entity";
+
+ // The presence element must have an "entity" attribute.
+ private String mEntity;
+
+ // The presence element contains any number of <tuple> elements
+ private final List<Tuple> mTupleList = new ArrayList<>();
+
+ // The presence element contains any number of <note> elements;
+ private final List<Note> mNoteList = new ArrayList<>();
+
+ public Presence() {
+ }
+
+ public Presence(@NonNull Uri contact) {
+ initEntity(contact);
+ }
+
+ private void initEntity(Uri contact) {
+ mEntity = contact.toString();
+ }
+
+ @VisibleForTesting
+ public void setEntity(String entity) {
+ mEntity = entity;
+ }
+
+ @Override
+ protected String initNamespace() {
+ return PidfConstant.NAMESPACE;
+ }
+
+ @Override
+ protected String initElementName() {
+ return ELEMENT_NAME;
+ }
+
+ public String getEntity() {
+ return mEntity;
+ }
+
+ public void addTuple(@NonNull Tuple tuple) {
+ mTupleList.add(tuple);
+ }
+
+ public @NonNull List<Tuple> getTupleList() {
+ return Collections.unmodifiableList(mTupleList);
+ }
+
+ public void addNote(@NonNull Note note) {
+ mNoteList.add(note);
+ }
+
+ public @NonNull List<Note> getNoteList() {
+ return Collections.unmodifiableList(mNoteList);
+ }
+
+ @Override
+ public void serialize(XmlSerializer serializer) throws IOException {
+ String namespace = getNamespace();
+ String elementName = getElementName();
+
+ serializer.startTag(namespace, elementName);
+ // entity attribute
+ serializer.attribute(XmlPullParser.NO_NAMESPACE, ATTRIBUTE_NAME_ENTITY, mEntity);
+
+ // tuple elements
+ for (Tuple tuple : mTupleList) {
+ tuple.serialize(serializer);
+ }
+
+ // note elements
+ for (Note note : mNoteList) {
+ note.serialize(serializer);
+ }
+ serializer.endTag(namespace, elementName);
+ }
+
+ @Override
+ public void parse(XmlPullParser parser) throws IOException, XmlPullParserException {
+ String namespace = parser.getNamespace();
+ String name = parser.getName();
+
+ if (!verifyParsingElement(namespace, name)) {
+ throw new XmlPullParserException("Incorrect element: " + namespace + ", " + name);
+ }
+
+ mEntity = parser.getAttributeValue(XmlPullParser.NO_NAMESPACE, ATTRIBUTE_NAME_ENTITY);
+
+ // Move to the next event.
+ int eventType = parser.next();
+
+ while(!(eventType == XmlPullParser.END_TAG
+ && getNamespace().equals(parser.getNamespace())
+ && getElementName().equals(parser.getName()))) {
+
+ if (eventType == XmlPullParser.START_TAG) {
+ String tagName = parser.getName();
+
+ if (isTupleElement(eventType, tagName)) {
+ Tuple tuple = new Tuple();
+ tuple.parse(parser);
+ mTupleList.add(tuple);
+ } else if (isNoteElement(eventType, tagName)) {
+ Note note = new Note();
+ note.parse(parser);
+ mNoteList.add(note);
+ }
+ }
+
+ eventType = parser.next();
+
+ // Leave directly if the event type is the end of the document.
+ if (eventType == XmlPullParser.END_DOCUMENT) {
+ return;
+ }
+ }
+ }
+
+ private boolean isTupleElement(int eventType, String name) {
+ return (eventType == XmlPullParser.START_TAG && Tuple.ELEMENT_NAME.equals(name)) ?
+ true : false;
+ }
+
+ private boolean isNoteElement(int eventType, String name) {
+ return (eventType == XmlPullParser.START_TAG && Note.ELEMENT_NAME.equals(name)) ?
+ true : false;
+ }
+}
diff --git a/src/java/com/android/ims/rcs/uce/presence/pidfparser/pidf/Status.java b/src/java/com/android/ims/rcs/uce/presence/pidfparser/pidf/Status.java
new file mode 100644
index 00000000..92ad5d68
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/presence/pidfparser/pidf/Status.java
@@ -0,0 +1,99 @@
+/*
+ * 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.ims.rcs.uce.presence.pidfparser.pidf;
+
+import android.util.Log;
+
+import com.android.ims.rcs.uce.presence.pidfparser.ElementBase;
+import com.android.ims.rcs.uce.util.UceUtils;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.IOException;
+
+/**
+ * The "status" element of the pidf.
+ */
+public class Status extends ElementBase {
+ private static final String LOG_TAG = UceUtils.getLogPrefix() + "Status";
+
+ /** The name of this element */
+ public static final String ELEMENT_NAME = "status";
+
+ // The "status" element contain one optional "basic" element.
+ private Basic mBasic;
+
+ public Status() {
+ }
+
+ @Override
+ protected String initNamespace() {
+ return PidfConstant.NAMESPACE;
+ }
+
+ @Override
+ protected String initElementName() {
+ return ELEMENT_NAME;
+ }
+
+ public void setBasic(Basic basic) {
+ mBasic = basic;
+ }
+
+ public Basic getBasic() {
+ return mBasic;
+ }
+
+ @Override
+ public void serialize(XmlSerializer serializer) throws IOException {
+ if (mBasic == null) {
+ return;
+ }
+ final String namespace = getNamespace();
+ final String element = getElementName();
+ serializer.startTag(namespace, element);
+ mBasic.serialize(serializer);
+ serializer.endTag(namespace, element);
+ }
+
+ @Override
+ public void parse(XmlPullParser parser) throws IOException, XmlPullParserException {
+ String namespace = parser.getNamespace();
+ String name = parser.getName();
+
+ if (!verifyParsingElement(namespace, name)) {
+ throw new XmlPullParserException("Incorrect element: " + namespace + ", " + name);
+ }
+
+ // Move to the next tag to get the Basic element.
+ int eventType = parser.nextTag();
+
+ // Get the value if the event type is text.
+ if (eventType == XmlPullParser.START_TAG) {
+ Basic basic = new Basic();
+ basic.parse(parser);
+ mBasic = basic;
+ } else {
+ Log.d(LOG_TAG, "The eventType is not START_TAG=" + eventType);
+ }
+
+ // Move to the end tag.
+ moveToElementEndTag(parser, eventType);
+ }
+}
diff --git a/src/java/com/android/ims/rcs/uce/presence/pidfparser/pidf/Timestamp.java b/src/java/com/android/ims/rcs/uce/presence/pidfparser/pidf/Timestamp.java
new file mode 100644
index 00000000..4c0d8105
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/presence/pidfparser/pidf/Timestamp.java
@@ -0,0 +1,91 @@
+/*
+ * 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.ims.rcs.uce.presence.pidfparser.pidf;
+
+import android.text.TextUtils;
+
+import com.android.ims.rcs.uce.presence.pidfparser.ElementBase;
+
+import java.io.IOException;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+public class Timestamp extends ElementBase {
+ /** The name of this element */
+ public static final String ELEMENT_NAME = "timestamp";
+
+ private String mTimestamp;
+
+ public Timestamp() {
+ }
+
+ public Timestamp(String timestamp) {
+ mTimestamp = timestamp;
+ }
+
+ @Override
+ protected String initNamespace() {
+ return PidfConstant.NAMESPACE;
+ }
+
+ @Override
+ protected String initElementName() {
+ return ELEMENT_NAME;
+ }
+
+ public String getValue() {
+ return mTimestamp;
+ }
+
+ @Override
+ public void serialize(XmlSerializer serializer) throws IOException {
+ if (mTimestamp == null) {
+ return;
+ }
+ final String namespace = getNamespace();
+ final String element = getElementName();
+ serializer.startTag(namespace, element);
+ serializer.text(mTimestamp);
+ serializer.endTag(namespace, element);
+ }
+
+ @Override
+ public void parse(XmlPullParser parser) throws IOException, XmlPullParserException {
+ String namespace = parser.getNamespace();
+ String name = parser.getName();
+
+ if (!verifyParsingElement(namespace, name)) {
+ throw new XmlPullParserException("Incorrect element: " + namespace + ", " + name);
+ }
+
+ // Move to the next event to get the value.
+ int eventType = parser.next();
+
+ // Get the value if the event type is text.
+ if (eventType == XmlPullParser.TEXT) {
+ String timestamp = parser.getText();
+ if (!TextUtils.isEmpty(timestamp)) {
+ mTimestamp = timestamp;
+ }
+ }
+
+ // Move to the end tag.
+ moveToElementEndTag(parser, eventType);
+ }
+}
diff --git a/src/java/com/android/ims/rcs/uce/presence/pidfparser/pidf/Tuple.java b/src/java/com/android/ims/rcs/uce/presence/pidfparser/pidf/Tuple.java
new file mode 100644
index 00000000..014dbed0
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/presence/pidfparser/pidf/Tuple.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.ims.rcs.uce.presence.pidfparser.pidf;
+
+import com.android.ims.rcs.uce.presence.pidfparser.ElementBase;
+import com.android.ims.rcs.uce.presence.pidfparser.capabilities.ServiceCaps;
+import com.android.ims.rcs.uce.presence.pidfparser.omapres.ServiceDescription;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * The "tuple" element of the pidf.
+ */
+public class Tuple extends ElementBase {
+ /**
+ * The tuple element consists the following elements:
+ * 1: one "status" element
+ * 2: any number of optional extension elements
+ * 3: an optional "contact" element
+ * 4: any number of optional "note" elements
+ * 5: an optional "timestamp" element
+ */
+
+ /** The name of this element */
+ public static final String ELEMENT_NAME = "tuple";
+
+ private static final String ATTRIBUTE_NAME_TUPLE_ID = "id";
+
+ private static long sTupleId = 0;
+
+ private static final Object LOCK = new Object();
+
+ private String mId;
+ private Status mStatus;
+ private ServiceDescription mServiceDescription;
+ private ServiceCaps mServiceCaps;
+ private Contact mContact;
+ private List<Note> mNoteList = new ArrayList<>();
+ private Timestamp mTimestamp;
+
+ public Tuple() {
+ mId = getTupleId();
+ }
+
+ @Override
+ protected String initNamespace() {
+ return PidfConstant.NAMESPACE;
+ }
+
+ @Override
+ protected String initElementName() {
+ return ELEMENT_NAME;
+ }
+
+ public void setStatus(Status status) {
+ mStatus = status;
+ }
+
+ public Status getStatus() {
+ return mStatus;
+ }
+
+ public void setServiceDescription(ServiceDescription servDescription) {
+ mServiceDescription = servDescription;
+ }
+
+ public ServiceDescription getServiceDescription() {
+ return mServiceDescription;
+ }
+
+ public void setServiceCaps(ServiceCaps serviceCaps) {
+ mServiceCaps = serviceCaps;
+ }
+
+ public ServiceCaps getServiceCaps() {
+ return mServiceCaps;
+ }
+
+ public void setContact(Contact contact) {
+ mContact = contact;
+ }
+
+ public Contact getContact() {
+ return mContact;
+ }
+
+ public void addNote(Note note) {
+ mNoteList.add(note);
+ }
+
+ public List<Note> getNoteList() {
+ return Collections.unmodifiableList(mNoteList);
+ }
+
+ public void setTimestamp(Timestamp timestamp) {
+ mTimestamp = timestamp;
+ }
+
+ public Timestamp getTimestamp() {
+ return mTimestamp;
+ }
+
+ @Override
+ public void serialize(XmlSerializer serializer) throws IOException {
+ String namespace = getNamespace();
+ String elementName = getElementName();
+
+ serializer.startTag(namespace, elementName);
+ // id attribute
+ serializer.attribute(XmlPullParser.NO_NAMESPACE, ATTRIBUTE_NAME_TUPLE_ID, mId);
+
+ // status element
+ mStatus.serialize(serializer);
+
+ // Service description
+ if (mServiceDescription != null) {
+ mServiceDescription.serialize(serializer);
+ }
+
+ // Service capabilities
+ if (mServiceCaps != null) {
+ mServiceCaps.serialize(serializer);
+ }
+
+ // contact element
+ if (mContact != null) {
+ mContact.serialize(serializer);
+ }
+
+ // note element
+ for (Note note: mNoteList) {
+ note.serialize(serializer);
+ }
+
+ // Timestamp
+ if (mTimestamp != null) {
+ mTimestamp.serialize(serializer);
+ }
+ serializer.endTag(namespace, elementName);
+ }
+
+ @Override
+ public void parse(XmlPullParser parser) throws IOException, XmlPullParserException {
+ String namespace = parser.getNamespace();
+ String name = parser.getName();
+
+ if (!verifyParsingElement(namespace, name)) {
+ throw new XmlPullParserException("Incorrect element: " + namespace + ", " + name);
+ }
+
+ // id attribute
+ mId = parser.getAttributeValue(XmlPullParser.NO_NAMESPACE, ATTRIBUTE_NAME_TUPLE_ID);
+
+ // Move to the next event.
+ int eventType = parser.next();
+
+ while(!(eventType == XmlPullParser.END_TAG
+ && getNamespace().equals(parser.getNamespace())
+ && getElementName().equals(parser.getName()))) {
+
+ if (eventType == XmlPullParser.START_TAG) {
+ String tagName = parser.getName();
+
+ if (Status.ELEMENT_NAME.equals(tagName)) {
+ Status status = new Status();
+ status.parse(parser);
+ mStatus = status;
+ } else if (ServiceDescription.ELEMENT_NAME.equals(tagName)) {
+ ServiceDescription serviceDescription = new ServiceDescription();
+ serviceDescription.parse(parser);
+ mServiceDescription = serviceDescription;
+ } else if (ServiceCaps.ELEMENT_NAME.equals(tagName)) {
+ ServiceCaps serviceCaps = new ServiceCaps();
+ serviceCaps.parse(parser);
+ mServiceCaps = serviceCaps;
+ } else if (Contact.ELEMENT_NAME.equals(tagName)) {
+ Contact contact = new Contact();
+ contact.parse(parser);
+ mContact = contact;
+ } else if (Note.ELEMENT_NAME.equals(tagName)) {
+ Note note = new Note();
+ note.parse(parser);
+ mNoteList.add(note);
+ } else if (Timestamp.ELEMENT_NAME.equals(tagName)) {
+ Timestamp timestamp = new Timestamp();
+ timestamp.parse(parser);
+ mTimestamp = timestamp;
+ }
+ }
+
+ eventType = parser.next();
+
+ // Leave directly if the event type is the end of the document.
+ if (eventType == XmlPullParser.END_DOCUMENT) {
+ return;
+ }
+ }
+ }
+
+ private String getTupleId() {
+ synchronized (LOCK) {
+ return "tid" + (sTupleId++);
+ }
+ }
+}
diff --git a/src/java/com/android/ims/rcs/uce/presence/publish/DeviceCapabilityInfo.java b/src/java/com/android/ims/rcs/uce/presence/publish/DeviceCapabilityInfo.java
new file mode 100644
index 00000000..16d6cea2
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/presence/publish/DeviceCapabilityInfo.java
@@ -0,0 +1,648 @@
+/*
+ * 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.ims.rcs.uce.presence.publish;
+
+import static android.telephony.ims.RcsContactUceCapability.SOURCE_TYPE_CACHED;
+
+import android.content.Context;
+import android.net.Uri;
+import android.telecom.TelecomManager;
+import android.telephony.AccessNetworkConstants;
+import android.telephony.ims.ImsRegistrationAttributes;
+import android.telephony.ims.RcsContactPresenceTuple;
+import android.telephony.ims.RcsContactPresenceTuple.ServiceCapabilities;
+import android.telephony.ims.RcsContactUceCapability;
+import android.telephony.ims.RcsContactUceCapability.CapabilityMechanism;
+import android.telephony.ims.RcsContactUceCapability.OptionsBuilder;
+import android.telephony.ims.RcsContactUceCapability.PresenceBuilder;
+import android.telephony.ims.feature.MmTelFeature;
+import android.telephony.ims.feature.MmTelFeature.MmTelCapabilities;
+import android.util.IndentingPrintWriter;
+import android.util.ArraySet;
+import android.util.LocalLog;
+import android.util.Log;
+
+import com.android.ims.rcs.uce.util.FeatureTags;
+import com.android.ims.rcs.uce.util.UceUtils;
+
+import java.io.PrintWriter;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * Stores the device's capabilities information.
+ */
+public class DeviceCapabilityInfo {
+ private static final String LOG_TAG = UceUtils.getLogPrefix() + "DeviceCapabilityInfo";
+
+ private final int mSubId;
+
+ private final LocalLog mLocalLog = new LocalLog(UceUtils.LOG_SIZE);
+
+ // FT overrides to add to the IMS registration, which will be added to the existing
+ // capabilities.
+ private final Set<String> mOverrideAddFeatureTags = new ArraySet<>();
+
+ // FT overrides to remove from the existing IMS registration, which will remove the related
+ // capabilities.
+ private final Set<String> mOverrideRemoveFeatureTags = new ArraySet<>();
+
+ // Tracks capability status based on the IMS registration.
+ private PublishServiceDescTracker mServiceCapRegTracker;
+
+ // The feature tags associated with the last IMS registration update.
+ private Set<String> mLastRegistrationFeatureTags = Collections.emptySet();
+ // The feature tags associated with the last IMS registration update, which also include
+ // overrides
+ private Set<String> mLastRegistrationOverrideFeatureTags = Collections.emptySet();
+
+ // The mmtel feature is registered or not
+ private boolean mMmtelRegistered;
+
+ // The network type which ims mmtel registers on.
+ private int mMmtelNetworkRegType;
+
+ // The list of the mmtel associated uris
+ private List<Uri> mMmtelAssociatedUris = Collections.emptyList();
+
+ // The rcs feature is registered or not
+ private boolean mRcsRegistered;
+
+ // The list of the rcs associated uris
+ private List<Uri> mRcsAssociatedUris = Collections.emptyList();
+
+ // Whether or not presence is reported as capable
+ private boolean mPresenceCapable;
+
+ // The network type which ims rcs registers on.
+ private int mRcsNetworkRegType;
+
+ // The MMTel capabilities of this subscription Id
+ private MmTelFeature.MmTelCapabilities mMmTelCapabilities;
+
+ // Whether the settings are changed or not
+ private int mTtyPreferredMode;
+ private boolean mAirplaneMode;
+ private boolean mMobileData;
+ private boolean mVtSetting;
+
+ public DeviceCapabilityInfo(int subId, String[] capToRegistrationMap) {
+ mSubId = subId;
+ mServiceCapRegTracker = PublishServiceDescTracker.fromCarrierConfig(capToRegistrationMap);
+ reset();
+ }
+
+ /**
+ * Reset all the status.
+ */
+ public synchronized void reset() {
+ logd("reset");
+ mMmtelRegistered = false;
+ mMmtelNetworkRegType = AccessNetworkConstants.TRANSPORT_TYPE_INVALID;
+ mRcsRegistered = false;
+ mRcsNetworkRegType = AccessNetworkConstants.TRANSPORT_TYPE_INVALID;
+ mTtyPreferredMode = TelecomManager.TTY_MODE_OFF;
+ mAirplaneMode = false;
+ mMobileData = true;
+ mVtSetting = true;
+ mMmTelCapabilities = new MmTelCapabilities();
+ mMmtelAssociatedUris = Collections.EMPTY_LIST;
+ mRcsAssociatedUris = Collections.EMPTY_LIST;
+ }
+
+ /**
+ * Update the capability registration tracker feature tag override mapping.
+ * @return if true, this has caused a change in the Feature Tags associated with the device
+ * and a new PUBLISH should be generated.
+ */
+ public synchronized boolean updateCapabilityRegistrationTrackerMap(String[] newMap) {
+ Set<String> oldTags = mServiceCapRegTracker.copyRegistrationFeatureTags();
+ mServiceCapRegTracker = PublishServiceDescTracker.fromCarrierConfig(newMap);
+ mServiceCapRegTracker.updateImsRegistration(mLastRegistrationOverrideFeatureTags);
+ boolean changed = !oldTags.equals(mServiceCapRegTracker.copyRegistrationFeatureTags());
+ if (changed) logi("Carrier Config Change resulted in associated FT list change");
+ return changed;
+ }
+
+ public synchronized boolean isImsRegistered() {
+ return mMmtelRegistered;
+ }
+
+ /**
+ * Update the status that IMS MMTEL is registered.
+ */
+ public synchronized void updateImsMmtelRegistered(int type) {
+ StringBuilder builder = new StringBuilder();
+ builder.append("IMS MMTEL registered: original state=").append(mMmtelRegistered)
+ .append(", changes type from ").append(mMmtelNetworkRegType)
+ .append(" to ").append(type);
+ logi(builder.toString());
+
+ if (!mMmtelRegistered) {
+ mMmtelRegistered = true;
+ }
+
+ if (mMmtelNetworkRegType != type) {
+ mMmtelNetworkRegType = type;
+ }
+ }
+
+ /**
+ * Update the status that IMS MMTEL is unregistered.
+ */
+ public synchronized void updateImsMmtelUnregistered() {
+ logi("IMS MMTEL unregistered: original state=" + mMmtelRegistered);
+ if (mMmtelRegistered) {
+ mMmtelRegistered = false;
+ }
+ mMmtelNetworkRegType = AccessNetworkConstants.TRANSPORT_TYPE_INVALID;
+ }
+
+ /**
+ * Update the MMTel associated URIs which are provided by the IMS service.
+ */
+ public synchronized void updateMmTelAssociatedUri(Uri[] uris) {
+ int originalSize = mMmtelAssociatedUris.size();
+ if (uris != null) {
+ mMmtelAssociatedUris = Arrays.stream(uris)
+ .filter(Objects::nonNull)
+ .collect(Collectors.toList());
+ } else {
+ mMmtelAssociatedUris.clear();
+ }
+ int currentSize = mMmtelAssociatedUris.size();
+ logd("updateMmTelAssociatedUri: size from " + originalSize + " to " + currentSize);
+ }
+
+ /**
+ * Get the MMTEL associated URI. When there are multiple uris in the list, take the first uri.
+ * Return null if the list of the MMTEL associated uri is empty.
+ */
+ public synchronized Uri getMmtelAssociatedUri() {
+ if (!mMmtelAssociatedUris.isEmpty()) {
+ return mMmtelAssociatedUris.get(0);
+ }
+ return null;
+ }
+
+ /**
+ * Update the status that IMS RCS is registered.
+ * @return true if the IMS registration status changed, false if it did not.
+ */
+ public synchronized boolean updateImsRcsRegistered(ImsRegistrationAttributes attr) {
+ StringBuilder builder = new StringBuilder();
+ builder.append("IMS RCS registered: original state=").append(mRcsRegistered)
+ .append(", changes type from ").append(mRcsNetworkRegType)
+ .append(" to ").append(attr.getTransportType());
+ logi(builder.toString());
+
+ boolean changed = false;
+ if (!mRcsRegistered) {
+ mRcsRegistered = true;
+ changed = true;
+ }
+
+ if (mRcsNetworkRegType != attr.getTransportType()) {
+ mRcsNetworkRegType = attr.getTransportType();
+ changed = true;
+ }
+
+ mLastRegistrationFeatureTags = attr.getFeatureTags();
+ changed |= updateRegistration(mLastRegistrationFeatureTags);
+
+ return changed;
+ }
+
+ /**
+ * Update the status that IMS RCS is unregistered.
+ */
+ public synchronized boolean updateImsRcsUnregistered() {
+ logi("IMS RCS unregistered: original state=" + mRcsRegistered);
+ boolean changed = false;
+ if (mRcsRegistered) {
+ mRcsRegistered = false;
+ changed = true;
+ }
+ mRcsNetworkRegType = AccessNetworkConstants.TRANSPORT_TYPE_INVALID;
+ return changed;
+ }
+
+ /**
+ * Update the RCS associated URIs which is provided by the IMS service.
+ */
+ public synchronized void updateRcsAssociatedUri(Uri[] uris) {
+ int originalSize = mRcsAssociatedUris.size();
+ if (uris != null) {
+ mRcsAssociatedUris = Arrays.stream(uris)
+ .filter(Objects::nonNull)
+ .collect(Collectors.toList());
+ } else {
+ mRcsAssociatedUris.clear();
+ }
+ int currentSize = mRcsAssociatedUris.size();
+ logd("updateRcsAssociatedUri: size from " + originalSize + " to " + currentSize);
+ }
+
+ /**
+ * Get the RCS associated URI. When there are multiple uris in the list, take the first uri.
+ * Return null if the list of the RCS associated uri is empty.
+ */
+ public synchronized Uri getRcsAssociatedUri() {
+ if (!mRcsAssociatedUris.isEmpty()) {
+ return mRcsAssociatedUris.get(0);
+ }
+ return null;
+ }
+
+ /**
+ * Get the IMS associated URI. It will first get the uri of MMTEL if it is not empty, otherwise
+ * it will try to get the uri of RCS. The null will be returned if both MMTEL and RCS are empty.
+ */
+ public synchronized Uri getImsAssociatedUri() {
+ if (!mRcsAssociatedUris.isEmpty()) {
+ return mRcsAssociatedUris.get(0);
+ } else if (!mMmtelAssociatedUris.isEmpty()) {
+ return mMmtelAssociatedUris.get(0);
+ } else {
+ return null;
+ }
+ }
+
+ public synchronized boolean addRegistrationOverrideCapabilities(Set<String> featureTags) {
+ logd("override - add: " + featureTags);
+ mOverrideRemoveFeatureTags.removeAll(featureTags);
+ mOverrideAddFeatureTags.addAll(featureTags);
+ // Call with the last feature tags so that the new ones will be potentially picked up.
+ return updateRegistration(mLastRegistrationFeatureTags);
+ };
+
+ public synchronized boolean removeRegistrationOverrideCapabilities(Set<String> featureTags) {
+ logd("override - remove: " + featureTags);
+ mOverrideAddFeatureTags.removeAll(featureTags);
+ mOverrideRemoveFeatureTags.addAll(featureTags);
+ // Call with the last feature tags so that the new ones will be potentially picked up.
+ return updateRegistration(mLastRegistrationFeatureTags);
+ };
+
+ public synchronized boolean clearRegistrationOverrideCapabilities() {
+ logd("override - clear");
+ mOverrideAddFeatureTags.clear();
+ mOverrideRemoveFeatureTags.clear();
+ // Call with the last feature tags so that base tags will be restored
+ return updateRegistration(mLastRegistrationFeatureTags);
+ };
+
+ /**
+ * Update the IMS registration tracked by the PublishServiceDescTracker if needed.
+ * @return true if the registration changed, else otherwise.
+ */
+ private boolean updateRegistration(Set<String> baseTags) {
+ Set<String> updatedTags = updateImsRegistrationFeatureTags(baseTags);
+ if (!mLastRegistrationOverrideFeatureTags.equals(updatedTags)) {
+ mLastRegistrationOverrideFeatureTags = updatedTags;
+ mServiceCapRegTracker.updateImsRegistration(updatedTags);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Combine IMS registration with overrides to produce a new feature tag Set.
+ * @return true if the IMS registration changed, false otherwise.
+ */
+ private synchronized Set<String> updateImsRegistrationFeatureTags(Set<String> featureTags) {
+ Set<String> tags = new ArraySet<>(featureTags);
+ tags.addAll(mOverrideAddFeatureTags);
+ tags.removeAll(mOverrideRemoveFeatureTags);
+ return tags;
+ }
+
+ /**
+ * Update the TTY preferred mode.
+ * @return {@code true} if tty preferred mode is changed, {@code false} otherwise.
+ */
+ public synchronized boolean updateTtyPreferredMode(int ttyMode) {
+ if (mTtyPreferredMode != ttyMode) {
+ logd("TTY preferred mode changes from " + mTtyPreferredMode + " to " + ttyMode);
+ mTtyPreferredMode = ttyMode;
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Update airplane mode state.
+ * @return {@code true} if the airplane mode is changed, {@code false} otherwise.
+ */
+ public synchronized boolean updateAirplaneMode(boolean state) {
+ if (mAirplaneMode != state) {
+ logd("Airplane mode changes from " + mAirplaneMode + " to " + state);
+ mAirplaneMode = state;
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Update mobile data setting.
+ * @return {@code true} if the mobile data setting is changed, {@code false} otherwise.
+ */
+ public synchronized boolean updateMobileData(boolean mobileData) {
+ if (mMobileData != mobileData) {
+ logd("Mobile data changes from " + mMobileData + " to " + mobileData);
+ mMobileData = mobileData;
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Update VT setting.
+ * @return {@code true} if vt setting is changed, {@code false}.otherwise.
+ */
+ public synchronized boolean updateVtSetting(boolean vtSetting) {
+ if (mVtSetting != vtSetting) {
+ logd("VT setting changes from " + mVtSetting + " to " + vtSetting);
+ mVtSetting = vtSetting;
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Update the MMTEL capabilities if the capabilities is changed.
+ * @return {@code true} if the mmtel capabilities are changed, {@code false} otherwise.
+ */
+ public synchronized boolean updateMmtelCapabilitiesChanged(MmTelCapabilities capabilities) {
+ if (capabilities == null) {
+ return false;
+ }
+ boolean oldVolteAvailable = isVolteAvailable(mMmtelNetworkRegType, mMmTelCapabilities);
+ boolean oldVoWifiAvailable = isVoWifiAvailable(mMmtelNetworkRegType, mMmTelCapabilities);
+ boolean oldVtAvailable = isVtAvailable(mMmtelNetworkRegType, mMmTelCapabilities);
+ boolean oldViWifiAvailable = isViWifiAvailable(mMmtelNetworkRegType, mMmTelCapabilities);
+ boolean oldCallComposerAvailable = isCallComposerAvailable(mMmTelCapabilities);
+
+ boolean volteAvailable = isVolteAvailable(mMmtelNetworkRegType, capabilities);
+ boolean voWifiAvailable = isVoWifiAvailable(mMmtelNetworkRegType, capabilities);
+ boolean vtAvailable = isVtAvailable(mMmtelNetworkRegType, capabilities);
+ boolean viWifiAvailable = isViWifiAvailable(mMmtelNetworkRegType, capabilities);
+ boolean callComposerAvailable = isCallComposerAvailable(capabilities);
+
+ logd("updateMmtelCapabilitiesChanged: from " + mMmTelCapabilities + " to " + capabilities);
+
+ // Update to the new mmtel capabilities
+ mMmTelCapabilities = deepCopyCapabilities(capabilities);
+
+ if (oldVolteAvailable != volteAvailable
+ || oldVoWifiAvailable != voWifiAvailable
+ || oldVtAvailable != vtAvailable
+ || oldViWifiAvailable != viWifiAvailable
+ || oldCallComposerAvailable != callComposerAvailable) {
+ return true;
+ }
+ return false;
+ }
+
+ public synchronized void updatePresenceCapable(boolean isCapable) {
+ mPresenceCapable = isCapable;
+ }
+
+ public synchronized boolean isPresenceCapable() {
+ return mPresenceCapable;
+ }
+
+ private boolean isVolteAvailable(int networkRegType, MmTelCapabilities capabilities) {
+ return (networkRegType == AccessNetworkConstants.TRANSPORT_TYPE_WWAN)
+ && capabilities.isCapable(MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VOICE);
+ }
+
+ private boolean isVoWifiAvailable(int networkRegType, MmTelCapabilities capabilities) {
+ return (networkRegType == AccessNetworkConstants.TRANSPORT_TYPE_WLAN)
+ && capabilities.isCapable(MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VOICE);
+ }
+
+ private boolean isVtAvailable(int networkRegType, MmTelCapabilities capabilities) {
+ return (networkRegType == AccessNetworkConstants.TRANSPORT_TYPE_WWAN)
+ && capabilities.isCapable(MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VIDEO);
+ }
+
+ private boolean isViWifiAvailable(int networkRegType, MmTelCapabilities capabilities) {
+ return (networkRegType == AccessNetworkConstants.TRANSPORT_TYPE_WLAN)
+ && capabilities.isCapable(MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VIDEO);
+ }
+
+ private boolean isCallComposerAvailable(MmTelCapabilities capabilities) {
+ return capabilities.isCapable(
+ MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_CALL_COMPOSER);
+ }
+
+ /**
+ * Get the device's capabilities.
+ */
+ public synchronized RcsContactUceCapability getDeviceCapabilities(
+ @CapabilityMechanism int mechanism, Context context) {
+ switch (mechanism) {
+ case RcsContactUceCapability.CAPABILITY_MECHANISM_PRESENCE:
+ return getPresenceCapabilities(context);
+ case RcsContactUceCapability.CAPABILITY_MECHANISM_OPTIONS:
+ return getOptionsCapabilities(context);
+ default:
+ logw("getDeviceCapabilities: invalid mechanism " + mechanism);
+ return null;
+ }
+ }
+
+ // Get the device's capabilities with the PRESENCE mechanism.
+ private RcsContactUceCapability getPresenceCapabilities(Context context) {
+ Uri uri = PublishUtils.getDeviceContactUri(context, mSubId, this);
+ if (uri == null) {
+ logw("getPresenceCapabilities: uri is empty");
+ return null;
+ }
+ Set<ServiceDescription> capableFromReg =
+ mServiceCapRegTracker.copyRegistrationCapabilities();
+
+ PresenceBuilder presenceBuilder = new PresenceBuilder(uri,
+ RcsContactUceCapability.SOURCE_TYPE_CACHED,
+ RcsContactUceCapability.REQUEST_RESULT_FOUND);
+ // RCS presence tag (added to all presence documents)
+ ServiceDescription presDescription = getCustomizedDescription(
+ ServiceDescription.SERVICE_DESCRIPTION_PRESENCE, capableFromReg);
+ addCapability(presenceBuilder, presDescription.getTupleBuilder(), uri);
+ capableFromReg.remove(presDescription);
+
+ // mmtel
+ ServiceDescription voiceDescription = getCustomizedDescription(
+ ServiceDescription.SERVICE_DESCRIPTION_MMTEL_VOICE, capableFromReg);
+ ServiceDescription vtDescription = getCustomizedDescription(
+ ServiceDescription.SERVICE_DESCRIPTION_MMTEL_VOICE_VIDEO, capableFromReg);
+ ServiceDescription descToUse = (hasVolteCapability() && hasVtCapability()) ?
+ vtDescription : voiceDescription;
+ ServiceCapabilities servCaps = new ServiceCapabilities.Builder(
+ hasVolteCapability(), hasVtCapability())
+ .addSupportedDuplexMode(ServiceCapabilities.DUPLEX_MODE_FULL).build();
+ addCapability(presenceBuilder, descToUse.getTupleBuilder()
+ .setServiceCapabilities(servCaps), uri);
+ capableFromReg.remove(voiceDescription);
+ capableFromReg.remove(vtDescription);
+
+ // call composer via mmtel
+ ServiceDescription composerDescription = getCustomizedDescription(
+ ServiceDescription.SERVICE_DESCRIPTION_CALL_COMPOSER_MMTEL, capableFromReg);
+ if (hasCallComposerCapability()) {
+ addCapability(presenceBuilder, composerDescription.getTupleBuilder(), uri);
+ }
+ capableFromReg.remove(composerDescription);
+
+ // External features can only be found using registration states from other components.
+ // Count these features as capable and include in PIDF XML if they are registered.
+ for (ServiceDescription capability : capableFromReg) {
+ addCapability(presenceBuilder, capability.getTupleBuilder(), uri);
+ }
+
+ return presenceBuilder.build();
+ }
+
+ /**
+ * Search the refSet for the ServiceDescription that matches the service-id && version and
+ * return that or return the reference if there is no match.
+ */
+ private ServiceDescription getCustomizedDescription(ServiceDescription reference,
+ Set<ServiceDescription> refSet) {
+ return refSet.stream().filter(s -> s.serviceId.equals(reference.serviceId)
+ && s.version.equals(reference.version)).findFirst().orElse(reference);
+ }
+
+ // Get the device's capabilities with the OPTIONS mechanism.
+ private RcsContactUceCapability getOptionsCapabilities(Context context) {
+ Uri uri = PublishUtils.getDeviceContactUri(context, mSubId, this);
+ if (uri == null) {
+ logw("getOptionsCapabilities: uri is empty");
+ return null;
+ }
+
+ Set<String> capableFromReg = mServiceCapRegTracker.copyRegistrationFeatureTags();
+
+ OptionsBuilder optionsBuilder = new OptionsBuilder(uri, SOURCE_TYPE_CACHED);
+ optionsBuilder.setRequestResult(RcsContactUceCapability.REQUEST_RESULT_FOUND);
+ FeatureTags.addFeatureTags(optionsBuilder, hasVolteCapability(), hasVtCapability(),
+ isPresenceCapable(), hasCallComposerCapability(), capableFromReg);
+ return optionsBuilder.build();
+ }
+
+ private void addCapability(RcsContactUceCapability.PresenceBuilder presenceBuilder,
+ RcsContactPresenceTuple.Builder tupleBuilder, Uri contactUri) {
+ presenceBuilder.addCapabilityTuple(tupleBuilder.setContactUri(contactUri).build());
+ }
+
+ // Check if the device has the VoLTE capability
+ private synchronized boolean hasVolteCapability() {
+ return overrideCapability(FeatureTags.FEATURE_TAG_MMTEL, mMmTelCapabilities != null
+ && mMmTelCapabilities.isCapable(MmTelCapabilities.CAPABILITY_TYPE_VOICE));
+ }
+
+ // Check if the device has the VT capability
+ private synchronized boolean hasVtCapability() {
+ return overrideCapability(FeatureTags.FEATURE_TAG_VIDEO, mMmTelCapabilities != null
+ && mMmTelCapabilities.isCapable(MmTelCapabilities.CAPABILITY_TYPE_VIDEO));
+ }
+
+ // Check if the device has the Call Composer capability
+ private synchronized boolean hasCallComposerCapability() {
+ return overrideCapability(FeatureTags.FEATURE_TAG_CALL_COMPOSER_VIA_TELEPHONY,
+ mMmTelCapabilities != null && mMmTelCapabilities.isCapable(
+ MmTelCapabilities.CAPABILITY_TYPE_CALL_COMPOSER));
+ }
+
+ /**
+ * @return the overridden value for the provided feature tag or the original capability if there
+ * is no override.
+ */
+ private synchronized boolean overrideCapability(String featureTag, boolean originalCap) {
+ if (mOverrideRemoveFeatureTags.contains(featureTag)) {
+ return false;
+ }
+
+ if (mOverrideAddFeatureTags.contains(featureTag)) {
+ return true;
+ }
+
+ return originalCap;
+ }
+
+ private synchronized MmTelCapabilities deepCopyCapabilities(MmTelCapabilities capabilities) {
+ MmTelCapabilities mmTelCapabilities = new MmTelCapabilities();
+ if (capabilities.isCapable(MmTelCapabilities.CAPABILITY_TYPE_VOICE)) {
+ mmTelCapabilities.addCapabilities(MmTelCapabilities.CAPABILITY_TYPE_VOICE);
+ }
+ if (capabilities.isCapable(MmTelCapabilities.CAPABILITY_TYPE_VIDEO)) {
+ mmTelCapabilities.addCapabilities(MmTelCapabilities.CAPABILITY_TYPE_VIDEO);
+ }
+ if (capabilities.isCapable(MmTelCapabilities.CAPABILITY_TYPE_UT)) {
+ mmTelCapabilities.addCapabilities(MmTelCapabilities.CAPABILITY_TYPE_UT);
+ }
+ if (capabilities.isCapable(MmTelCapabilities.CAPABILITY_TYPE_SMS)) {
+ mmTelCapabilities.addCapabilities(MmTelCapabilities.CAPABILITY_TYPE_SMS);
+ }
+ if (capabilities.isCapable(MmTelCapabilities.CAPABILITY_TYPE_CALL_COMPOSER)) {
+ mmTelCapabilities.addCapabilities(MmTelCapabilities.CAPABILITY_TYPE_CALL_COMPOSER);
+ }
+ return mmTelCapabilities;
+ }
+
+ private void logd(String log) {
+ Log.d(LOG_TAG, getLogPrefix().append(log).toString());
+ mLocalLog.log("[D] " + log);
+ }
+
+ private void logi(String log) {
+ Log.i(LOG_TAG, getLogPrefix().append(log).toString());
+ mLocalLog.log("[I] " + log);
+ }
+
+ private void logw(String log) {
+ Log.w(LOG_TAG, getLogPrefix().append(log).toString());
+ mLocalLog.log("[W] " + log);
+ }
+
+ private StringBuilder getLogPrefix() {
+ StringBuilder builder = new StringBuilder("[");
+ builder.append(mSubId);
+ builder.append("] ");
+ return builder;
+ }
+
+ public void dump(PrintWriter printWriter) {
+ IndentingPrintWriter pw = new IndentingPrintWriter(printWriter, " ");
+ pw.println("DeviceCapabilityInfo :");
+ pw.increaseIndent();
+
+ mServiceCapRegTracker.dump(pw);
+
+ pw.println("Log:");
+ pw.increaseIndent();
+ mLocalLog.dump(pw);
+ pw.decreaseIndent();
+
+ pw.decreaseIndent();
+ }
+}
diff --git a/src/java/com/android/ims/rcs/uce/presence/publish/DeviceCapabilityListener.java b/src/java/com/android/ims/rcs/uce/presence/publish/DeviceCapabilityListener.java
new file mode 100644
index 00000000..e881ae0c
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/presence/publish/DeviceCapabilityListener.java
@@ -0,0 +1,736 @@
+/*
+ * 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.ims.rcs.uce.presence.publish;
+
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import android.provider.Settings;
+import android.provider.Telephony;
+import android.telecom.TelecomManager;
+import android.telephony.AccessNetworkConstants;
+import android.telephony.AccessNetworkConstants.TransportType;
+import android.telephony.ims.ImsException;
+import android.telephony.ims.ImsManager;
+import android.telephony.ims.ImsMmTelManager;
+import android.telephony.ims.ImsMmTelManager.CapabilityCallback;
+import android.telephony.ims.ImsRcsManager;
+import android.telephony.ims.ImsReasonInfo;
+import android.telephony.ims.ImsRegistrationAttributes;
+import android.telephony.ims.ProvisioningManager;
+import android.telephony.ims.RegistrationManager;
+import android.telephony.ims.feature.MmTelFeature.MmTelCapabilities;
+import android.util.IndentingPrintWriter;
+import android.util.LocalLog;
+import android.util.Log;
+
+import com.android.ims.rcs.uce.presence.publish.PublishController.PublishControllerCallback;
+import com.android.ims.rcs.uce.presence.publish.PublishController.PublishTriggerType;
+import com.android.ims.rcs.uce.util.UceUtils;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.telephony.util.HandlerExecutor;
+
+import java.io.PrintWriter;
+import java.util.Objects;
+
+/**
+ * Listen to the device changes and notify the PublishController to publish the device's
+ * capabilities to the Presence server.
+ */
+public class DeviceCapabilityListener {
+
+ private static final String LOG_TAG = UceUtils.getLogPrefix() + "DeviceCapListener";
+
+ private static final long REGISTER_IMS_CHANGED_DELAY = 15000L; // 15 seconds
+
+ /**
+ * Used to inject ImsMmTelManager instances for testing.
+ */
+ @VisibleForTesting
+ public interface ImsMmTelManagerFactory {
+ ImsMmTelManager getImsMmTelManager(int subId);
+ }
+
+ /**
+ * Used to inject ImsRcsManager instances for testing.
+ */
+ @VisibleForTesting
+ public interface ImsRcsManagerFactory {
+ ImsRcsManager getImsRcsManager(int subId);
+ }
+
+ /**
+ * Used to inject ProvisioningManager instances for testing.
+ */
+ @VisibleForTesting
+ public interface ProvisioningManagerFactory {
+ ProvisioningManager getProvisioningManager(int subId);
+ }
+
+ /*
+ * Handle registering IMS callback and triggering the publish request because of the
+ * capabilities changed.
+ */
+ private class DeviceCapabilityHandler extends Handler {
+ private static final long TRIGGER_PUBLISH_REQUEST_DELAY_MS = 500L;
+
+ private static final int EVENT_REGISTER_IMS_CONTENT_CHANGE = 1;
+ private static final int EVENT_UNREGISTER_IMS_CHANGE = 2;
+ private static final int EVENT_REQUEST_PUBLISH = 3;
+
+ DeviceCapabilityHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ logd("handleMessage: " + msg.what);
+ if (mIsDestroyed) return;
+ switch (msg.what) {
+ case EVENT_REGISTER_IMS_CONTENT_CHANGE:
+ registerImsProvisionCallback();
+ break;
+ case EVENT_UNREGISTER_IMS_CHANGE:
+ unregisterImsProvisionCallback();
+ break;
+ case EVENT_REQUEST_PUBLISH:
+ int triggerType = msg.arg1;
+ mCallback.requestPublishFromInternal(triggerType);
+ break;
+ }
+ }
+
+ public void sendRegisterImsContentChangedMessage(long delay) {
+ // Remove the existing message and send a new one with the delayed time.
+ removeMessages(EVENT_REGISTER_IMS_CONTENT_CHANGE);
+ Message msg = obtainMessage(EVENT_REGISTER_IMS_CONTENT_CHANGE);
+ sendMessageDelayed(msg, delay);
+ }
+
+ public void removeRegisterImsContentChangedMessage() {
+ removeMessages(EVENT_REGISTER_IMS_CONTENT_CHANGE);
+ }
+
+ public void sendUnregisterImsCallbackMessage() {
+ removeMessages(EVENT_REGISTER_IMS_CONTENT_CHANGE);
+ sendEmptyMessage(EVENT_UNREGISTER_IMS_CHANGE);
+ }
+
+ public void sendTriggeringPublishMessage(@PublishTriggerType int type) {
+ logd("sendTriggeringPublishMessage: type=" + type);
+ // Remove the existing message and resend a new message.
+ removeMessages(EVENT_REQUEST_PUBLISH);
+ Message message = obtainMessage();
+ message.what = EVENT_REQUEST_PUBLISH;
+ message.arg1 = type;
+ sendMessageDelayed(message, TRIGGER_PUBLISH_REQUEST_DELAY_MS);
+ }
+ }
+
+ private final int mSubId;
+ private final Context mContext;
+ private final LocalLog mLocalLog = new LocalLog(UceUtils.LOG_SIZE);
+ private volatile boolean mInitialized;
+ private volatile boolean mIsDestroyed;
+ private volatile boolean mIsRcsConnected;
+ private volatile boolean mIsImsCallbackRegistered;
+
+ // The callback to trigger the internal publish request
+ private final PublishControllerCallback mCallback;
+ private final DeviceCapabilityInfo mCapabilityInfo;
+ private final HandlerThread mHandlerThread;
+ private final DeviceCapabilityHandler mHandler;
+ private final HandlerExecutor mHandlerExecutor;
+
+ private ImsMmTelManager mImsMmTelManager;
+ private ImsMmTelManagerFactory mImsMmTelManagerFactory = (subId) -> getImsMmTelManager(subId);
+
+ private ImsRcsManager mImsRcsManager;
+ private ImsRcsManagerFactory mImsRcsManagerFactory = (subId) -> getImsRcsManager(subId);
+
+ private ProvisioningManager mProvisioningManager;
+ private ProvisioningManagerFactory mProvisioningMgrFactory = (subId)
+ -> ProvisioningManager.createForSubscriptionId(subId);
+
+ private ContentObserver mMobileDataObserver = null;
+ private ContentObserver mSimInfoContentObserver = null;
+
+ private final Object mLock = new Object();
+
+ public DeviceCapabilityListener(Context context, int subId, DeviceCapabilityInfo info,
+ PublishControllerCallback callback) {
+ mSubId = subId;
+ logi("create");
+
+ mContext = context;
+ mCallback = callback;
+ mCapabilityInfo = info;
+ mInitialized = false;
+
+ mHandlerThread = new HandlerThread("DeviceCapListenerThread");
+ mHandlerThread.start();
+ mHandler = new DeviceCapabilityHandler(mHandlerThread.getLooper());
+ mHandlerExecutor = new HandlerExecutor(mHandler);
+ }
+
+ /**
+ * Turn on the device capabilities changed listener
+ */
+ public void initialize() {
+ synchronized (mLock) {
+ if (mIsDestroyed) {
+ logw("initialize: This instance is already destroyed");
+ return;
+ }
+ if (mInitialized) return;
+
+ logi("initialize");
+ mImsMmTelManager = mImsMmTelManagerFactory.getImsMmTelManager(mSubId);
+ mImsRcsManager = mImsRcsManagerFactory.getImsRcsManager(mSubId);
+ mProvisioningManager = mProvisioningMgrFactory.getProvisioningManager(mSubId);
+ registerReceivers();
+ registerImsProvisionCallback();
+
+ mInitialized = true;
+ }
+ }
+
+ // The RcsFeature has been connected to the framework
+ public void onRcsConnected() {
+ mIsRcsConnected = true;
+ mHandler.sendRegisterImsContentChangedMessage(0L);
+ }
+
+ // The framework has lost the binding to the RcsFeature.
+ public void onRcsDisconnected() {
+ mIsRcsConnected = false;
+ mHandler.sendUnregisterImsCallbackMessage();
+ }
+
+ /**
+ * Notify the instance is destroyed
+ */
+ public void onDestroy() {
+ logi("onDestroy");
+ mIsDestroyed = true;
+ synchronized (mLock) {
+ if (!mInitialized) return;
+ logi("turnOffListener");
+ mInitialized = false;
+ unregisterReceivers();
+ unregisterImsProvisionCallback();
+ mHandlerThread.quit();
+ }
+ }
+
+ /*
+ * Register receivers to listen to the data changes.
+ */
+ private void registerReceivers() {
+ logd("registerReceivers");
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(Intent.ACTION_AIRPLANE_MODE_CHANGED);
+ filter.addAction(TelecomManager.ACTION_TTY_PREFERRED_MODE_CHANGED);
+ mContext.registerReceiver(mReceiver, filter);
+
+ ContentResolver resolver = mContext.getContentResolver();
+ if (resolver != null) {
+ // Listen to the mobile data content changed.
+ resolver.registerContentObserver(
+ Settings.Global.getUriFor(Settings.Global.MOBILE_DATA), false,
+ getMobileDataObserver());
+ // Listen to the SIM info content changed.
+ resolver.registerContentObserver(Telephony.SimInfo.CONTENT_URI, false,
+ getSimInfoContentObserver());
+ }
+ }
+
+ private void unregisterReceivers() {
+ logd("unregisterReceivers");
+ mContext.unregisterReceiver(mReceiver);
+ ContentResolver resolver = mContext.getContentResolver();
+ if (resolver != null) {
+ resolver.unregisterContentObserver(getMobileDataObserver());
+ resolver.unregisterContentObserver(getSimInfoContentObserver());
+ }
+ }
+
+ private void registerImsProvisionCallback() {
+ if (mIsImsCallbackRegistered) {
+ logd("registerImsProvisionCallback: already registered.");
+ return;
+ }
+
+ logd("registerImsProvisionCallback");
+ try {
+ // Register mmtel callback
+ if (mImsMmTelManager != null) {
+ mImsMmTelManager.registerImsRegistrationCallback(mHandlerExecutor,
+ mMmtelRegistrationCallback);
+ mImsMmTelManager.registerMmTelCapabilityCallback(mHandlerExecutor,
+ mMmtelCapabilityCallback);
+ }
+
+ // Register rcs callback
+ if (mImsRcsManager != null) {
+ mImsRcsManager.registerImsRegistrationCallback(mHandlerExecutor,
+ mRcsRegistrationCallback);
+ }
+
+ // Register provisioning changed callback
+ mProvisioningManager.registerProvisioningChangedCallback(mHandlerExecutor,
+ mProvisionChangedCallback);
+
+ // Set the IMS callback is registered.
+ mIsImsCallbackRegistered = true;
+ } catch (ImsException e) {
+ logw("registerImsProvisionCallback error: " + e);
+ // Unregister the callback
+ unregisterImsProvisionCallback();
+
+ // Retry registering IMS callback only when the RCS is connected.
+ if (mIsRcsConnected) {
+ mHandler.sendRegisterImsContentChangedMessage(REGISTER_IMS_CHANGED_DELAY);
+ }
+ }
+ }
+
+ private void unregisterImsProvisionCallback() {
+ logd("unregisterImsProvisionCallback");
+
+ // Clear the registering IMS callback message from the handler thread
+ mHandler.removeRegisterImsContentChangedMessage();
+
+ // Unregister mmtel callback
+ if (mImsMmTelManager != null) {
+ try {
+ mImsMmTelManager.unregisterImsRegistrationCallback(mMmtelRegistrationCallback);
+ } catch (RuntimeException e) {
+ logw("unregister MMTel registration error: " + e.getMessage());
+ }
+ try {
+ mImsMmTelManager.unregisterMmTelCapabilityCallback(mMmtelCapabilityCallback);
+ } catch (RuntimeException e) {
+ logw("unregister MMTel capability error: " + e.getMessage());
+ }
+ }
+
+ // Unregister rcs callback
+ if (mImsRcsManager != null) {
+ try {
+ mImsRcsManager.unregisterImsRegistrationCallback(mRcsRegistrationCallback);
+ } catch (RuntimeException e) {
+ logw("unregister rcs capability error: " + e.getMessage());
+ }
+ }
+
+ try {
+ // Unregister provisioning changed callback
+ mProvisioningManager.unregisterProvisioningChangedCallback(mProvisionChangedCallback);
+ } catch (RuntimeException e) {
+ logw("unregister provisioning callback error: " + e.getMessage());
+ }
+
+ // Clear the IMS callback registered flag.
+ mIsImsCallbackRegistered = false;
+ }
+
+ @VisibleForTesting
+ public final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (intent == null || intent.getAction() == null) return;
+ switch (intent.getAction()) {
+ case TelecomManager.ACTION_TTY_PREFERRED_MODE_CHANGED:
+ int preferredMode = intent.getIntExtra(TelecomManager.EXTRA_TTY_PREFERRED_MODE,
+ TelecomManager.TTY_MODE_OFF);
+ handleTtyPreferredModeChanged(preferredMode);
+ break;
+
+ case Intent.ACTION_AIRPLANE_MODE_CHANGED:
+ boolean airplaneMode = intent.getBooleanExtra("state", false);
+ handleAirplaneModeChanged(airplaneMode);
+ break;
+ }
+ }
+ };
+
+ private ContentObserver getMobileDataObserver() {
+ synchronized (mLock) {
+ if (mMobileDataObserver == null) {
+ mMobileDataObserver = new ContentObserver(new Handler(mHandler.getLooper())) {
+ @Override
+ public void onChange(boolean selfChange) {
+ boolean isEnabled = Settings.Global.getInt(mContext.getContentResolver(),
+ Settings.Global.MOBILE_DATA, 1) == 1;
+ handleMobileDataChanged(isEnabled);
+ }
+ };
+ }
+ return mMobileDataObserver;
+ }
+ }
+
+ private ContentObserver getSimInfoContentObserver() {
+ synchronized (mLock) {
+ if (mSimInfoContentObserver == null) {
+ mSimInfoContentObserver = new ContentObserver(new Handler(mHandler.getLooper())) {
+ @Override
+ public void onChange(boolean selfChange) {
+ if (mImsMmTelManager == null) {
+ logw("SimInfo change error: MmTelManager is null");
+ return;
+ }
+
+ try {
+ boolean isEnabled = mImsMmTelManager.isVtSettingEnabled();
+ handleVtSettingChanged(isEnabled);
+ } catch (RuntimeException e) {
+ logw("SimInfo change error: " + e);
+ }
+ }
+ };
+ }
+ return mSimInfoContentObserver;
+ }
+ }
+
+ private ImsMmTelManager getImsMmTelManager(int subId) {
+ try {
+ ImsManager imsManager = mContext.getSystemService(
+ android.telephony.ims.ImsManager.class);
+ return (imsManager == null) ? null : imsManager.getImsMmTelManager(subId);
+ } catch (IllegalArgumentException e) {
+ logw("getImsMmTelManager error: " + e.getMessage());
+ return null;
+ }
+ }
+
+ private ImsRcsManager getImsRcsManager(int subId) {
+ try {
+ ImsManager imsManager = mContext.getSystemService(
+ android.telephony.ims.ImsManager.class);
+ return (imsManager == null) ? null : imsManager.getImsRcsManager(subId);
+ } catch (IllegalArgumentException e) {
+ logw("getImsRcsManager error: " + e.getMessage());
+ return null;
+ }
+ }
+
+ @VisibleForTesting
+ public final RegistrationManager.RegistrationCallback mRcsRegistrationCallback =
+ new RegistrationManager.RegistrationCallback() {
+ @Override
+ public void onRegistered(ImsRegistrationAttributes attributes) {
+ synchronized (mLock) {
+ logi("onRcsRegistered: " + attributes);
+ if (!mIsImsCallbackRegistered) return;
+ handleImsRcsRegistered(attributes);
+ }
+ }
+
+ @Override
+ public void onUnregistered(ImsReasonInfo info) {
+ synchronized (mLock) {
+ logi("onRcsUnregistered: " + info);
+ if (!mIsImsCallbackRegistered) return;
+ handleImsRcsUnregistered();
+ }
+ }
+
+ @Override
+ public void onSubscriberAssociatedUriChanged(Uri[] uris) {
+ synchronized (mLock) {
+ logi("onRcsSubscriberAssociatedUriChanged");
+ handleRcsSubscriberAssociatedUriChanged(uris, true);
+ }
+ }
+ };
+
+ @VisibleForTesting
+ public final RegistrationManager.RegistrationCallback mMmtelRegistrationCallback =
+ new RegistrationManager.RegistrationCallback() {
+ @Override
+ public void onRegistered(@TransportType int transportType) {
+ synchronized (mLock) {
+ String type = AccessNetworkConstants.transportTypeToString(transportType);
+ logi("onMmTelRegistered: " + type);
+ if (!mIsImsCallbackRegistered) return;
+ handleImsMmtelRegistered(transportType);
+ }
+ }
+
+ @Override
+ public void onUnregistered(ImsReasonInfo info) {
+ synchronized (mLock) {
+ logi("onMmTelUnregistered: " + info);
+ if (!mIsImsCallbackRegistered) return;
+ handleImsMmtelUnregistered();
+ }
+ }
+
+ @Override
+ public void onSubscriberAssociatedUriChanged(Uri[] uris) {
+ synchronized (mLock) {
+ logi("onMmTelSubscriberAssociatedUriChanged");
+ handleMmTelSubscriberAssociatedUriChanged(uris, true);
+ }
+ }
+ };
+
+ @VisibleForTesting
+ public final ImsMmTelManager.CapabilityCallback mMmtelCapabilityCallback =
+ new CapabilityCallback() {
+ @Override
+ public void onCapabilitiesStatusChanged(MmTelCapabilities capabilities) {
+ if (capabilities == null) {
+ logw("onCapabilitiesStatusChanged: parameter is null");
+ return;
+ }
+ synchronized (mLock) {
+ handleMmtelCapabilitiesStatusChanged(capabilities);
+ }
+ }
+ };
+
+ @VisibleForTesting
+ public final ProvisioningManager.Callback mProvisionChangedCallback =
+ new ProvisioningManager.Callback() {
+ @Override
+ public void onProvisioningIntChanged(int item, int value) {
+ logi("onProvisioningIntChanged: item=" + item + ", value=" + value);
+ switch (item) {
+ case ProvisioningManager.KEY_EAB_PROVISIONING_STATUS:
+ case ProvisioningManager.KEY_VOLTE_PROVISIONING_STATUS:
+ case ProvisioningManager.KEY_VT_PROVISIONING_STATUS:
+ handleProvisioningChanged();
+ case ProvisioningManager.KEY_RCS_PUBLISH_SOURCE_THROTTLE_MS:
+ handlePublishThrottleChanged(value);
+ break;
+ }
+ }
+ };
+
+ private void handleTtyPreferredModeChanged(int preferredMode) {
+ boolean isChanged = mCapabilityInfo.updateTtyPreferredMode(preferredMode);
+ logi("TTY preferred mode changed: " + preferredMode + ", isChanged=" + isChanged);
+ if (isChanged) {
+ mHandler.sendTriggeringPublishMessage(
+ PublishController.PUBLISH_TRIGGER_TTY_PREFERRED_CHANGE);
+ }
+ }
+
+ private void handleAirplaneModeChanged(boolean state) {
+ boolean isChanged = mCapabilityInfo.updateAirplaneMode(state);
+ logi("Airplane mode changed: " + state + ", isChanged="+ isChanged);
+ if (isChanged) {
+ mHandler.sendTriggeringPublishMessage(
+ PublishController.PUBLISH_TRIGGER_AIRPLANE_MODE_CHANGE);
+ }
+ }
+
+ private void handleMobileDataChanged(boolean isEnabled) {
+ boolean isChanged = mCapabilityInfo.updateMobileData(isEnabled);
+ logi("Mobile data changed: " + isEnabled + ", isChanged=" + isChanged);
+ if (isChanged) {
+ mHandler.sendTriggeringPublishMessage(
+ PublishController.PUBLISH_TRIGGER_MOBILE_DATA_CHANGE);
+ }
+ }
+
+ private void handleVtSettingChanged(boolean isEnabled) {
+ boolean isChanged = mCapabilityInfo.updateVtSetting(isEnabled);
+ logi("VT setting changed: " + isEnabled + ", isChanged=" + isChanged);
+ if (isChanged) {
+ mHandler.sendTriggeringPublishMessage(
+ PublishController.PUBLISH_TRIGGER_VT_SETTING_CHANGE);
+ }
+ }
+
+ /*
+ * This method is called when the MMTEL is registered.
+ */
+ private void handleImsMmtelRegistered(int imsTransportType) {
+ mCapabilityInfo.updateImsMmtelRegistered(imsTransportType);
+ mHandler.sendTriggeringPublishMessage(
+ PublishController.PUBLISH_TRIGGER_MMTEL_REGISTERED);
+ }
+
+ /*
+ * This method is called when the MMTEL is unregistered.
+ */
+ private void handleImsMmtelUnregistered() {
+ mCapabilityInfo.updateImsMmtelUnregistered();
+ // When the MMTEL is unregistered, the mmtel associated uri should be cleared.
+ handleMmTelSubscriberAssociatedUriChanged(null, false);
+ mHandler.sendTriggeringPublishMessage(
+ PublishController.PUBLISH_TRIGGER_MMTEL_UNREGISTERED);
+ }
+
+ /*
+ * This method is called when the MMTEL associated uri has changed.
+ */
+ private void handleMmTelSubscriberAssociatedUriChanged(Uri[] uris, boolean triggerPublish) {
+ Uri originalUri = mCapabilityInfo.getMmtelAssociatedUri();
+ mCapabilityInfo.updateMmTelAssociatedUri(uris);
+ Uri currentUri = mCapabilityInfo.getMmtelAssociatedUri();
+
+ boolean hasChanged = !(Objects.equals(originalUri, currentUri));
+ logi("handleMmTelSubscriberAssociatedUriChanged: triggerPublish=" + triggerPublish +
+ ", hasChanged=" + hasChanged);
+
+ if (triggerPublish && hasChanged) {
+ mHandler.sendTriggeringPublishMessage(
+ PublishController.PUBLISH_TRIGGER_MMTEL_URI_CHANGE);
+ }
+ }
+
+ private void handleMmtelCapabilitiesStatusChanged(MmTelCapabilities capabilities) {
+ boolean isChanged = mCapabilityInfo.updateMmtelCapabilitiesChanged(capabilities);
+ logi("MMTel capabilities status changed: isChanged=" + isChanged);
+ if (isChanged) {
+ mHandler.sendTriggeringPublishMessage(
+ PublishController.PUBLISH_TRIGGER_MMTEL_CAPABILITY_CHANGE);
+ }
+ }
+
+ /*
+ * This method is called when RCS is registered.
+ */
+ private void handleImsRcsRegistered(ImsRegistrationAttributes attr) {
+ if (mCapabilityInfo.updateImsRcsRegistered(attr)) {
+ mHandler.sendTriggeringPublishMessage(PublishController.PUBLISH_TRIGGER_RCS_REGISTERED);
+ }
+ }
+
+ /*
+ * This method is called when RCS is unregistered.
+ */
+ private void handleImsRcsUnregistered() {
+ boolean hasChanged = mCapabilityInfo.updateImsRcsUnregistered();
+ // When the RCS is unregistered, the rcs associated uri should be cleared.
+ handleRcsSubscriberAssociatedUriChanged(null, false);
+ // Trigger publish if the state has changed.
+ if (hasChanged) {
+ mHandler.sendTriggeringPublishMessage(
+ PublishController.PUBLISH_TRIGGER_RCS_UNREGISTERED);
+ }
+ }
+
+ /*
+ * This method is called when the RCS associated uri has changed.
+ */
+ private void handleRcsSubscriberAssociatedUriChanged(Uri[] uris, boolean triggerPublish) {
+ Uri originalUri = mCapabilityInfo.getRcsAssociatedUri();
+ mCapabilityInfo.updateRcsAssociatedUri(uris);
+ Uri currentUri = mCapabilityInfo.getRcsAssociatedUri();
+
+ boolean hasChanged = !(Objects.equals(originalUri, currentUri));
+ logi("handleRcsSubscriberAssociatedUriChanged: triggerPublish=" + triggerPublish +
+ ", hasChanged=" + hasChanged);
+
+ if (triggerPublish && hasChanged) {
+ mHandler.sendTriggeringPublishMessage(PublishController.PUBLISH_TRIGGER_RCS_URI_CHANGE);
+ }
+ }
+
+ /*
+ * This method is called when the provisioning is changed
+ */
+ private void handleProvisioningChanged() {
+ mHandler.sendTriggeringPublishMessage(
+ PublishController.PUBLISH_TRIGGER_PROVISIONING_CHANGE);
+ }
+
+ /*
+ * Update the publish throttle.
+ */
+ private void handlePublishThrottleChanged(int value) {
+ mCallback.updatePublishThrottle(value);
+ }
+
+ @VisibleForTesting
+ public Handler getHandler() {
+ return mHandler;
+ }
+
+ @VisibleForTesting
+ public void setImsMmTelManagerFactory(ImsMmTelManagerFactory factory) {
+ mImsMmTelManagerFactory = factory;
+ }
+
+ @VisibleForTesting
+ public void setImsRcsManagerFactory(ImsRcsManagerFactory factory) {
+ mImsRcsManagerFactory = factory;
+ }
+
+ @VisibleForTesting
+ public void setProvisioningMgrFactory(ProvisioningManagerFactory factory) {
+ mProvisioningMgrFactory = factory;
+ }
+
+ @VisibleForTesting
+ public void setImsCallbackRegistered(boolean registered) {
+ mIsImsCallbackRegistered = registered;
+ }
+
+ private void logd(String log) {
+ Log.d(LOG_TAG, getLogPrefix().append(log).toString());
+ mLocalLog.log("[D] " + log);
+ }
+
+ private void logi(String log) {
+ Log.i(LOG_TAG, getLogPrefix().append(log).toString());
+ mLocalLog.log("[I] " + log);
+ }
+
+ private void logw(String log) {
+ Log.w(LOG_TAG, getLogPrefix().append(log).toString());
+ mLocalLog.log("[W] " + log);
+ }
+
+ private StringBuilder getLogPrefix() {
+ StringBuilder builder = new StringBuilder("[");
+ builder.append(mSubId);
+ builder.append("] ");
+ return builder;
+ }
+
+ public void dump(PrintWriter printWriter) {
+ IndentingPrintWriter pw = new IndentingPrintWriter(printWriter, " ");
+ pw.println("DeviceCapListener" + "[subId: " + mSubId + "]:");
+ pw.increaseIndent();
+
+ mCapabilityInfo.dump(pw);
+
+ pw.println("Log:");
+ pw.increaseIndent();
+ mLocalLog.dump(pw);
+ pw.decreaseIndent();
+ pw.println("---");
+
+ pw.decreaseIndent();
+ }
+}
diff --git a/src/java/com/android/ims/rcs/uce/presence/publish/PublishController.java b/src/java/com/android/ims/rcs/uce/presence/publish/PublishController.java
new file mode 100644
index 00000000..7ea90473
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/presence/publish/PublishController.java
@@ -0,0 +1,232 @@
+/*
+ * 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.ims.rcs.uce.presence.publish;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.telephony.ims.RcsContactUceCapability;
+import android.telephony.ims.RcsContactUceCapability.CapabilityMechanism;
+import android.telephony.ims.RcsUceAdapter.PublishState;
+import android.telephony.ims.aidl.IRcsUcePublishStateCallback;
+
+import com.android.ims.rcs.uce.ControllerBase;
+
+import java.io.PrintWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.time.Instant;
+import java.util.Set;
+
+/**
+ * The interface related to the PUBLISH request.
+ */
+public interface PublishController extends ControllerBase {
+
+ /** Publish is triggered by the ImsService */
+ int PUBLISH_TRIGGER_SERVICE = 1;
+
+ /** Publish trigger type: retry */
+ int PUBLISH_TRIGGER_RETRY = 2;
+
+ /** Publish trigger type: TTY preferred changes */
+ int PUBLISH_TRIGGER_TTY_PREFERRED_CHANGE = 3;
+
+ /** Publish trigger type: Airplane mode changes */
+ int PUBLISH_TRIGGER_AIRPLANE_MODE_CHANGE = 4;
+
+ /** Publish trigger type: Mobile data changes */
+ int PUBLISH_TRIGGER_MOBILE_DATA_CHANGE = 5;
+
+ /** Publish trigger type: VT setting changes */
+ int PUBLISH_TRIGGER_VT_SETTING_CHANGE = 6;
+
+ /** Publish trigger type: MMTEL registered */
+ int PUBLISH_TRIGGER_MMTEL_REGISTERED = 7;
+
+ /** Publish trigger type: MMTEL unregistered */
+ int PUBLISH_TRIGGER_MMTEL_UNREGISTERED = 8;
+
+ /** Publish trigger type: MMTEL capability changes */
+ int PUBLISH_TRIGGER_MMTEL_CAPABILITY_CHANGE = 9;
+
+ /** Publish trigger type: MMTEL associated uri changes */
+ int PUBLISH_TRIGGER_MMTEL_URI_CHANGE = 10;
+
+ /** Publish trigger type: RCS registered */
+ int PUBLISH_TRIGGER_RCS_REGISTERED = 11;
+
+ /** Publish trigger type: RCS unregistered */
+ int PUBLISH_TRIGGER_RCS_UNREGISTERED = 12;
+
+ /** Publish trigger type: RCS associated uri changes */
+ int PUBLISH_TRIGGER_RCS_URI_CHANGE = 13;
+
+ /** Publish trigger type: provisioning changes */
+ int PUBLISH_TRIGGER_PROVISIONING_CHANGE = 14;
+
+ /**The caps have been overridden for a test*/
+ int PUBLISH_TRIGGER_OVERRIDE_CAPS = 15;
+
+ /** The Carrier Config for the subscription has Changed **/
+ int PUBLISH_TRIGGER_CARRIER_CONFIG_CHANGED = 16;
+
+ @IntDef(value = {
+ PUBLISH_TRIGGER_SERVICE,
+ PUBLISH_TRIGGER_RETRY,
+ PUBLISH_TRIGGER_TTY_PREFERRED_CHANGE,
+ PUBLISH_TRIGGER_AIRPLANE_MODE_CHANGE,
+ PUBLISH_TRIGGER_MOBILE_DATA_CHANGE,
+ PUBLISH_TRIGGER_VT_SETTING_CHANGE,
+ PUBLISH_TRIGGER_MMTEL_REGISTERED,
+ PUBLISH_TRIGGER_MMTEL_UNREGISTERED,
+ PUBLISH_TRIGGER_MMTEL_CAPABILITY_CHANGE,
+ PUBLISH_TRIGGER_MMTEL_URI_CHANGE,
+ PUBLISH_TRIGGER_RCS_REGISTERED,
+ PUBLISH_TRIGGER_RCS_UNREGISTERED,
+ PUBLISH_TRIGGER_RCS_URI_CHANGE,
+ PUBLISH_TRIGGER_PROVISIONING_CHANGE,
+ PUBLISH_TRIGGER_OVERRIDE_CAPS,
+ PUBLISH_TRIGGER_CARRIER_CONFIG_CHANGED
+ }, prefix="PUBLISH_TRIGGER_")
+ @Retention(RetentionPolicy.SOURCE)
+ @interface PublishTriggerType {}
+
+ /**
+ * Receive the callback from the sub-components which interact with PublishController.
+ */
+ interface PublishControllerCallback {
+ /**
+ * Request publish from local.
+ */
+ void requestPublishFromInternal(@PublishTriggerType int type);
+
+ /**
+ * Receive the command error callback of the request from ImsService.
+ */
+ void onRequestCommandError(PublishRequestResponse requestResponse);
+
+ /**
+ * Receive the network response callback fo the request from ImsService.
+ */
+ void onRequestNetworkResp(PublishRequestResponse requestResponse);
+
+ /**
+ * Set the timer to cancel the request. This timer is to prevent taking too long for
+ * waiting the response callback.
+ */
+ void setupRequestCanceledTimer(long taskId, long delay);
+
+ /**
+ * Clear the request canceled timer. This api will be called if the request is finished.
+ */
+ void clearRequestCanceledTimer();
+
+ /**
+ * Update the publish request result.
+ */
+ void updatePublishRequestResult(int publishState, Instant updatedTimestamp, String pidfXml);
+
+ /**
+ * Update the value of the publish throttle.
+ */
+ void updatePublishThrottle(int value);
+
+ /**
+ * Update the device state with the publish request result.
+ */
+ void refreshDeviceState(int SipCode, String reason);
+ }
+
+ /**
+ * Add new feature tags to the Set used to calculate the capabilities in PUBLISH.
+ * <p>
+ * Used for testing ONLY.
+ * @return the new capabilities that will be used for PUBLISH.
+ */
+ RcsContactUceCapability addRegistrationOverrideCapabilities(Set<String> featureTags);
+
+ /**
+ * Remove existing feature tags to the Set used to calculate the capabilities in PUBLISH.
+ * <p>
+ * Used for testing ONLY.
+ * @return the new capabilities that will be used for PUBLISH.
+ */
+ RcsContactUceCapability removeRegistrationOverrideCapabilities(Set<String> featureTags);
+
+ /**
+ * Clear all overrides in the Set used to calculate the capabilities in PUBLISH.
+ * <p>
+ * Used for testing ONLY.
+ * @return the new capabilities that will be used for PUBLISH.
+ */
+ RcsContactUceCapability clearRegistrationOverrideCapabilities();
+
+ /**
+ * @return latest RcsContactUceCapability instance that will be used for PUBLISH.
+ */
+ RcsContactUceCapability getLatestRcsContactUceCapability();
+
+ /**
+ * Retrieve the RCS UCE Publish state.
+ */
+ @PublishState int getUcePublishState();
+
+ /**
+ * @return the last PIDF XML used for publish or {@code null} if the device is not published.
+ */
+ String getLastPidfXml();
+
+ /**
+ * Notify that the device's capabilities have been unpublished from the network.
+ */
+ void onUnpublish();
+
+ /**
+ * Retrieve the device's capabilities.
+ */
+ RcsContactUceCapability getDeviceCapabilities(@CapabilityMechanism int mechanism);
+
+ /**
+ * Publish the device's capabilities to the Presence server.
+ */
+ void requestPublishCapabilitiesFromService(int triggerType);
+
+ /**
+ * Register a {@link PublishStateCallback} to listen to the published state changed.
+ */
+ void registerPublishStateCallback(@NonNull IRcsUcePublishStateCallback c);
+
+ /**
+ * Removes an existing {@link PublishStateCallback}.
+ */
+ void unregisterPublishStateCallback(@NonNull IRcsUcePublishStateCallback c);
+
+ /**
+ * Setup the timer to reset the device state.
+ */
+ void setupResetDeviceStateTimer(long resetAfterSec);
+
+ /**
+ * Clear the reset device state timer.
+ */
+ void clearResetDeviceStateTimer();
+
+ /**
+ * Dump the state of this PublishController to the printWriter.
+ */
+ void dump(PrintWriter printWriter);
+}
diff --git a/src/java/com/android/ims/rcs/uce/presence/publish/PublishControllerImpl.java b/src/java/com/android/ims/rcs/uce/presence/publish/PublishControllerImpl.java
new file mode 100644
index 00000000..e2340ff5
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/presence/publish/PublishControllerImpl.java
@@ -0,0 +1,1080 @@
+/*
+ * 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.ims.rcs.uce.presence.publish;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.os.Build;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.PersistableBundle;
+import android.os.RemoteCallbackList;
+import android.os.RemoteException;
+import android.telephony.CarrierConfigManager;
+import android.telephony.ims.ImsException;
+import android.telephony.ims.RcsContactUceCapability;
+import android.telephony.ims.RcsContactUceCapability.CapabilityMechanism;
+import android.telephony.ims.RcsUceAdapter;
+import android.telephony.ims.RcsUceAdapter.PublishState;
+import android.telephony.ims.aidl.IImsCapabilityCallback;
+import android.telephony.ims.aidl.IRcsUcePublishStateCallback;
+import android.telephony.ims.feature.RcsFeature.RcsImsCapabilities;
+import android.telephony.ims.feature.RcsFeature.RcsImsCapabilities.RcsImsCapabilityFlag;
+import android.util.IndentingPrintWriter;
+import android.util.LocalLog;
+import android.util.Log;
+
+import com.android.ims.RcsFeatureManager;
+import com.android.ims.rcs.uce.UceController;
+import com.android.ims.rcs.uce.UceController.UceControllerCallback;
+import com.android.ims.rcs.uce.UceDeviceState;
+import com.android.ims.rcs.uce.UceDeviceState.DeviceStateResult;
+import com.android.ims.rcs.uce.util.UceUtils;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.os.SomeArgs;
+
+import java.io.PrintWriter;
+import java.lang.ref.WeakReference;
+import java.time.Instant;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * The implementation of PublishController.
+ */
+public class PublishControllerImpl implements PublishController {
+
+ private static final String LOG_TAG = UceUtils.getLogPrefix() + "PublishController";
+
+ /**
+ * Used to inject PublishProcessor instances for testing.
+ */
+ @VisibleForTesting
+ public interface PublishProcessorFactory {
+ PublishProcessor createPublishProcessor(Context context, int subId,
+ DeviceCapabilityInfo capabilityInfo, PublishControllerCallback callback);
+ }
+
+ /**
+ * Used to inject DeviceCapabilityListener instances for testing.
+ */
+ @VisibleForTesting
+ public interface DeviceCapListenerFactory {
+ DeviceCapabilityListener createDeviceCapListener(Context context, int subId,
+ DeviceCapabilityInfo capInfo, PublishControllerCallback callback);
+ }
+
+ private final int mSubId;
+ private final Context mContext;
+ private final LocalLog mLocalLog = new LocalLog(UceUtils.LOG_SIZE);
+ private PublishHandler mPublishHandler;
+ private volatile boolean mIsDestroyedFlag;
+ private volatile boolean mReceivePublishFromService;
+ private volatile RcsFeatureManager mRcsFeatureManager;
+ private final UceControllerCallback mUceCtrlCallback;
+
+ // The capability type that the device is using.
+ private @RcsImsCapabilityFlag int mCapabilityType;
+ // The device publish state
+ private @PublishState int mPublishState;
+ // The timestamp of updating the publish state
+ private Instant mPublishStateUpdatedTime = Instant.now();
+ // The last PIDF XML used in the publish
+ private String mPidfXml;
+
+ // The callbacks to notify publish state changed.
+ private RemoteCallbackList<IRcsUcePublishStateCallback> mPublishStateCallbacks;
+
+ private final Object mPublishStateLock = new Object();
+
+ // The information of the device's capabilities.
+ private DeviceCapabilityInfo mDeviceCapabilityInfo;
+
+ // The processor of publishing device's capabilities.
+ private PublishProcessor mPublishProcessor;
+ private PublishProcessorFactory mPublishProcessorFactory = (context, subId, capInfo, callback)
+ -> new PublishProcessor(context, subId, capInfo, callback);
+
+ // The listener to listen to the device's capabilities changed.
+ private DeviceCapabilityListener mDeviceCapListener;
+ private DeviceCapListenerFactory mDeviceCapListenerFactory = (context, subId, capInfo, callback)
+ -> new DeviceCapabilityListener(context, subId, capInfo, callback);
+
+ // Listen to the RCS availability status changed.
+ private final IImsCapabilityCallback mRcsCapabilitiesCallback =
+ new IImsCapabilityCallback.Stub() {
+ @Override
+ public void onQueryCapabilityConfiguration(
+ int resultCapability, int resultRadioTech, boolean enabled) {
+ }
+ @Override
+ public void onCapabilitiesStatusChanged(@RcsImsCapabilityFlag int capabilities) {
+ logd("onCapabilitiesStatusChanged: " + capabilities);
+ mPublishHandler.sendRcsCapabilitiesStatusChangedMsg(capabilities);
+ }
+ @Override
+ public void onChangeCapabilityConfigurationError(int capability, int radioTech,
+ int reason) {
+ }
+ };
+
+ public PublishControllerImpl(Context context, int subId, UceControllerCallback callback,
+ Looper looper) {
+ mSubId = subId;
+ mContext = context;
+ mUceCtrlCallback = callback;
+ logi("create");
+ initPublishController(looper);
+ }
+
+ @VisibleForTesting
+ public PublishControllerImpl(Context context, int subId, UceControllerCallback c,
+ Looper looper, DeviceCapListenerFactory deviceCapFactory,
+ PublishProcessorFactory processorFactory) {
+ mSubId = subId;
+ mContext = context;
+ mUceCtrlCallback = c;
+ mDeviceCapListenerFactory = deviceCapFactory;
+ mPublishProcessorFactory = processorFactory;
+ initPublishController(looper);
+ }
+
+ private void initPublishController(Looper looper) {
+ mCapabilityType = PublishUtils.getCapabilityType(mContext, mSubId);
+ mPublishState = getInitialPublishState(mCapabilityType);
+ mPublishStateCallbacks = new RemoteCallbackList<>();
+ mPublishHandler = new PublishHandler(this, looper);
+
+ String[] serviceDescFeatureTagMap = getCarrierServiceDescriptionFeatureTagMap();
+ mDeviceCapabilityInfo = new DeviceCapabilityInfo(mSubId, serviceDescFeatureTagMap);
+
+ initPublishProcessor();
+ initDeviceCapabilitiesListener();
+
+ // Turn on the listener to listen to the device changes.
+ mDeviceCapListener.initialize();
+
+ logd("initPublishController completed: capabilityType=" + mCapabilityType +
+ ", publishState=" + mPublishState);
+ }
+
+ /**
+ * Get the initial publish state according to the given capability type.
+ * <p>
+ * The default publish state is NOT_PUBLISH when the capability type is PRESENCE.
+ * The default publish state is OK when the capability type is SIP OPTIONS.
+ * Otherwise, the default initial value is ERROR.
+ */
+ private int getInitialPublishState(@RcsImsCapabilityFlag int capabilityType) {
+ if (capabilityType == RcsImsCapabilities.CAPABILITY_TYPE_PRESENCE_UCE) {
+ return RcsUceAdapter.PUBLISH_STATE_NOT_PUBLISHED;
+ } else if (capabilityType == RcsImsCapabilities.CAPABILITY_TYPE_OPTIONS_UCE) {
+ return RcsUceAdapter.PUBLISH_STATE_OK;
+ } else {
+ return RcsUceAdapter.PUBLISH_STATE_OTHER_ERROR;
+ }
+ }
+
+ private void initPublishProcessor() {
+ mPublishProcessor = mPublishProcessorFactory.createPublishProcessor(mContext, mSubId,
+ mDeviceCapabilityInfo, mPublishControllerCallback);
+ }
+
+ private void initDeviceCapabilitiesListener() {
+ mDeviceCapListener = mDeviceCapListenerFactory.createDeviceCapListener(mContext, mSubId,
+ mDeviceCapabilityInfo, mPublishControllerCallback);
+ }
+
+ @Override
+ public void onRcsConnected(RcsFeatureManager manager) {
+ logd("onRcsConnected");
+ mPublishHandler.sendRcsConnectedMsg(manager);
+ }
+
+ @Override
+ public void onRcsDisconnected() {
+ logd("onRcsDisconnected");
+ mPublishHandler.sendRcsDisconnectedMsg();
+ }
+
+ @Override
+ public void onDestroy() {
+ logi("onDestroy");
+ mPublishHandler.sendDestroyedMsg();
+ }
+
+ @Override
+ public void onCarrierConfigChanged() {
+ logi("onCarrierConfigChanged");
+ mPublishHandler.sendCarrierConfigChangedMsg();
+ }
+
+ @Override
+ public int getUcePublishState() {
+ synchronized (mPublishStateLock) {
+ return (!mIsDestroyedFlag) ? mPublishState : RcsUceAdapter.PUBLISH_STATE_OTHER_ERROR;
+ }
+ }
+
+ @Override
+ public RcsContactUceCapability addRegistrationOverrideCapabilities(Set<String> featureTags) {
+ if (mDeviceCapabilityInfo.addRegistrationOverrideCapabilities(featureTags)) {
+ mPublishHandler.sendPublishMessage(PublishController.PUBLISH_TRIGGER_OVERRIDE_CAPS);
+ }
+ return mDeviceCapabilityInfo.getDeviceCapabilities(
+ RcsContactUceCapability.CAPABILITY_MECHANISM_PRESENCE, mContext);
+ }
+
+ @Override
+ public RcsContactUceCapability removeRegistrationOverrideCapabilities(Set<String> featureTags) {
+ if (mDeviceCapabilityInfo.removeRegistrationOverrideCapabilities(featureTags)) {
+ mPublishHandler.sendPublishMessage(PublishController.PUBLISH_TRIGGER_OVERRIDE_CAPS);
+ }
+ return mDeviceCapabilityInfo.getDeviceCapabilities(
+ RcsContactUceCapability.CAPABILITY_MECHANISM_PRESENCE, mContext);
+ }
+
+ @Override
+ public RcsContactUceCapability clearRegistrationOverrideCapabilities() {
+ if (mDeviceCapabilityInfo.clearRegistrationOverrideCapabilities()) {
+ mPublishHandler.sendPublishMessage(PublishController.PUBLISH_TRIGGER_OVERRIDE_CAPS);
+ }
+ return mDeviceCapabilityInfo.getDeviceCapabilities(
+ RcsContactUceCapability.CAPABILITY_MECHANISM_PRESENCE, mContext);
+ }
+
+ @Override
+ public RcsContactUceCapability getLatestRcsContactUceCapability() {
+ return mDeviceCapabilityInfo.getDeviceCapabilities(
+ RcsContactUceCapability.CAPABILITY_MECHANISM_PRESENCE, mContext);
+ }
+
+ @Override
+ public String getLastPidfXml() {
+ return mPidfXml;
+ }
+
+ /**
+ * Register a {@link PublishStateCallback} to listen to the published state changed.
+ */
+ @Override
+ public void registerPublishStateCallback(@NonNull IRcsUcePublishStateCallback c) {
+ synchronized (mPublishStateLock) {
+ if (mIsDestroyedFlag) return;
+ mPublishStateCallbacks.register(c);
+ logd("registerPublishStateCallback: size="
+ + mPublishStateCallbacks.getRegisteredCallbackCount());
+ }
+ // Notify the current publish state
+ mPublishHandler.sendNotifyCurrentPublishStateMessage(c);
+ }
+
+ /**
+ * Removes an existing {@link PublishStateCallback}.
+ */
+ @Override
+ public void unregisterPublishStateCallback(@NonNull IRcsUcePublishStateCallback c) {
+ synchronized (mPublishStateLock) {
+ if (mIsDestroyedFlag) return;
+ mPublishStateCallbacks.unregister(c);
+ }
+ }
+
+ @Override
+ public void setupResetDeviceStateTimer(long resetAfterSec) {
+ logd("setupResetDeviceStateTimer: resetAfterSec=" + resetAfterSec);
+ mPublishHandler.sendResetDeviceStateTimerMessage(resetAfterSec);
+ }
+
+ @Override
+ public void clearResetDeviceStateTimer() {
+ logd("clearResetDeviceStateTimer");
+ mPublishHandler.clearResetDeviceStateTimer();
+ }
+
+ // Clear all the publish state callbacks since the publish controller instance is destroyed.
+ private void clearPublishStateCallbacks() {
+ synchronized (mPublishStateLock) {
+ logd("clearPublishStateCallbacks");
+ final int lastIndex = mPublishStateCallbacks.getRegisteredCallbackCount() - 1;
+ for (int index = lastIndex; index >= 0; index--) {
+ IRcsUcePublishStateCallback callback =
+ mPublishStateCallbacks.getRegisteredCallbackItem(index);
+ mPublishStateCallbacks.unregister(callback);
+ }
+ }
+ }
+
+ /**
+ * Notify that the device's capabilities has been unpublished from the network.
+ */
+ @Override
+ public void onUnpublish() {
+ logd("onUnpublish");
+ if (mIsDestroyedFlag) return;
+ mPublishHandler.sendPublishStateChangedMessage(RcsUceAdapter.PUBLISH_STATE_NOT_PUBLISHED,
+ Instant.now(), null /*pidfXml*/);
+ }
+
+ @Override
+ public RcsContactUceCapability getDeviceCapabilities(@CapabilityMechanism int mechanism) {
+ return mDeviceCapabilityInfo.getDeviceCapabilities(mechanism, mContext);
+ }
+
+ // The local publish request from the sub-components which interact with PublishController.
+ private final PublishControllerCallback mPublishControllerCallback =
+ new PublishControllerCallback() {
+ @Override
+ public void requestPublishFromInternal(@PublishTriggerType int type) {
+ logd("requestPublishFromInternal: type=" + type);
+ mPublishHandler.sendPublishMessage(type);
+ }
+
+ @Override
+ public void onRequestCommandError(PublishRequestResponse requestResponse) {
+ logd("onRequestCommandError: taskId=" + requestResponse.getTaskId()
+ + ", time=" + requestResponse.getResponseTimestamp());
+ mPublishHandler.sendRequestCommandErrorMessage(requestResponse);
+ }
+
+ @Override
+ public void onRequestNetworkResp(PublishRequestResponse requestResponse) {
+ logd("onRequestNetworkResp: taskId=" + requestResponse.getTaskId()
+ + ", time=" + requestResponse.getResponseTimestamp());
+ mPublishHandler.sendRequestNetworkRespMessage(requestResponse);
+ }
+
+ @Override
+ public void setupRequestCanceledTimer(long taskId, long delay) {
+ logd("setupRequestCanceledTimer: taskId=" + taskId + ", delay=" + delay);
+ mPublishHandler.sendRequestCanceledTimerMessage(taskId, delay);
+ }
+
+ @Override
+ public void clearRequestCanceledTimer() {
+ logd("clearRequestCanceledTimer");
+ mPublishHandler.clearRequestCanceledTimer();
+ }
+
+ @Override
+ public void updatePublishRequestResult(@PublishState int state,
+ Instant updatedTime, String pidfXml) {
+ logd("updatePublishRequestResult: " + state + ", time=" + updatedTime);
+ mPublishHandler.sendPublishStateChangedMessage(state, updatedTime, pidfXml);
+ }
+
+ @Override
+ public void updatePublishThrottle(int value) {
+ logd("updatePublishThrottle: value=" + value);
+ mPublishProcessor.updatePublishThrottle(value);
+ }
+
+ @Override
+ public void refreshDeviceState(int sipCode, String reason) {
+ mUceCtrlCallback.refreshDeviceState(sipCode, reason,
+ UceController.REQUEST_TYPE_PUBLISH);
+ }
+ };
+
+ /**
+ * Publish the device's capabilities to the network. This method is triggered by ImsService.
+ */
+ @Override
+ public void requestPublishCapabilitiesFromService(int triggerType) {
+ logi("Receive the publish request from service: service trigger type=" + triggerType);
+ mPublishHandler.sendPublishMessage(PublishController.PUBLISH_TRIGGER_SERVICE);
+ }
+
+ private static class PublishHandler extends Handler {
+ private static final int MSG_RCS_CONNECTED = 1;
+ private static final int MSG_RCS_DISCONNECTED = 2;
+ private static final int MSG_DESTROYED = 3;
+ private static final int MSG_CARRIER_CONFIG_CHANGED = 4;
+ private static final int MSG_RCS_CAPABILITIES_CHANGED = 5;
+ private static final int MSG_PUBLISH_STATE_CHANGED = 6;
+ private static final int MSG_NOTIFY_CURRENT_PUBLISH_STATE = 7;
+ private static final int MSG_REQUEST_PUBLISH = 8;
+ private static final int MSG_REQUEST_CMD_ERROR = 9;
+ private static final int MSG_REQUEST_NETWORK_RESPONSE = 10;
+ private static final int MSG_REQUEST_CANCELED = 11;
+ private static final int MSG_RESET_DEVICE_STATE = 12;
+
+ private final WeakReference<PublishControllerImpl> mPublishControllerRef;
+
+ public PublishHandler(PublishControllerImpl publishController, Looper looper) {
+ super(looper);
+ mPublishControllerRef = new WeakReference<>(publishController);
+ }
+
+ @Override
+ public void handleMessage(Message message) {
+ PublishControllerImpl publishCtrl = mPublishControllerRef.get();
+ if (publishCtrl == null) {
+ return;
+ }
+ if (publishCtrl.mIsDestroyedFlag) return;
+ publishCtrl.logd("handleMessage: " + EVENT_DESCRIPTION.get(message.what));
+ switch (message.what) {
+ case MSG_RCS_CONNECTED: {
+ SomeArgs args = (SomeArgs) message.obj;
+ RcsFeatureManager manager = (RcsFeatureManager) args.arg1;
+ args.recycle();
+ publishCtrl.handleRcsConnectedMessage(manager);
+ break;
+ }
+ case MSG_RCS_DISCONNECTED:
+ publishCtrl.handleRcsDisconnectedMessage();
+ break;
+
+ case MSG_DESTROYED:
+ publishCtrl.handleDestroyedMessage();
+ break;
+
+ case MSG_CARRIER_CONFIG_CHANGED:
+ publishCtrl.handleCarrierConfigChangedMessage();
+ break;
+
+ case MSG_RCS_CAPABILITIES_CHANGED:
+ int RcsCapabilities = message.arg1;
+ publishCtrl.handleRcsCapabilitiesChangedMessage(RcsCapabilities);
+ break;
+
+ case MSG_PUBLISH_STATE_CHANGED: {
+ SomeArgs args = (SomeArgs) message.obj;
+ int newPublishState = (Integer) args.arg1;
+ Instant updatedTimestamp = (Instant) args.arg2;
+ String pidfXml = (String) args.arg3;
+ args.recycle();
+ publishCtrl.handlePublishStateChangedMessage(newPublishState, updatedTimestamp,
+ pidfXml);
+ break;
+ }
+ case MSG_NOTIFY_CURRENT_PUBLISH_STATE:
+ IRcsUcePublishStateCallback c = (IRcsUcePublishStateCallback) message.obj;
+ publishCtrl.handleNotifyCurrentPublishStateMessage(c);
+ break;
+
+ case MSG_REQUEST_PUBLISH:
+ int type = message.arg1;
+ publishCtrl.handleRequestPublishMessage(type);
+ break;
+
+ case MSG_REQUEST_CMD_ERROR:
+ PublishRequestResponse cmdErrorResponse = (PublishRequestResponse) message.obj;
+ publishCtrl.mPublishProcessor.onCommandError(cmdErrorResponse);
+ break;
+
+ case MSG_REQUEST_NETWORK_RESPONSE:
+ PublishRequestResponse networkResponse = (PublishRequestResponse) message.obj;
+ publishCtrl.mPublishProcessor.onNetworkResponse(networkResponse);
+ break;
+
+ case MSG_REQUEST_CANCELED:
+ long taskId = (Long) message.obj;
+ publishCtrl.handleRequestCanceledMessage(taskId);
+ break;
+
+ case MSG_RESET_DEVICE_STATE:
+ publishCtrl.handleResetDeviceStateMessage();
+ break;
+
+ default:
+ publishCtrl.logd("invalid message: " + message.what);
+ break;
+ }
+ publishCtrl.logd("handleMessage done: " + EVENT_DESCRIPTION.get(message.what));
+ }
+
+ /**
+ * Remove all the messages from the handler.
+ */
+ public void onDestroy() {
+ removeCallbacksAndMessages(null);
+ }
+
+ public void sendRcsConnectedMsg(RcsFeatureManager manager) {
+ PublishControllerImpl publishCtrl = mPublishControllerRef.get();
+ if (publishCtrl == null) return;
+ if (publishCtrl.mIsDestroyedFlag) return;
+
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = manager;
+ Message message = obtainMessage();
+ message.what = MSG_RCS_CONNECTED;
+ message.obj = args;
+ sendMessage(message);
+ }
+
+ public void sendRcsDisconnectedMsg() {
+ PublishControllerImpl publishCtrl = mPublishControllerRef.get();
+ if (publishCtrl == null) return;
+ if (publishCtrl.mIsDestroyedFlag) return;
+
+ Message message = obtainMessage();
+ message.what = MSG_RCS_DISCONNECTED;
+ sendMessage(message);
+ }
+
+ public void sendDestroyedMsg() {
+ PublishControllerImpl publishCtrl = mPublishControllerRef.get();
+ if (publishCtrl == null) return;
+ if (publishCtrl.mIsDestroyedFlag) return;
+
+ Message message = obtainMessage();
+ message.what = MSG_DESTROYED;
+ sendMessage(message);
+ }
+
+ public void sendCarrierConfigChangedMsg() {
+ PublishControllerImpl publishCtrl = mPublishControllerRef.get();
+ if (publishCtrl == null) return;
+ if (publishCtrl.mIsDestroyedFlag) return;
+
+ Message message = obtainMessage();
+ message.what = MSG_CARRIER_CONFIG_CHANGED;
+ sendMessage(message);
+ }
+
+ public void sendRcsCapabilitiesStatusChangedMsg(@RcsImsCapabilityFlag int capabilities) {
+ PublishControllerImpl publishCtrl = mPublishControllerRef.get();
+ if (publishCtrl == null) return;
+ if (publishCtrl.mIsDestroyedFlag) return;
+
+ Message message = obtainMessage();
+ message.what = MSG_RCS_CAPABILITIES_CHANGED;
+ message.arg1 = capabilities;
+ sendMessage(message);
+ }
+
+ /**
+ * Send the message to notify the publish state is changed.
+ */
+ public void sendPublishStateChangedMessage(@PublishState int publishState,
+ @NonNull Instant updatedTimestamp, String pidfXml) {
+ PublishControllerImpl publishCtrl = mPublishControllerRef.get();
+ if (publishCtrl == null) return;
+ if (publishCtrl.mIsDestroyedFlag) return;
+
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = publishState;
+ args.arg2 = updatedTimestamp;
+ args.arg3 = pidfXml;
+ Message message = obtainMessage();
+ message.what = MSG_PUBLISH_STATE_CHANGED;
+ message.obj = args;
+ sendMessage(message);
+ }
+
+ /**
+ * Send the message to notify the new added callback of the latest publish state.
+ */
+ public void sendNotifyCurrentPublishStateMessage(
+ IRcsUcePublishStateCallback callback) {
+ PublishControllerImpl publishCtrl = mPublishControllerRef.get();
+ if (publishCtrl == null) return;
+ if (publishCtrl.mIsDestroyedFlag) return;
+
+ Message message = obtainMessage();
+ message.what = MSG_NOTIFY_CURRENT_PUBLISH_STATE;
+ message.obj = callback;
+ sendMessage(message);
+ }
+
+ public void sendPublishMessage(@PublishTriggerType int type) {
+ sendPublishMessage(type, 0L);
+ }
+
+ public void sendPublishMessage(@PublishTriggerType int type, long delay) {
+ PublishControllerImpl publishCtrl = mPublishControllerRef.get();
+ if (publishCtrl == null) return;
+ if (publishCtrl.mIsDestroyedFlag) return;
+
+ // Disallow publish if the PRESENCE PUBLISH is not enabled and this request is not
+ // triggered by the ImsService.
+ if (!publishCtrl.isPresencePublishEnabled() && type != PUBLISH_TRIGGER_SERVICE) {
+ publishCtrl.logd("sendPublishMessage: disallowed type=" + type);
+ return;
+ }
+
+ Message message = obtainMessage();
+ message.what = MSG_REQUEST_PUBLISH;
+ message.arg1 = type;
+ sendMessageDelayed(message, delay);
+ }
+
+ public void sendRequestCommandErrorMessage(PublishRequestResponse response) {
+ PublishControllerImpl publishCtrl = mPublishControllerRef.get();
+ if (publishCtrl == null) {
+ return;
+ }
+ if (publishCtrl.mIsDestroyedFlag) return;
+ Message message = obtainMessage();
+ message.what = MSG_REQUEST_CMD_ERROR;
+ message.obj = response;
+ sendMessage(message);
+ }
+
+ public void sendRequestNetworkRespMessage(PublishRequestResponse response) {
+ PublishControllerImpl publishCtrl = mPublishControllerRef.get();
+ if (publishCtrl == null) {
+ return;
+ }
+ if (publishCtrl.mIsDestroyedFlag) return;
+ Message message = obtainMessage();
+ message.what = MSG_REQUEST_NETWORK_RESPONSE;
+ message.obj = response;
+ sendMessage(message);
+ }
+
+ public void sendRequestCanceledTimerMessage(long taskId, long delay) {
+ PublishControllerImpl publishCtrl = mPublishControllerRef.get();
+ if (publishCtrl == null) {
+ return;
+ }
+ if (publishCtrl.mIsDestroyedFlag) return;
+ removeMessages(MSG_REQUEST_CANCELED, (Long) taskId);
+
+ Message message = obtainMessage();
+ message.what = MSG_REQUEST_CANCELED;
+ message.obj = (Long) taskId;
+ sendMessageDelayed(message, delay);
+ }
+
+ public void clearRequestCanceledTimer() {
+ PublishControllerImpl publishCtrl = mPublishControllerRef.get();
+ if (publishCtrl == null) {
+ return;
+ }
+ if (publishCtrl.mIsDestroyedFlag) return;
+ removeMessages(MSG_REQUEST_CANCELED);
+ }
+
+ public void sendResetDeviceStateTimerMessage(long resetAfterSec) {
+ PublishControllerImpl publishCtrl = mPublishControllerRef.get();
+ if (publishCtrl == null) {
+ return;
+ }
+ if (publishCtrl.mIsDestroyedFlag) return;
+ // Remove old timer and setup the new timer.
+ removeMessages(MSG_RESET_DEVICE_STATE);
+ Message message = obtainMessage();
+ message.what = MSG_RESET_DEVICE_STATE;
+ sendMessageDelayed(message, TimeUnit.SECONDS.toMillis(resetAfterSec));
+ }
+
+ public void clearResetDeviceStateTimer() {
+ PublishControllerImpl publishCtrl = mPublishControllerRef.get();
+ if (publishCtrl == null) {
+ return;
+ }
+ if (publishCtrl.mIsDestroyedFlag) return;
+ removeMessages(MSG_RESET_DEVICE_STATE);
+ }
+
+ private static Map<Integer, String> EVENT_DESCRIPTION = new HashMap<>();
+ static {
+ EVENT_DESCRIPTION.put(MSG_RCS_CONNECTED, "RCS_CONNECTED");
+ EVENT_DESCRIPTION.put(MSG_RCS_DISCONNECTED, "RCS_DISCONNECTED");
+ EVENT_DESCRIPTION.put(MSG_DESTROYED, "DESTROYED");
+ EVENT_DESCRIPTION.put(MSG_CARRIER_CONFIG_CHANGED, "CARRIER_CONFIG_CHANGED");
+ EVENT_DESCRIPTION.put(MSG_RCS_CAPABILITIES_CHANGED, "RCS_CAPABILITIES_CHANGED");
+ EVENT_DESCRIPTION.put(MSG_PUBLISH_STATE_CHANGED, "PUBLISH_STATE_CHANGED");
+ EVENT_DESCRIPTION.put(MSG_NOTIFY_CURRENT_PUBLISH_STATE, "NOTIFY_PUBLISH_STATE");
+ EVENT_DESCRIPTION.put(MSG_REQUEST_PUBLISH, "REQUEST_PUBLISH");
+ EVENT_DESCRIPTION.put(MSG_REQUEST_CMD_ERROR, "REQUEST_CMD_ERROR");
+ EVENT_DESCRIPTION.put(MSG_REQUEST_NETWORK_RESPONSE, "REQUEST_NETWORK_RESPONSE");
+ EVENT_DESCRIPTION.put(MSG_REQUEST_CANCELED, "REQUEST_CANCELED");
+ EVENT_DESCRIPTION.put(MSG_RESET_DEVICE_STATE, "RESET_DEVICE_STATE");
+ }
+ }
+
+ /**
+ * Check if the PUBLISH request is allowed.
+ */
+ private boolean isPublishRequestAllowed() {
+ // The PUBLISH request requires that the RCS PRESENCE is capable.
+ if (!mDeviceCapabilityInfo.isPresenceCapable()) {
+ logd("isPublishRequestAllowed: capability presence uce is not enabled.");
+ return false;
+ }
+
+ // The first PUBLISH request is required to be triggered from the service.
+ if (!mReceivePublishFromService) {
+ logd("isPublishRequestAllowed: "
+ + "The first PUBLISH request from the server has not been received.");
+ return false;
+ }
+
+ // Check whether the device state is not allowed to execute the PUBLISH request.
+ DeviceStateResult deviceState = mUceCtrlCallback.getDeviceState();
+ if (deviceState.isRequestForbidden()) {
+ logd("isPublishRequestAllowed: The device state is disallowed. "
+ + deviceState.getDeviceState());
+ return false;
+ }
+
+ // Check whether there is already a publish request running or not. When the running
+ // request is finished and there is a pending request, it will send a new request.
+ if (mPublishProcessor.isPublishingNow()) {
+ logd("isPublishRequestAllowed: There is already a publish request running now.");
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Check whether the PRESENCE PUBLISH should be enabled or not. It should be enabled only when
+ * the PRESENCE mechanism is supported.
+ */
+ private boolean isPresencePublishEnabled() {
+ synchronized (mPublishStateLock) {
+ return mCapabilityType == RcsImsCapabilities.CAPABILITY_TYPE_PRESENCE_UCE;
+ }
+ }
+
+ /**
+ * Handle the RCS connected message. This method is called in the handler thread.
+ */
+ private void handleRcsConnectedMessage(RcsFeatureManager manager) {
+ if (mIsDestroyedFlag) return;
+ mRcsFeatureManager = manager;
+ mDeviceCapListener.onRcsConnected();
+ mPublishProcessor.onRcsConnected(manager);
+ registerRcsAvailabilityChanged(manager);
+ }
+
+ /**
+ * Handle the RCS disconnected message. This method is called in the handler thread.
+ */
+ private void handleRcsDisconnectedMessage() {
+ if (mIsDestroyedFlag) return;
+ mRcsFeatureManager = null;
+ mDeviceCapabilityInfo.updatePresenceCapable(false);
+ mDeviceCapListener.onRcsDisconnected();
+ mPublishProcessor.onRcsDisconnected();
+
+ // When the RCS is disconnected, update the publish state to NOT_PUBLISH if the PRESENCE
+ // PUBLISH is enabled.
+ if (isPresencePublishEnabled()) {
+ handlePublishStateChangedMessage(RcsUceAdapter.PUBLISH_STATE_NOT_PUBLISHED,
+ Instant.now(), null /*pidfXml*/);
+ }
+ }
+
+ /**
+ * Handle the Destroyed message. This method is called in the handler thread.
+ */
+ private void handleDestroyedMessage() {
+ mIsDestroyedFlag = true;
+ mDeviceCapabilityInfo.updatePresenceCapable(false);
+ unregisterRcsAvailabilityChanged();
+ mDeviceCapListener.onDestroy(); // It will turn off the listener automatically.
+ mPublishHandler.onDestroy();
+ mPublishProcessor.onDestroy();
+ synchronized (mPublishStateLock) {
+ clearPublishStateCallbacks();
+ }
+ }
+
+ /*
+ * Register the availability callback to receive the RCS capabilities change. This method is
+ * called when the RCS is connected.
+ */
+ private void registerRcsAvailabilityChanged(RcsFeatureManager manager) {
+ try {
+ manager.registerRcsAvailabilityCallback(mSubId, mRcsCapabilitiesCallback);
+ } catch (ImsException e) {
+ logw("registerRcsAvailabilityChanged exception " + e);
+ }
+ }
+
+ /*
+ * Unregister the availability callback. This method is called when the PublishController
+ * instance is destroyed.
+ */
+ private void unregisterRcsAvailabilityChanged() {
+ RcsFeatureManager manager = mRcsFeatureManager;
+ if (manager == null) return;
+ try {
+ manager.unregisterRcsAvailabilityCallback(mSubId, mRcsCapabilitiesCallback);
+ } catch (Exception e) {
+ // Do not handle the exception
+ }
+ }
+
+ /**
+ * Handle the carrier config changed message. This method is called in the handler thread.
+ */
+ private void handleCarrierConfigChangedMessage() {
+ if (mIsDestroyedFlag) return;
+
+ updateCapabilityTypeAndPublishStateIfNeeded();
+
+ String[] newMap = getCarrierServiceDescriptionFeatureTagMap();
+ if (mDeviceCapabilityInfo.updateCapabilityRegistrationTrackerMap(newMap)) {
+ mPublishHandler.sendPublishMessage(
+ PublishController.PUBLISH_TRIGGER_CARRIER_CONFIG_CHANGED);
+ }
+ }
+
+ /**
+ * Check whether the capability type has changed or not because of the carrier config changed.
+ * If the capability type has changed, the publish state also needs to be reinitialized.
+ * <p>
+ * This method is called in the handler thread.
+ */
+ private void updateCapabilityTypeAndPublishStateIfNeeded() {
+ synchronized (mPublishStateLock) {
+ int originalMechanism = mCapabilityType;
+ mCapabilityType = PublishUtils.getCapabilityType(mContext, mSubId);
+
+ // Return when the capability type has not changed.
+ if (originalMechanism == mCapabilityType) {
+ logd("updateCapTypeAndPublishStateIfNeeded: " +
+ "The capability type is not changed=" + mCapabilityType);
+ return;
+ }
+
+ // Reinitialize the publish state because the capability type has changed.
+ int updatedPublishState = getInitialPublishState(mCapabilityType);
+
+ logd("updateCapTypeAndPublishStateIfNeeded from " + originalMechanism +
+ " to " + mCapabilityType + ", new publish state=" + updatedPublishState);
+
+ // Update the publish state directly. Because this method is called in the
+ // handler thread already, the process of updating publish state does not need to be
+ // sent to the looper again.
+ handlePublishStateChangedMessage(updatedPublishState, Instant.now(), null /*pidfxml*/);
+ }
+ }
+
+ private String[] getCarrierServiceDescriptionFeatureTagMap() {
+ CarrierConfigManager manager = mContext.getSystemService(CarrierConfigManager.class);
+ PersistableBundle bundle = manager != null ? manager.getConfigForSubId(mSubId) :
+ CarrierConfigManager.getDefaultConfig();
+ return bundle.getStringArray(CarrierConfigManager.Ims.
+ KEY_PUBLISH_SERVICE_DESC_FEATURE_TAG_MAP_OVERRIDE_STRING_ARRAY);
+ }
+
+ private void handleRcsCapabilitiesChangedMessage(int capabilities) {
+ logd("handleRcsCapabilitiesChangedMessage: " + capabilities);
+ if (mIsDestroyedFlag) return;
+ RcsImsCapabilities RcsImsCapabilities = new RcsImsCapabilities(capabilities);
+ mDeviceCapabilityInfo.updatePresenceCapable(
+ RcsImsCapabilities.isCapable(RcsUceAdapter.CAPABILITY_TYPE_PRESENCE_UCE));
+ // Trigger a publish request if the RCS capabilities presence is enabled.
+ if (mDeviceCapabilityInfo.isPresenceCapable()) {
+ mPublishProcessor.checkAndSendPendingRequest();
+ }
+ }
+
+ /**
+ * Update the publish state and notify the publish state callback if the new state is different
+ * from original state.
+ */
+ private void handlePublishStateChangedMessage(@PublishState int newPublishState,
+ Instant updatedTimestamp, String pidfXml) {
+ synchronized (mPublishStateLock) {
+ if (mIsDestroyedFlag) return;
+ // Check if the time of the given publish state is not earlier than existing time.
+ if (updatedTimestamp == null || !updatedTimestamp.isAfter(mPublishStateUpdatedTime)) {
+ logd("handlePublishStateChangedMessage: updatedTimestamp is not allowed: "
+ + mPublishStateUpdatedTime + " to " + updatedTimestamp
+ + ", publishState=" + newPublishState);
+ return;
+ }
+ logd("publish state changes from " + mPublishState + " to " + newPublishState +
+ ", time=" + updatedTimestamp);
+ mPublishStateUpdatedTime = updatedTimestamp;
+ mPidfXml = pidfXml;
+ // Bail early and do not update listeners if the publish state didn't change.
+ if (mPublishState == newPublishState) return;
+ mPublishState = newPublishState;
+ }
+
+ // Trigger the publish state changed in handler thread since it may take time.
+ logd("Notify publish state changed: " + mPublishState);
+ mPublishStateCallbacks.broadcast(c -> {
+ try {
+ c.onPublishStateChanged(mPublishState);
+ } catch (RemoteException e) {
+ logw("Notify publish state changed error: " + e);
+ }
+ });
+ logd("Notify publish state changed: completed");
+ }
+
+ private void handleNotifyCurrentPublishStateMessage(IRcsUcePublishStateCallback callback) {
+ if (mIsDestroyedFlag || callback == null) return;
+ try {
+ callback.onPublishStateChanged(getUcePublishState());
+ } catch (RemoteException e) {
+ logw("handleCurrentPublishStateUpdateMessage exception: " + e);
+ }
+ }
+
+ private void handleRequestPublishMessage(@PublishTriggerType int type) {
+ if (mIsDestroyedFlag) return;
+
+ logd("handleRequestPublishMessage: type=" + type);
+
+ // Set the PUBLISH FROM SERVICE flag and reset the device state if the PUBLISH request is
+ // triggered by the ImsService.
+ if (type == PublishController.PUBLISH_TRIGGER_SERVICE) {
+ // Set the flag
+ if (!mReceivePublishFromService) {
+ mReceivePublishFromService = true;
+ }
+ // Reset device state
+ DeviceStateResult deviceState = mUceCtrlCallback.getDeviceState();
+ if (deviceState.isRequestForbidden()) {
+ mUceCtrlCallback.resetDeviceState();
+ }
+ }
+
+ // Set the pending flag and return if the request is not allowed.
+ if (!isPublishRequestAllowed()) {
+ logd("handleRequestPublishMessage: SKIP. The request is not allowed. type=" + type);
+ mPublishProcessor.setPendingRequest(type);
+ return;
+ }
+
+ // Update the latest PUBLISH allowed time according to the given trigger type.
+ mPublishProcessor.updatePublishingAllowedTime(type);
+
+ // Get the publish request delay time. If the delay is not present, the first
+ // PUBLISH is not allowed to be executed; If the delay time is 0, it means that
+ // this request can be executed immediately.
+ Optional<Long> delay = mPublishProcessor.getPublishingDelayTime();
+ if (!delay.isPresent()) {
+ logd("handleRequestPublishMessage: SKIP. The delay is empty. type=" + type);
+ mPublishProcessor.setPendingRequest(type);
+ return;
+ }
+
+ logd("handleRequestPublishMessage: " + type + ", delay=" + delay.get());
+ if (delay.get() == 0L) {
+ mPublishProcessor.doPublish(type);
+ } else {
+ mPublishHandler.sendPublishMessage(type, delay.get());
+ }
+ }
+
+ private void handleRequestCanceledMessage(long taskId) {
+ if (mIsDestroyedFlag) return;
+ mPublishProcessor.cancelPublishRequest(taskId);
+ }
+
+ private void handleResetDeviceStateMessage() {
+ if(mIsDestroyedFlag) return;
+ mUceCtrlCallback.resetDeviceState();
+ }
+
+ @VisibleForTesting
+ public void setCapabilityType(int type) {
+ mCapabilityType = type;
+ mPublishState = getInitialPublishState(mCapabilityType);
+ }
+
+ @VisibleForTesting
+ public void setPublishStateCallback(RemoteCallbackList<IRcsUcePublishStateCallback> list) {
+ mPublishStateCallbacks = list;
+ }
+
+ @VisibleForTesting
+ public PublishHandler getPublishHandler() {
+ return mPublishHandler;
+ }
+
+ @VisibleForTesting
+ public IImsCapabilityCallback getRcsCapabilitiesCallback() {
+ return mRcsCapabilitiesCallback;
+ }
+
+ @VisibleForTesting
+ public PublishControllerCallback getPublishControllerCallback() {
+ return mPublishControllerCallback;
+ }
+
+ private void logd(String log) {
+ Log.d(LOG_TAG, getLogPrefix().append(log).toString());
+ mLocalLog.log("[D] " + log);
+ }
+
+ private void logi(String log) {
+ Log.i(LOG_TAG, getLogPrefix().append(log).toString());
+ mLocalLog.log("[I] " + log);
+ }
+
+ private void logw(String log) {
+ Log.w(LOG_TAG, getLogPrefix().append(log).toString());
+ mLocalLog.log("[W] " + log);
+ }
+
+ private StringBuilder getLogPrefix() {
+ StringBuilder builder = new StringBuilder("[");
+ builder.append(mSubId);
+ builder.append("] ");
+ return builder;
+ }
+
+ @Override
+ public void dump(PrintWriter printWriter) {
+ IndentingPrintWriter pw = new IndentingPrintWriter(printWriter, " ");
+ pw.println("PublishControllerImpl" + "[subId: " + mSubId + "]:");
+ pw.increaseIndent();
+
+ pw.print("isPresenceCapable=");
+ pw.println(mDeviceCapabilityInfo.isPresenceCapable());
+ pw.print("mPublishState=");
+ pw.print(mPublishState);
+ pw.print(" at time ");
+ pw.println(mPublishStateUpdatedTime);
+ pw.println("Last PIDF XML:");
+ pw.increaseIndent();
+ if (Build.IS_ENG) {
+ pw.println(mPidfXml);
+ } else if (Build.IS_DEBUGGABLE) {
+ String pidfXml = (mPidfXml == null) ? "null" : mPidfXml;
+ pw.println(PublishUtils.removeNumbersFromUris(pidfXml));
+ } else {
+ pw.println(mPidfXml != null ? "***" : "null");
+ }
+ pw.decreaseIndent();
+
+ if (mPublishProcessor != null) {
+ mPublishProcessor.dump(pw);
+ } else {
+ pw.println("mPublishProcessor is null");
+ }
+
+ pw.println();
+ mDeviceCapListener.dump(pw);
+
+ pw.println("Log:");
+ pw.increaseIndent();
+ mLocalLog.dump(pw);
+ pw.decreaseIndent();
+ pw.println("---");
+
+ pw.decreaseIndent();
+ }
+}
diff --git a/src/java/com/android/ims/rcs/uce/presence/publish/PublishProcessor.java b/src/java/com/android/ims/rcs/uce/presence/publish/PublishProcessor.java
new file mode 100644
index 00000000..68aeaa8f
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/presence/publish/PublishProcessor.java
@@ -0,0 +1,492 @@
+/*
+ * 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.ims.rcs.uce.presence.publish;
+
+import static android.telephony.ims.RcsContactUceCapability.CAPABILITY_MECHANISM_PRESENCE;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.os.RemoteException;
+import android.telephony.ims.RcsContactUceCapability;
+import android.text.TextUtils;
+import android.util.IndentingPrintWriter;
+import android.util.LocalLog;
+import android.util.Log;
+
+import com.android.ims.RcsFeatureManager;
+import com.android.ims.rcs.uce.presence.pidfparser.PidfParser;
+import com.android.ims.rcs.uce.presence.publish.PublishController.PublishControllerCallback;
+import com.android.ims.rcs.uce.presence.publish.PublishController.PublishTriggerType;
+import com.android.ims.rcs.uce.util.UceUtils;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.PrintWriter;
+import java.time.Instant;
+import java.util.Optional;
+
+/**
+ * Send the publish request and handle the response of the publish request result.
+ */
+public class PublishProcessor {
+
+ private static final String LOG_TAG = UceUtils.getLogPrefix() + "PublishProcessor";
+
+ // The length of time waiting for the response callback.
+ private static final long RESPONSE_CALLBACK_WAITING_TIME = 60000L;
+
+ private final int mSubId;
+ private final Context mContext;
+ private volatile boolean mIsDestroyed;
+ private volatile RcsFeatureManager mRcsFeatureManager;
+
+ // Manage the state of the publish processor.
+ private PublishProcessorState mProcessorState;
+
+ // The information of the device's capabilities.
+ private final DeviceCapabilityInfo mDeviceCapabilities;
+
+ // The callback of the PublishController
+ private final PublishControllerCallback mPublishCtrlCallback;
+
+ // The lock of processing the pending request.
+ private final Object mPendingRequestLock = new Object();
+
+ private final LocalLog mLocalLog = new LocalLog(UceUtils.LOG_SIZE);
+
+ public PublishProcessor(Context context, int subId, DeviceCapabilityInfo capabilityInfo,
+ PublishControllerCallback publishCtrlCallback) {
+ mSubId = subId;
+ mContext = context;
+ mDeviceCapabilities = capabilityInfo;
+ mPublishCtrlCallback = publishCtrlCallback;
+ mProcessorState = new PublishProcessorState(subId);
+ }
+
+ /**
+ * The RcsFeature has been connected to the framework.
+ */
+ public void onRcsConnected(RcsFeatureManager featureManager) {
+ mLocalLog.log("onRcsConnected");
+ logi("onRcsConnected");
+ mRcsFeatureManager = featureManager;
+ // Check if there is a pending request.
+ checkAndSendPendingRequest();
+ }
+
+ /**
+ * The framework has lost the binding to the RcsFeature.
+ */
+ public void onRcsDisconnected() {
+ mLocalLog.log("onRcsDisconnected");
+ logi("onRcsDisconnected");
+ mRcsFeatureManager = null;
+ mProcessorState.onRcsDisconnected();
+ }
+
+ /**
+ * Set the destroy flag
+ */
+ public void onDestroy() {
+ mLocalLog.log("onDestroy");
+ logi("onDestroy");
+ mIsDestroyed = true;
+ }
+
+ /**
+ * Execute the publish request. This method is called by the handler of the PublishController.
+ * @param triggerType The type of triggering the publish request.
+ */
+ public void doPublish(@PublishTriggerType int triggerType) {
+ mProcessorState.setPublishingFlag(true);
+ if (!doPublishInternal(triggerType)) {
+ // Reset the publishing flag if the request cannot be sent to the IMS service.
+ mProcessorState.setPublishingFlag(false);
+ }
+ }
+ /**
+ * Execute the publish request internally.
+ * @param triggerType The type of triggering the publish request.
+ * @return true if the publish is sent to the IMS service successfully, false otherwise.
+ */
+ private boolean doPublishInternal(@PublishTriggerType int triggerType) {
+ if (mIsDestroyed) return false;
+
+ mLocalLog.log("doPublishInternal: trigger type=" + triggerType);
+ logi("doPublishInternal: trigger type=" + triggerType);
+
+ // Return if this request is not allowed to be executed.
+ if (!isRequestAllowed(triggerType)) {
+ mLocalLog.log("doPublishInternal: The request is not allowed.");
+ return false;
+ }
+
+ // Get the latest device's capabilities.
+ RcsContactUceCapability deviceCapability =
+ mDeviceCapabilities.getDeviceCapabilities(CAPABILITY_MECHANISM_PRESENCE, mContext);
+ if (deviceCapability == null) {
+ logw("doPublishInternal: device capability is null");
+ return false;
+ }
+
+ // Convert the device's capabilities to pidf format.
+ String pidfXml = PidfParser.convertToPidf(deviceCapability);
+ if (TextUtils.isEmpty(pidfXml)) {
+ logw("doPublishInternal: pidfXml is empty");
+ return false;
+ }
+
+ // Set the pending request and return if RCS is not connected. When the RCS is connected
+ // afterward, it will send a new request if there's a pending request.
+ RcsFeatureManager featureManager = mRcsFeatureManager;
+ if (featureManager == null) {
+ logw("doPublishInternal: RCS is not connected.");
+ setPendingRequest(triggerType);
+ return false;
+ }
+
+ // Publish to the Presence server.
+ return publishCapabilities(featureManager, pidfXml);
+ }
+
+ /*
+ * According to the given trigger type, check whether the request is allowed to be executed or
+ * not.
+ */
+ private boolean isRequestAllowed(@PublishTriggerType int triggerType) {
+ // Check if the instance is destroyed.
+ if (mIsDestroyed) {
+ logd("isPublishAllowed: This instance is already destroyed");
+ return false;
+ }
+
+ // Check if it has provisioned. When the provisioning changes, a new publish request will
+ // be triggered.
+ if (!UceUtils.isEabProvisioned(mContext, mSubId)) {
+ logd("isPublishAllowed: NOT provisioned");
+ return false;
+ }
+
+ // Do not request publish if the IMS is not registered. When the IMS is registered
+ // afterward, a new publish request will be triggered.
+ if (!mDeviceCapabilities.isImsRegistered()) {
+ logd("isPublishAllowed: IMS is not registered");
+ return false;
+ }
+
+ // Skip this request if the PUBLISH is not allowed at current time. Resend the PUBLISH
+ // request and it will be triggered with an appropriate delay time.
+ if (!mProcessorState.isPublishAllowedAtThisTime()) {
+ logd("isPublishAllowed: Current time is not allowed, resend this request");
+ mPublishCtrlCallback.requestPublishFromInternal(triggerType);
+ return false;
+ }
+ return true;
+ }
+
+ // Publish the device capabilities with the given pidf.
+ private boolean publishCapabilities(@NonNull RcsFeatureManager featureManager,
+ @NonNull String pidfXml) {
+ PublishRequestResponse requestResponse = null;
+ try {
+ // Clear the pending flag because it is going to send the latest device's capabilities.
+ clearPendingRequest();
+
+ // Generate a unique taskId to track this request.
+ long taskId = mProcessorState.generatePublishTaskId();
+ requestResponse = new PublishRequestResponse(mPublishCtrlCallback, taskId, pidfXml);
+
+ mLocalLog.log("publish capabilities: taskId=" + taskId);
+ logi("publishCapabilities: taskId=" + taskId);
+
+ // request publication
+ featureManager.requestPublication(pidfXml, requestResponse.getResponseCallback());
+
+ // Send a request canceled timer to avoid waiting too long for the response callback.
+ mPublishCtrlCallback.setupRequestCanceledTimer(taskId, RESPONSE_CALLBACK_WAITING_TIME);
+ return true;
+ } catch (RemoteException e) {
+ mLocalLog.log("publish capability exception: " + e.getMessage());
+ logw("publishCapabilities: exception=" + e.getMessage());
+ // Exception occurred, end this request.
+ setRequestEnded(requestResponse);
+ checkAndSendPendingRequest();
+ return false;
+ }
+ }
+
+ /**
+ * Handle the command error callback of the publish request. This method is called by the
+ * handler of the PublishController.
+ */
+ public void onCommandError(PublishRequestResponse requestResponse) {
+ if (!checkRequestRespValid(requestResponse)) {
+ mLocalLog.log("Command error callback is invalid");
+ logw("onCommandError: request response is invalid");
+ setRequestEnded(requestResponse);
+ checkAndSendPendingRequest();
+ return;
+ }
+
+ mLocalLog.log("Receive command error code=" + requestResponse.getCmdErrorCode());
+ logd("onCommandError: " + requestResponse.toString());
+
+ if (requestResponse.needRetry() && !mProcessorState.isReachMaximumRetries()) {
+ handleRequestRespWithRetry(requestResponse);
+ } else {
+ handleRequestRespWithoutRetry(requestResponse);
+ }
+ }
+
+ /**
+ * Handle the network response callback of the publish request. This method is called by the
+ * handler of the PublishController.
+ */
+ public void onNetworkResponse(PublishRequestResponse requestResponse) {
+ if (!checkRequestRespValid(requestResponse)) {
+ mLocalLog.log("Network response callback is invalid");
+ logw("onNetworkResponse: request response is invalid");
+ setRequestEnded(requestResponse);
+ checkAndSendPendingRequest();
+ return;
+ }
+
+ mLocalLog.log("Receive network response code=" + requestResponse.getNetworkRespSipCode());
+ logd("onNetworkResponse: " + requestResponse.toString());
+
+ if (requestResponse.needRetry() && !mProcessorState.isReachMaximumRetries()) {
+ handleRequestRespWithRetry(requestResponse);
+ } else {
+ handleRequestRespWithoutRetry(requestResponse);
+ }
+ }
+
+ // Check if the request response callback is valid.
+ private boolean checkRequestRespValid(PublishRequestResponse requestResponse) {
+ if (requestResponse == null) {
+ logd("checkRequestRespValid: request response is null");
+ return false;
+ }
+
+ if (!mProcessorState.isPublishingNow()) {
+ logd("checkRequestRespValid: the request is finished");
+ return false;
+ }
+
+ // Abandon this response callback if the current taskId is different to the response
+ // callback taskId. This response callback is obsoleted.
+ long taskId = mProcessorState.getCurrentTaskId();
+ long responseTaskId = requestResponse.getTaskId();
+ if (taskId != responseTaskId) {
+ logd("checkRequestRespValid: invalid taskId! current taskId=" + taskId
+ + ", response callback taskId=" + responseTaskId);
+ return false;
+ }
+
+ if (mIsDestroyed) {
+ logd("checkRequestRespValid: is already destroyed! taskId=" + taskId);
+ return false;
+ }
+ return true;
+ }
+
+ /*
+ * Handle the publishing request with retry. This method is called when it receives a failed
+ * request response and need to retry.
+ */
+ private void handleRequestRespWithRetry(PublishRequestResponse requestResponse) {
+ // Increase the retry count
+ mProcessorState.increaseRetryCount();
+
+ // Reset the pending flag because it is going to resend a request.
+ clearPendingRequest();
+
+ // Finish this request and resend a new publish request
+ setRequestEnded(requestResponse);
+ mPublishCtrlCallback.requestPublishFromInternal(PublishController.PUBLISH_TRIGGER_RETRY);
+ }
+
+ /*
+ * Handle the publishing request without retry. This method is called when it receives the
+ * request response and it does not need to retry.
+ */
+ private void handleRequestRespWithoutRetry(PublishRequestResponse requestResponse) {
+ Instant responseTime = requestResponse.getResponseTimestamp();
+
+ // Record the time when the request is successful and reset the retry count.
+ if (requestResponse.isRequestSuccess()) {
+ mProcessorState.setLastPublishedTime(responseTime);
+ mProcessorState.resetRetryCount();
+ }
+
+ // Update the publish state after the request has finished.
+ int publishState = requestResponse.getPublishState();
+ String pidfXml = requestResponse.getPidfXml();
+ mPublishCtrlCallback.updatePublishRequestResult(publishState, responseTime, pidfXml);
+
+ // Refresh the device state with the publish request result.
+ requestResponse.getResponseSipCode().ifPresent(sipCode -> {
+ String reason = requestResponse.getResponseReason().orElse("");
+ mPublishCtrlCallback.refreshDeviceState(sipCode, reason);
+ });
+
+ // Finish the request and check if there is pending request.
+ setRequestEnded(requestResponse);
+ checkAndSendPendingRequest();
+ }
+
+ /**
+ * Cancel the publishing request since it has token too long for waiting the response callback.
+ * This method is called by the handler of the PublishController.
+ */
+ public void cancelPublishRequest(long taskId) {
+ mLocalLog.log("cancel publish request: taskId=" + taskId);
+ logd("cancelPublishRequest: taskId=" + taskId);
+ setRequestEnded(null);
+ checkAndSendPendingRequest();
+ }
+
+ /*
+ * Finish the publishing request. This method is required to be called before the publishing
+ * request is finished.
+ */
+ private void setRequestEnded(PublishRequestResponse requestResponse) {
+ long taskId = -1L;
+ if (requestResponse != null) {
+ requestResponse.onDestroy();
+ taskId = requestResponse.getTaskId();
+ }
+ mProcessorState.setPublishingFlag(false);
+ mPublishCtrlCallback.clearRequestCanceledTimer();
+
+ mLocalLog.log("Set request ended: taskId=" + taskId);
+ logd("setRequestEnded: taskId=" + taskId);
+ }
+
+ /*
+ * Set the pending flag when it cannot be executed now.
+ */
+ public void setPendingRequest(@PublishTriggerType int triggerType) {
+ synchronized (mPendingRequestLock) {
+ mProcessorState.setPendingRequest(triggerType);
+ }
+ }
+
+ /**
+ * Check and trigger a new publish request if there is a pending request.
+ */
+ public void checkAndSendPendingRequest() {
+ synchronized (mPendingRequestLock) {
+ if (mIsDestroyed) return;
+ if (mProcessorState.hasPendingRequest()) {
+ // Retrieve the trigger type of the pending request
+ int type = mProcessorState.getPendingRequestTriggerType()
+ .orElse(PublishController.PUBLISH_TRIGGER_RETRY);
+ logd("checkAndSendPendingRequest: send pending request, type=" + type);
+
+ // Clear the pending flag because it is going to send a PUBLISH request.
+ mProcessorState.clearPendingRequest();
+ mPublishCtrlCallback.requestPublishFromInternal(type);
+ }
+ }
+ }
+
+ /**
+ * Clear the pending request. It means that the publish request is triggered and this flag can
+ * be removed.
+ */
+ private void clearPendingRequest() {
+ synchronized (mPendingRequestLock) {
+ mProcessorState.clearPendingRequest();
+ }
+ }
+
+ /**
+ * Update the publishing allowed time with the given trigger type. This method wil be called
+ * before adding a PUBLISH request to the handler.
+ * @param triggerType The trigger type of this PUBLISH request
+ */
+ public void updatePublishingAllowedTime(@PublishTriggerType int triggerType) {
+ mProcessorState.updatePublishingAllowedTime(triggerType);
+ }
+
+ /**
+ * @return The delay time to allow to execute the PUBLISH request. This method will be called
+ * to determine the delay time before adding a PUBLISH request to the handler.
+ */
+ public Optional<Long> getPublishingDelayTime() {
+ return mProcessorState.getPublishingDelayTime();
+ }
+
+ /**
+ * Update the publish throttle.
+ */
+ public void updatePublishThrottle(int publishThrottle) {
+ mProcessorState.updatePublishThrottle(publishThrottle);
+ }
+
+ /**
+ * @return true if the publish request is running now.
+ */
+ public boolean isPublishingNow() {
+ return mProcessorState.isPublishingNow();
+ }
+
+ @VisibleForTesting
+ public void setProcessorState(PublishProcessorState processorState) {
+ mProcessorState = processorState;
+ }
+
+ private void logd(String log) {
+ Log.d(LOG_TAG, getLogPrefix().append(log).toString());
+ }
+
+ private void logi(String log) {
+ Log.i(LOG_TAG, getLogPrefix().append(log).toString());
+ }
+
+ private void logw(String log) {
+ Log.w(LOG_TAG, getLogPrefix().append(log).toString());
+ }
+
+ private StringBuilder getLogPrefix() {
+ StringBuilder builder = new StringBuilder("[");
+ builder.append(mSubId);
+ builder.append("] ");
+ return builder;
+ }
+
+ public void dump(PrintWriter printWriter) {
+ IndentingPrintWriter pw = new IndentingPrintWriter(printWriter, " ");
+ pw.println("PublishProcessor" + "[subId: " + mSubId + "]:");
+ pw.increaseIndent();
+
+ pw.print("ProcessorState: isPublishing=");
+ pw.print(mProcessorState.isPublishingNow());
+ pw.print(", hasReachedMaxRetries=");
+ pw.print(mProcessorState.isReachMaximumRetries());
+ pw.print(", delayTimeToAllowPublish=");
+ pw.println(mProcessorState.getPublishingDelayTime().orElse(-1L));
+
+ pw.println("Log:");
+ pw.increaseIndent();
+ mLocalLog.dump(pw);
+ pw.decreaseIndent();
+ pw.println("---");
+
+ pw.decreaseIndent();
+ }
+}
diff --git a/src/java/com/android/ims/rcs/uce/presence/publish/PublishProcessorState.java b/src/java/com/android/ims/rcs/uce/presence/publish/PublishProcessorState.java
new file mode 100644
index 00000000..40d901f6
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/presence/publish/PublishProcessorState.java
@@ -0,0 +1,399 @@
+/*
+ * 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.ims.rcs.uce.presence.publish;
+
+import android.util.Log;
+
+import com.android.ims.rcs.uce.presence.publish.PublishController.PublishTriggerType;
+import com.android.ims.rcs.uce.util.UceUtils;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * The helper class to manage the publish request parameters.
+ */
+public class PublishProcessorState {
+
+ private static final String LOG_TAG = UceUtils.getLogPrefix() + "PublishProcessorState";
+
+ /*
+ * Manager the pending request flag and the trigger type of this pending request.
+ */
+ private static class PendingRequest {
+ private boolean mPendingFlag;
+ private Optional<Integer> mTriggerType;
+ private final Object mLock = new Object();
+
+ public PendingRequest() {
+ mTriggerType = Optional.empty();
+ }
+
+ // Set the flag to indicate there is a pending request.
+ public void setPendingRequest(@PublishTriggerType int triggerType) {
+ synchronized (mLock) {
+ mPendingFlag = true;
+ mTriggerType = Optional.of(triggerType);
+ }
+ }
+
+ // Clear the flag. The publish request is triggered and this flag can be cleared.
+ public void clearPendingRequest() {
+ synchronized (mLock) {
+ mPendingFlag = false;
+ mTriggerType = Optional.empty();
+ }
+ }
+
+ // Check if there is pending request need to be executed.
+ public boolean hasPendingRequest() {
+ synchronized (mLock) {
+ return mPendingFlag;
+ }
+ }
+
+ // Get the trigger type of the pending request.
+ public Optional<Integer> getPendingRequestTriggerType() {
+ synchronized (mLock) {
+ return mTriggerType;
+ }
+ }
+ }
+
+ /**
+ * Manager when the PUBLISH request can be executed.
+ */
+ private static class PublishThrottle {
+ // The unit time interval of the request retry.
+ private static final int RETRY_BASE_PERIOD_MIN = 1;
+
+ // The maximum number of the publication retries.
+ private static final int PUBLISH_MAXIMUM_NUM_RETRIES = 3;
+
+ // Get the minimum time that allows two PUBLISH requests can be executed continuously.
+ // It is one of the calculation conditions for the next publish allowed time.
+ private long mRcsPublishThrottle;
+
+ // The number of times the PUBLISH failed to retry. It is one of the calculation conditions
+ // for the next publish allowed time.
+ private int mRetryCount;
+
+ // The subscription ID associated with this throttle helper.
+ private int mSubId;
+
+ // The time when the last PUBLISH request is success. It is one of the calculation
+ // conditions for the next publish allowed time.
+ private Optional<Instant> mLastPublishedTime;
+
+ // The time to allow to execute the publishing request.
+ private Optional<Instant> mPublishAllowedTime;
+
+ public PublishThrottle(int subId) {
+ mSubId = subId;
+ resetState();
+ }
+
+ // Set the time of the last successful PUBLISH request.
+ public void setLastPublishedTime(Instant lastPublishedTime) {
+ mLastPublishedTime = Optional.of(lastPublishedTime);
+ }
+
+ // Increase the retry count when the PUBLISH has failed and need to be retried.
+ public void increaseRetryCount() {
+ if (mRetryCount < PUBLISH_MAXIMUM_NUM_RETRIES) {
+ mRetryCount++;
+ }
+ // Adjust the publish allowed time.
+ calcLatestPublishAllowedTime();
+ }
+
+ // Reset the retry count when the PUBLISH request is success or it does not need to retry.
+ public void resetRetryCount() {
+ mRetryCount = 0;
+ // Adjust the publish allowed time.
+ calcLatestPublishAllowedTime();
+ }
+
+ // In the case that the ImsService is disconnected, reset state for when the service
+ // reconnects
+ public void resetState() {
+ mLastPublishedTime = Optional.empty();
+ mPublishAllowedTime = Optional.empty();
+ mRcsPublishThrottle = UceUtils.getRcsPublishThrottle(mSubId);
+ Log.d(LOG_TAG, "RcsPublishThrottle=" + mRcsPublishThrottle);
+ }
+
+ // Check if it has reached the maximum retries.
+ public boolean isReachMaximumRetries() {
+ return (mRetryCount >= PUBLISH_MAXIMUM_NUM_RETRIES) ? true : false;
+ }
+
+ // Update the RCS publish throttle
+ public void updatePublishThrottle(int publishThrottle) {
+ mRcsPublishThrottle = publishThrottle;
+ calcLatestPublishAllowedTime();
+ }
+
+ // Check if the PUBLISH request can be executed now.
+ public boolean isPublishAllowedAtThisTime() {
+ // If the allowed time has not been set, it means that it is not ready to PUBLISH.
+ // It means that it has not received the publish request from the service.
+ if (!mPublishAllowedTime.isPresent()) {
+ return false;
+ }
+
+ // Check whether the current time has exceeded the allowed PUBLISH.
+ return (Instant.now().isBefore(mPublishAllowedTime.get())) ? false : true;
+ }
+
+ // Update the PUBLISH allowed time with the given trigger type.
+ public void updatePublishingAllowedTime(@PublishTriggerType int triggerType) {
+ if (triggerType == PublishController.PUBLISH_TRIGGER_SERVICE) {
+ // If the request is triggered by service, reset the retry count and allow to
+ // execute the PUBLISH immediately.
+ mRetryCount = 0;
+ mPublishAllowedTime = Optional.of(Instant.now());
+ } else if (triggerType != PublishController.PUBLISH_TRIGGER_RETRY) {
+ // If the trigger type is not RETRY, it means that the device capabilities have
+ // changed, reset the retry cout.
+ resetRetryCount();
+ }
+ }
+
+ // Get the delay time to allow to execute the PUBLISH request.
+ public Optional<Long> getPublishingDelayTime() {
+ // If the allowed time has not been set, it means that it is not ready to PUBLISH.
+ // It means that it has not received the publish request from the service.
+ if (!mPublishAllowedTime.isPresent()) {
+ return Optional.empty();
+ }
+
+ // Setup the delay to the time which publish request is allowed to be executed.
+ long delayTime = ChronoUnit.MILLIS.between(Instant.now(), mPublishAllowedTime.get());
+ if (delayTime < 0) {
+ delayTime = 0L;
+ }
+ return Optional.of(delayTime);
+ }
+
+ // Calculate the latest time allowed to PUBLISH
+ private void calcLatestPublishAllowedTime() {
+ final long retryDelay = getNextRetryDelayTime();
+ if (!mLastPublishedTime.isPresent()) {
+ // If the publish request has not been successful before, it does not need to
+ // consider the PUBLISH throttle. The publish allowed time is decided by the retry
+ // delay.
+ mPublishAllowedTime = Optional.of(
+ Instant.now().plus(Duration.ofMillis(retryDelay)));
+ Log.d(LOG_TAG, "calcLatestPublishAllowedTime: The last published time is empty");
+ } else {
+ // The default allowed time is the last published successful time plus the
+ // PUBLISH throttle.
+ Instant lastPublishedTime = mLastPublishedTime.get();
+ Instant defaultAllowedTime = lastPublishedTime.plus(
+ Duration.ofMillis(mRcsPublishThrottle));
+
+ if (retryDelay == 0) {
+ // If there is no delay time, the default allowed time is used.
+ mPublishAllowedTime = Optional.of(defaultAllowedTime);
+ } else {
+ // When the retry count is updated and there is delay time, it needs to compare
+ // the default time and the retry delay time. The later time will be the
+ // final decision value.
+ Instant retryDelayTime = Instant.now().plus(Duration.ofMillis(retryDelay));
+ mPublishAllowedTime = Optional.of(
+ (retryDelayTime.isAfter(defaultAllowedTime))
+ ? retryDelayTime : defaultAllowedTime);
+ }
+ }
+ Log.d(LOG_TAG, "calcLatestPublishAllowedTime: " + mPublishAllowedTime.get());
+ }
+
+ // Get the milliseconds of the next retry delay.
+ private long getNextRetryDelayTime() {
+ // If the current retry count is zero, the delay time is also zero.
+ if (mRetryCount == 0) return 0L;
+ // Next retry delay time (minute)
+ int power = mRetryCount - 1;
+ Double delayTime = RETRY_BASE_PERIOD_MIN * Math.pow(2, power);
+ // Convert to millis
+ return TimeUnit.MINUTES.toMillis(delayTime.longValue());
+ }
+ }
+
+
+ private long mTaskId;
+
+ // Used to check whether the publish request is running now.
+ private volatile boolean mIsPublishing;
+
+ // Control the pending request flag.
+ private final PendingRequest mPendingRequest;
+
+ // Control the publish throttle
+ private final PublishThrottle mPublishThrottle;
+
+ private final Object mLock = new Object();
+
+ public PublishProcessorState(int subId) {
+ mPendingRequest = new PendingRequest();
+ mPublishThrottle = new PublishThrottle(subId);
+ }
+
+ /**
+ * @return A unique task Id for this request.
+ */
+ public long generatePublishTaskId() {
+ synchronized (mLock) {
+ mTaskId = UceUtils.generateTaskId();
+ return mTaskId;
+ }
+ }
+
+ /**
+ * @return The current valid PUBLISH task ID.
+ */
+ public long getCurrentTaskId() {
+ synchronized (mLock) {
+ return mTaskId;
+ }
+ }
+
+ /**
+ * Set the publishing flag to indicate whether it is executing a PUBLISH request or not.
+ */
+ public void setPublishingFlag(boolean flag) {
+ mIsPublishing = flag;
+ }
+
+ /**
+ * @return true if it is executing a PUBLISH request now.
+ */
+ public boolean isPublishingNow() {
+ return mIsPublishing;
+ }
+
+ /**
+ * Set the flag to indicate there is a pending request waiting to be executed.
+ */
+ public void setPendingRequest(@PublishTriggerType int triggerType) {
+ mPendingRequest.setPendingRequest(triggerType);
+ }
+
+ /**
+ * Clear the flag. It means a new publish request is triggered and the pending request flag
+ * can be cleared.
+ */
+ public void clearPendingRequest() {
+ mPendingRequest.clearPendingRequest();
+ }
+
+ /**
+ * @return true if there is pending request to be executed.
+ */
+ public boolean hasPendingRequest() {
+ return mPendingRequest.hasPendingRequest();
+ }
+
+ /**
+ * @return The trigger type of the pending request. If there is no pending request, it will
+ * return Optional.empty
+ */
+ public Optional<Integer> getPendingRequestTriggerType() {
+ return mPendingRequest.getPendingRequestTriggerType();
+ }
+
+ /**
+ * Set the time of the last successful PUBLISH request.
+ * @param lastPublishedTime The time when the last PUBLISH request is success
+ */
+ public void setLastPublishedTime(Instant lastPublishedTime) {
+ synchronized (mLock) {
+ mPublishThrottle.setLastPublishedTime(lastPublishedTime);
+ }
+ }
+
+ /**
+ * Increase the retry count when the PUBLISH has failed and need to retry.
+ */
+ public void increaseRetryCount() {
+ synchronized (mLock) {
+ mPublishThrottle.increaseRetryCount();
+ }
+ }
+
+ /**
+ * Reset the retry count when the PUBLISH request is success or it does not need to retry.
+ */
+ public void resetRetryCount() {
+ synchronized (mLock) {
+ mPublishThrottle.resetRetryCount();
+ }
+ }
+
+ /*
+ * Check if it has reached the maximum retry count.
+ */
+ public boolean isReachMaximumRetries() {
+ synchronized (mLock) {
+ return mPublishThrottle.isReachMaximumRetries();
+ }
+ }
+
+ /*
+ * Check if the PUBLISH can be executed now.
+ */
+ public boolean isPublishAllowedAtThisTime() {
+ synchronized (mLock) {
+ return mPublishThrottle.isPublishAllowedAtThisTime();
+ }
+ }
+
+ /**
+ * Update the PUBLISH allowed time with the given trigger type.
+ * @param triggerType The trigger type of this PUBLISH request
+ */
+ public void updatePublishingAllowedTime(@PublishTriggerType int triggerType) {
+ synchronized (mLock) {
+ mPublishThrottle.updatePublishingAllowedTime(triggerType);
+ }
+ }
+
+ // Get the delay time to allow to execute the PUBLISH request.
+ public Optional<Long> getPublishingDelayTime() {
+ synchronized (mLock) {
+ return mPublishThrottle.getPublishingDelayTime();
+ }
+ }
+
+ public void updatePublishThrottle(int publishThrottle) {
+ synchronized (mLock) {
+ mPublishThrottle.updatePublishThrottle(publishThrottle);
+ }
+ }
+
+ public void onRcsDisconnected() {
+ synchronized (mLock) {
+ setPublishingFlag(false /*isPublishing*/);
+ clearPendingRequest();
+ mPublishThrottle.resetState();
+ }
+ }
+}
diff --git a/src/java/com/android/ims/rcs/uce/presence/publish/PublishRequestResponse.java b/src/java/com/android/ims/rcs/uce/presence/publish/PublishRequestResponse.java
new file mode 100644
index 00000000..a05a8d35
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/presence/publish/PublishRequestResponse.java
@@ -0,0 +1,342 @@
+/*
+ * 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.ims.rcs.uce.presence.publish;
+
+import android.annotation.Nullable;
+import android.telephony.ims.RcsUceAdapter;
+import android.telephony.ims.aidl.IPublishResponseCallback;
+import android.telephony.ims.stub.RcsCapabilityExchangeImplBase;
+import android.util.Log;
+
+import com.android.ims.rcs.uce.presence.publish.PublishController.PublishControllerCallback;
+import com.android.ims.rcs.uce.util.NetworkSipCode;
+import com.android.ims.rcs.uce.util.UceUtils;
+
+import java.time.Instant;
+import java.util.Optional;
+
+/**
+ * Receiving the result callback of the publish request.
+ */
+public class PublishRequestResponse {
+
+ private static final String LOG_TAG = UceUtils.getLogPrefix() + "PublishRequestResp";
+
+ private final long mTaskId;
+ private final String mPidfXml;
+ private volatile boolean mNeedRetry;
+ private volatile PublishControllerCallback mPublishCtrlCallback;
+
+ private Optional<Integer> mCmdErrorCode;
+ private Optional<Integer> mNetworkRespSipCode;
+ private Optional<String> mReasonPhrase;
+ private Optional<Integer> mReasonHeaderCause;
+ private Optional<String> mReasonHeaderText;
+
+ // The timestamp when receive the response from the network.
+ private Instant mResponseTimestamp;
+
+ public PublishRequestResponse(PublishControllerCallback publishCtrlCallback, long taskId,
+ String pidfXml) {
+ mTaskId = taskId;
+ mPidfXml = pidfXml;
+ mPublishCtrlCallback = publishCtrlCallback;
+ mCmdErrorCode = Optional.empty();
+ mNetworkRespSipCode = Optional.empty();
+ mReasonPhrase = Optional.empty();
+ mReasonHeaderCause = Optional.empty();
+ mReasonHeaderText = Optional.empty();
+ }
+
+ // The result callback of the publish capability request.
+ private IPublishResponseCallback mResponseCallback = new IPublishResponseCallback.Stub() {
+ @Override
+ public void onCommandError(int code) {
+ PublishRequestResponse.this.onCommandError(code);
+ }
+
+ @Override
+ public void onNetworkResponse(int code, String reason) {
+ PublishRequestResponse.this.onNetworkResponse(code, reason);
+ }
+
+ @Override
+ public void onNetworkRespHeader(int code, String reasonPhrase, int reasonHeaderCause,
+ String reasonHeaderText) {
+ PublishRequestResponse.this.onNetworkResponse(code, reasonPhrase, reasonHeaderCause,
+ reasonHeaderText);
+ }
+ };
+
+ public IPublishResponseCallback getResponseCallback() {
+ return mResponseCallback;
+ }
+
+ public long getTaskId() {
+ return mTaskId;
+ }
+
+ /**
+ * Retrieve the command error code which received from the network.
+ */
+ public Optional<Integer> getCmdErrorCode() {
+ return mCmdErrorCode;
+ }
+
+ /**
+ * Retrieve the network response sip code which received from the network.
+ */
+ public Optional<Integer> getNetworkRespSipCode() {
+ return mNetworkRespSipCode;
+ }
+
+ /**
+ * Retrieve the reason phrase of the network response which received from the network.
+ */
+ public Optional<String> getReasonPhrase() {
+ return mReasonPhrase;
+ }
+
+ /**
+ * Retrieve the reason header from the network response.
+ */
+ public Optional<Integer> getReasonHeaderCause() {
+ return mReasonHeaderCause;
+ }
+
+ /**
+ * Retrieve the description of the reason header.
+ */
+ public Optional<String> getReasonHeaderText() {
+ return mReasonHeaderText;
+ }
+
+ /**
+ * Retrieve the SIP code from the network response. It will get the value from the Reason
+ * Header first. If the ReasonHeader is not present, it will get the value from the Network
+ * response instead.
+ */
+ public Optional<Integer> getResponseSipCode() {
+ return (mReasonHeaderCause.isPresent()) ? mReasonHeaderCause : mNetworkRespSipCode;
+ }
+
+ /**
+ * Retrieve the REASON from the network response. It will get the value from the Reason Header
+ * first. If the ReasonHeader is not present, it will get the value from the Network response
+ * instead.
+ */
+ public Optional<String> getResponseReason() {
+ return (mReasonHeaderText.isPresent()) ? mReasonHeaderText : mReasonPhrase;
+ }
+
+ /**
+ * Get the timestamp of receiving the network response callback.
+ */
+ public @Nullable Instant getResponseTimestamp() {
+ return mResponseTimestamp;
+ }
+
+ /**
+ * @return the PIDF XML sent during this request.
+ */
+ public String getPidfXml() {
+ return mPidfXml;
+ }
+
+ public void onDestroy() {
+ mPublishCtrlCallback = null;
+ }
+
+ private void onCommandError(int errorCode) {
+ mResponseTimestamp = Instant.now();
+ mCmdErrorCode = Optional.of(errorCode);
+ updateRetryFlagByCommandError();
+
+ PublishControllerCallback ctrlCallback = mPublishCtrlCallback;
+ if (ctrlCallback != null) {
+ ctrlCallback.onRequestCommandError(this);
+ } else {
+ Log.d(LOG_TAG, "onCommandError: already destroyed. error code=" + errorCode);
+ }
+ }
+
+ private void onNetworkResponse(int sipCode, String reason) {
+ mResponseTimestamp = Instant.now();
+ mNetworkRespSipCode = Optional.of(sipCode);
+ mReasonPhrase = Optional.ofNullable(reason);
+ updateRetryFlagByNetworkResponse();
+
+ PublishControllerCallback ctrlCallback = mPublishCtrlCallback;
+ if (ctrlCallback != null) {
+ ctrlCallback.onRequestNetworkResp(this);
+ } else {
+ Log.d(LOG_TAG, "onNetworkResponse: already destroyed. sip code=" + sipCode);
+ }
+ }
+
+ private void onNetworkResponse(int sipCode, String reasonPhrase, int reasonHeaderCause,
+ String reasonHeaderText) {
+ mResponseTimestamp = Instant.now();
+ mNetworkRespSipCode = Optional.of(sipCode);
+ mReasonPhrase = Optional.ofNullable(reasonPhrase);
+ mReasonHeaderCause = Optional.of(reasonHeaderCause);
+ mReasonHeaderText = Optional.ofNullable(reasonHeaderText);
+ updateRetryFlagByNetworkResponse();
+
+ PublishControllerCallback ctrlCallback = mPublishCtrlCallback;
+ if (ctrlCallback != null) {
+ ctrlCallback.onRequestNetworkResp(this);
+ } else {
+ Log.d(LOG_TAG, "onNetworkResponse: already destroyed. sipCode=" + sipCode +
+ ", reasonHeader=" + reasonHeaderCause);
+ }
+ }
+
+ private void updateRetryFlagByCommandError() {
+ switch(getCmdErrorCode().orElse(-1)) {
+ case RcsCapabilityExchangeImplBase.COMMAND_CODE_REQUEST_TIMEOUT:
+ case RcsCapabilityExchangeImplBase.COMMAND_CODE_INSUFFICIENT_MEMORY:
+ case RcsCapabilityExchangeImplBase.COMMAND_CODE_LOST_NETWORK_CONNECTION:
+ case RcsCapabilityExchangeImplBase.COMMAND_CODE_SERVICE_UNAVAILABLE:
+ mNeedRetry = true;
+ break;
+ }
+ }
+
+ private void updateRetryFlagByNetworkResponse() {
+ // Disable retry flag because the retry mechanism is implemented in the ImsService.
+ mNeedRetry = false;
+ }
+
+ /*
+ * Check whether the publishing request is successful.
+ */
+ public boolean isRequestSuccess() {
+ if (isCommandError()) {
+ return false;
+ }
+ // The result of the request was treated as successful if the command error code is present
+ // and its value is COMMAND_CODE_NO_CHANGE.
+ if (isCommandCodeNoChange()) {
+ return true;
+ }
+
+ final int sipCodeOk = NetworkSipCode.SIP_CODE_OK;
+ if (getNetworkRespSipCode().filter(c -> c == sipCodeOk).isPresent() &&
+ (!getReasonHeaderCause().isPresent()
+ || getReasonHeaderCause().filter(c -> c == sipCodeOk).isPresent())) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Check if the PUBLISH request is failed with receiving the command error.
+ * @return true if the command is failure.
+ */
+ private boolean isCommandError() {
+ // The request is failed if the command error code is present and its value is not
+ // COMMAND_CODE_NO_CHANGE.
+ if (getCmdErrorCode().isPresent() && !isCommandCodeNoChange()) {
+ return true;
+ }
+ return false;
+ }
+
+ // @return true If it received the command code COMMAND_CODE_NO_CHANGE
+ private boolean isCommandCodeNoChange() {
+ if (getCmdErrorCode().filter(code ->
+ code == RcsCapabilityExchangeImplBase.COMMAND_CODE_NO_CHANGE).isPresent()) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Check whether the publishing request needs to be retried.
+ */
+ public boolean needRetry() {
+ return mNeedRetry;
+ }
+
+ /**
+ * @return The publish state when the publish request is finished.
+ */
+ public int getPublishState() {
+ if (isCommandError()) {
+ return getPublishStateByCmdErrorCode();
+ } else {
+ return getPublishStateByNetworkResponse();
+ }
+ }
+
+ /**
+ * Convert the command error code to the publish state
+ */
+ private int getPublishStateByCmdErrorCode() {
+ if (getCmdErrorCode().orElse(-1) ==
+ RcsCapabilityExchangeImplBase.COMMAND_CODE_REQUEST_TIMEOUT) {
+ return RcsUceAdapter.PUBLISH_STATE_REQUEST_TIMEOUT;
+ }
+ return RcsUceAdapter.PUBLISH_STATE_OTHER_ERROR;
+ }
+
+ /**
+ * Convert the network sip code to the publish state
+ */
+ private int getPublishStateByNetworkResponse() {
+ int respSipCode;
+ if (isCommandCodeNoChange()) {
+ // If the command code is COMMAND_CODE_NO_CHANGE, it should be treated as successful.
+ respSipCode = NetworkSipCode.SIP_CODE_OK;
+ } else if (getReasonHeaderCause().isPresent()) {
+ respSipCode = getReasonHeaderCause().get();
+ } else {
+ respSipCode = getNetworkRespSipCode().orElse(-1);
+ }
+
+ switch (respSipCode) {
+ case NetworkSipCode.SIP_CODE_OK:
+ return RcsUceAdapter.PUBLISH_STATE_OK;
+ case NetworkSipCode.SIP_CODE_FORBIDDEN:
+ case NetworkSipCode.SIP_CODE_NOT_FOUND:
+ return RcsUceAdapter.PUBLISH_STATE_RCS_PROVISION_ERROR;
+ case NetworkSipCode.SIP_CODE_REQUEST_TIMEOUT:
+ return RcsUceAdapter.PUBLISH_STATE_REQUEST_TIMEOUT;
+ default:
+ return RcsUceAdapter.PUBLISH_STATE_OTHER_ERROR;
+ }
+ }
+
+ /**
+ * Get the information of the publish request response.
+ */
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ builder.append("taskId=").append(mTaskId)
+ .append(", CmdErrorCode=").append(getCmdErrorCode().orElse(-1))
+ .append(", NetworkRespSipCode=").append(getNetworkRespSipCode().orElse(-1))
+ .append(", ReasonPhrase=").append(getReasonPhrase().orElse(""))
+ .append(", ReasonHeaderCause=").append(getReasonHeaderCause().orElse(-1))
+ .append(", ReasonHeaderText=").append(getReasonHeaderText().orElse(""))
+ .append(", ResponseTimestamp=").append(mResponseTimestamp)
+ .append(", isRequestSuccess=").append(isRequestSuccess())
+ .append(", needRetry=").append(mNeedRetry);
+ return builder.toString();
+ }
+}
diff --git a/src/java/com/android/ims/rcs/uce/presence/publish/PublishServiceDescTracker.java b/src/java/com/android/ims/rcs/uce/presence/publish/PublishServiceDescTracker.java
new file mode 100644
index 00000000..d527e6a3
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/presence/publish/PublishServiceDescTracker.java
@@ -0,0 +1,320 @@
+/*
+ * Copyright (C) 2021 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.ims.rcs.uce.presence.publish;
+
+import android.telephony.CarrierConfigManager;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.IndentingPrintWriter;
+import android.util.Log;
+
+import com.android.ims.rcs.uce.util.FeatureTags;
+
+import java.io.PrintWriter;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * Parses the Android Carrier Configuration for service-description -> feature tag mappings and
+ * tracks the IMS registration to pass in the
+ * to determine capabilities for features that the framework does not manage.
+ *
+ * @see CarrierConfigManager.Ims#KEY_PUBLISH_SERVICE_DESC_FEATURE_TAG_MAP_OVERRIDE_STRING_ARRAY for
+ * more information on the format of this key.
+ */
+public class PublishServiceDescTracker {
+ private static final String TAG = "PublishServiceDescTracker";
+
+ /**
+ * Map from (service-id, version) to the feature tags required in registration required in order
+ * for the RCS feature to be considered "capable".
+ * <p>
+ * See {@link
+ * CarrierConfigManager.Ims#KEY_PUBLISH_SERVICE_DESC_FEATURE_TAG_MAP_OVERRIDE_STRING_ARRAY}
+ * for more information on how this can be overridden/extended.
+ */
+ private static final Map<ServiceDescription, Set<String>> DEFAULT_SERVICE_DESCRIPTION_MAP;
+ static {
+ ArrayMap<ServiceDescription, Set<String>> map = new ArrayMap<>(19);
+ map.put(ServiceDescription.SERVICE_DESCRIPTION_CHAT_IM,
+ Collections.singleton(FeatureTags.FEATURE_TAG_CHAT_IM));
+ map.put(ServiceDescription.SERVICE_DESCRIPTION_CHAT_SESSION,
+ Collections.singleton(FeatureTags.FEATURE_TAG_CHAT_SESSION));
+ map.put(ServiceDescription.SERVICE_DESCRIPTION_FT,
+ Collections.singleton(FeatureTags.FEATURE_TAG_FILE_TRANSFER));
+ map.put(ServiceDescription.SERVICE_DESCRIPTION_FT_SMS,
+ Collections.singleton(FeatureTags.FEATURE_TAG_FILE_TRANSFER_VIA_SMS));
+ map.put(ServiceDescription.SERVICE_DESCRIPTION_PRESENCE,
+ Collections.singleton(FeatureTags.FEATURE_TAG_PRESENCE));
+ // Same service-ID & version for MMTEL, but different description.
+ map.put(ServiceDescription.SERVICE_DESCRIPTION_MMTEL_VOICE,
+ Collections.singleton(FeatureTags.FEATURE_TAG_MMTEL));
+ map.put(ServiceDescription.SERVICE_DESCRIPTION_MMTEL_VOICE_VIDEO, new ArraySet<>(
+ Arrays.asList(FeatureTags.FEATURE_TAG_MMTEL, FeatureTags.FEATURE_TAG_VIDEO)));
+ map.put(ServiceDescription.SERVICE_DESCRIPTION_GEOPUSH,
+ Collections.singleton(FeatureTags.FEATURE_TAG_GEO_PUSH));
+ map.put(ServiceDescription.SERVICE_DESCRIPTION_GEOPUSH_SMS,
+ Collections.singleton(FeatureTags.FEATURE_TAG_GEO_PUSH_VIA_SMS));
+ map.put(ServiceDescription.SERVICE_DESCRIPTION_CALL_COMPOSER,
+ Collections.singleton(FeatureTags.FEATURE_TAG_CALL_COMPOSER_ENRICHED_CALLING));
+ map.put(ServiceDescription.SERVICE_DESCRIPTION_CALL_COMPOSER_MMTEL,
+ Collections.singleton(FeatureTags.FEATURE_TAG_CALL_COMPOSER_VIA_TELEPHONY));
+ map.put(ServiceDescription.SERVICE_DESCRIPTION_POST_CALL,
+ Collections.singleton(FeatureTags.FEATURE_TAG_POST_CALL));
+ map.put(ServiceDescription.SERVICE_DESCRIPTION_SHARED_MAP,
+ Collections.singleton(FeatureTags.FEATURE_TAG_SHARED_MAP));
+ map.put(ServiceDescription.SERVICE_DESCRIPTION_SHARED_SKETCH,
+ Collections.singleton(FeatureTags.FEATURE_TAG_SHARED_SKETCH));
+ // Feature tags defined twice for chatbot session because we want v1 and v2 based on bot
+ // version
+ map.put(ServiceDescription.SERVICE_DESCRIPTION_CHATBOT_SESSION, new ArraySet<>(
+ Arrays.asList(FeatureTags.FEATURE_TAG_CHATBOT_COMMUNICATION_USING_SESSION,
+ FeatureTags.FEATURE_TAG_CHATBOT_VERSION_SUPPORTED)));
+ map.put(ServiceDescription.SERVICE_DESCRIPTION_CHATBOT_SESSION_V2, new ArraySet<>(
+ Arrays.asList(FeatureTags.FEATURE_TAG_CHATBOT_COMMUNICATION_USING_SESSION,
+ FeatureTags.FEATURE_TAG_CHATBOT_VERSION_SUPPORTED)));
+ // Feature tags defined twice for chatbot sa session because we want v1 and v2 based on bot
+ // version
+ map.put(ServiceDescription.SERVICE_DESCRIPTION_CHATBOT_SA_SESSION, new ArraySet<>(
+ Arrays.asList(FeatureTags.FEATURE_TAG_CHATBOT_COMMUNICATION_USING_STANDALONE_MSG,
+ FeatureTags.FEATURE_TAG_CHATBOT_VERSION_SUPPORTED)));
+ map.put(ServiceDescription.SERVICE_DESCRIPTION_CHATBOT_SA_SESSION_V2, new ArraySet<>(
+ Arrays.asList(FeatureTags.FEATURE_TAG_CHATBOT_COMMUNICATION_USING_STANDALONE_MSG,
+ FeatureTags.FEATURE_TAG_CHATBOT_VERSION_SUPPORTED)));
+ map.put(ServiceDescription.SERVICE_DESCRIPTION_CHATBOT_ROLE,
+ Collections.singleton(FeatureTags.FEATURE_TAG_CHATBOT_ROLE));
+ DEFAULT_SERVICE_DESCRIPTION_MAP = Collections.unmodifiableMap(map);
+ }
+
+ // Maps from ServiceDescription to the set of feature tags required to consider the feature
+ // capable for PUBLISH.
+ private final Map<ServiceDescription, Set<String>> mServiceDescriptionFeatureTagMap;
+ // Handles cases where multiple ServiceDescriptions match a subset of the same feature tags.
+ // This will be used to only include the feature tags where the
+ private final Set<ServiceDescription> mServiceDescriptionPartialMatches = new ArraySet<>();
+ // The capabilities calculated based off of the last IMS registration.
+ private final Set<ServiceDescription> mRegistrationCapabilities = new ArraySet<>();
+ // Contains the feature tags used in the last update to IMS registration.
+ private Set<String> mRegistrationFeatureTags = new ArraySet<>();
+
+ /**
+ * Create a new instance, which incorporates any carrier config overrides of the default
+ * mapping.
+ */
+ public static PublishServiceDescTracker fromCarrierConfig(String[] carrierConfig) {
+ Map<ServiceDescription, Set<String>> elements = new ArrayMap<>();
+ for (Map.Entry<ServiceDescription, Set<String>> entry :
+ DEFAULT_SERVICE_DESCRIPTION_MAP.entrySet()) {
+
+ elements.put(entry.getKey(), entry.getValue().stream()
+ .map(PublishServiceDescTracker::removeInconsistencies)
+ .collect(Collectors.toSet()));
+ }
+ if (carrierConfig != null) {
+ for (String entry : carrierConfig) {
+ String[] serviceDesc = entry.split("\\|");
+ if (serviceDesc.length < 4) {
+ Log.w(TAG, "fromCarrierConfig: error parsing " + entry);
+ continue;
+ }
+ elements.put(new ServiceDescription(serviceDesc[0].trim(), serviceDesc[1].trim(),
+ serviceDesc[2].trim()), parseFeatureTags(serviceDesc[3]));
+ }
+ }
+ return new PublishServiceDescTracker(elements);
+ }
+
+ /**
+ * Parse the feature tags in the string, which will be separated by ";".
+ */
+ private static Set<String> parseFeatureTags(String featureTags) {
+ // First, split feature tags into individual params
+ String[] featureTagSplit = featureTags.split(";");
+ if (featureTagSplit.length == 0) {
+ return Collections.emptySet();
+ }
+ ArraySet<String> tags = new ArraySet<>(featureTagSplit.length);
+ // Add each tag, first trying to remove inconsistencies in string matching that may cause
+ // it to fail.
+ for (String tag : featureTagSplit) {
+ tags.add(removeInconsistencies(tag));
+ }
+ return tags;
+ }
+
+ private PublishServiceDescTracker(Map<ServiceDescription, Set<String>> serviceFeatureTagMap) {
+ mServiceDescriptionFeatureTagMap = serviceFeatureTagMap;
+ Set<ServiceDescription> keySet = mServiceDescriptionFeatureTagMap.keySet();
+ // Go through and collect any ServiceDescriptions that have the same service-id & version
+ // (but not the same description) and add them to a "partial match" list.
+ for (ServiceDescription c : keySet) {
+ mServiceDescriptionPartialMatches.addAll(keySet.stream()
+ .filter(s -> !Objects.equals(s, c) && isSimilar(c , s))
+ .collect(Collectors.toList()));
+ }
+ }
+
+ /**
+ * Update the IMS registration associated with this tracker.
+ * @param imsRegistration A List of feature tags that were associated with the last IMS
+ * registration.
+ */
+ public void updateImsRegistration(Set<String> imsRegistration) {
+ Set<String> sanitizedTags = imsRegistration.stream()
+ // Ensure formatting passed in is the same as format stored here.
+ .map(PublishServiceDescTracker::parseFeatureTags)
+ // Each entry should only contain one feature tag.
+ .map(s -> s.iterator().next()).collect(Collectors.toSet());
+
+ // For aliased service descriptions (service-id && version is the same, but desc is
+ // different), Keep a "score" of the number of feature tags that the service description
+ // has associated with it. If another is found with a higher score, replace this one.
+ Map<ServiceDescription, Integer> aliasedServiceDescScore = new ArrayMap<>();
+ synchronized (mRegistrationCapabilities) {
+ mRegistrationFeatureTags = imsRegistration;
+ mRegistrationCapabilities.clear();
+ for (Map.Entry<ServiceDescription, Set<String>> desc :
+ mServiceDescriptionFeatureTagMap.entrySet()) {
+ boolean found = true;
+ for (String tag : desc.getValue()) {
+ if (!sanitizedTags.contains(tag)) {
+ found = false;
+ break;
+ }
+ }
+ if (found) {
+ // There may be ambiguity with multiple entries having the same service-id &&
+ // version, but not the same description. In this case, we need to find any
+ // other entries with the same id & version and replace it with the new entry
+ // if it matches more "completely", i.e. match "mmtel;video" over "mmtel" if the
+ // registration set includes "mmtel;video". Skip putting that in for now and
+ // instead track the match with the most feature tags associated with it that
+ // are all found in the IMS registration.
+ if (mServiceDescriptionPartialMatches.contains(desc.getKey())) {
+ ServiceDescription aliasedDesc = aliasedServiceDescScore.keySet().stream()
+ .filter(s -> isSimilar(s, desc.getKey()))
+ .findFirst().orElse(null);
+ if (aliasedDesc != null) {
+ Integer prevEntrySize = aliasedServiceDescScore.get(aliasedDesc);
+ if (prevEntrySize != null
+ // Overrides are added below the original map, so prefer those.
+ && (prevEntrySize <= desc.getValue().size())) {
+ aliasedServiceDescScore.remove(aliasedDesc);
+ aliasedServiceDescScore.put(desc.getKey(), desc.getValue().size());
+ }
+ } else {
+ aliasedServiceDescScore.put(desc.getKey(), desc.getValue().size());
+ }
+ } else {
+ mRegistrationCapabilities.add(desc.getKey());
+ }
+ }
+ }
+ // Collect the highest "scored" ServiceDescriptions and add themto registration caps.
+ mRegistrationCapabilities.addAll(aliasedServiceDescScore.keySet());
+ }
+ }
+
+ /**
+ * @return A copy of the service-description pairs (service-id, version) that are associated
+ * with the last IMS registration update in {@link #updateImsRegistration(Set)}
+ */
+ public Set<ServiceDescription> copyRegistrationCapabilities() {
+ synchronized (mRegistrationCapabilities) {
+ return new ArraySet<>(mRegistrationCapabilities);
+ }
+ }
+
+ /**
+ * @return A copy of the last update to the IMS feature tags via {@link #updateImsRegistration}.
+ */
+ public Set<String> copyRegistrationFeatureTags() {
+ synchronized (mRegistrationCapabilities) {
+ return new ArraySet<>(mRegistrationFeatureTags);
+ }
+ }
+
+ /**
+ * Dumps the current state of this tracker.
+ */
+ public void dump(PrintWriter printWriter) {
+ IndentingPrintWriter pw = new IndentingPrintWriter(printWriter, " ");
+ pw.println("PublishServiceDescTracker");
+ pw.increaseIndent();
+
+ pw.println("ServiceDescription -> Feature Tag Map:");
+ pw.increaseIndent();
+ for (Map.Entry<ServiceDescription, Set<String>> entry :
+ mServiceDescriptionFeatureTagMap.entrySet()) {
+ pw.print(entry.getKey());
+ pw.print("->");
+ pw.println(entry.getValue());
+ }
+ pw.println();
+ pw.decreaseIndent();
+
+ if (!mServiceDescriptionPartialMatches.isEmpty()) {
+ pw.println("Similar ServiceDescriptions:");
+ pw.increaseIndent();
+ for (ServiceDescription entry : mServiceDescriptionPartialMatches) {
+ pw.println(entry);
+ }
+ pw.decreaseIndent();
+ } else {
+ pw.println("No Similar ServiceDescriptions:");
+ }
+ pw.println();
+
+ pw.println("Last IMS registration update:");
+ pw.increaseIndent();
+ for (String entry : mRegistrationFeatureTags) {
+ pw.println(entry);
+ }
+ pw.println();
+ pw.decreaseIndent();
+
+ pw.println("Capabilities:");
+ pw.increaseIndent();
+ for (ServiceDescription entry : mRegistrationCapabilities) {
+ pw.println(entry);
+ }
+ pw.println();
+ pw.decreaseIndent();
+
+ pw.decreaseIndent();
+ }
+
+ /**
+ * Test if two ServiceDescriptions are similar, meaning service-id && version are equal.
+ */
+ private static boolean isSimilar(ServiceDescription a, ServiceDescription b) {
+ return (a.serviceId.equals(b.serviceId) && a.version.equals(b.version));
+ }
+
+ /**
+ * Remove any formatting inconsistencies that could make string matching difficult.
+ */
+ private static String removeInconsistencies(String tag) {
+ tag = tag.toLowerCase();
+ tag = tag.replaceAll("\\s+", "");
+ return tag;
+ }
+}
diff --git a/src/java/com/android/ims/rcs/uce/presence/publish/PublishUtils.java b/src/java/com/android/ims/rcs/uce/presence/publish/PublishUtils.java
new file mode 100644
index 00000000..ea1d11b9
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/presence/publish/PublishUtils.java
@@ -0,0 +1,151 @@
+/*
+ * 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.ims.rcs.uce.presence.publish;
+
+import android.content.Context;
+import android.net.Uri;
+import android.telephony.PhoneNumberUtils;
+import android.telephony.TelephonyManager;
+import android.telephony.ims.feature.RcsFeature.RcsImsCapabilities;
+import android.telephony.ims.feature.RcsFeature.RcsImsCapabilities.RcsImsCapabilityFlag;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.ims.rcs.uce.util.UceUtils;
+
+import java.util.Arrays;
+
+/**
+ * The util class of publishing device's capabilities.
+ */
+public class PublishUtils {
+ private static final String LOG_TAG = UceUtils.getLogPrefix() + "PublishUtils";
+
+ private static final String SCHEME_SIP = "sip";
+ private static final String SCHEME_TEL = "tel";
+ private static final String DOMAIN_SEPARATOR = "@";
+
+ public static Uri getDeviceContactUri(Context context, int subId,
+ DeviceCapabilityInfo deviceCap) {
+ // Get the uri from the IMS associated URI which is provided by the IMS service.
+ Uri contactUri = deviceCap.getImsAssociatedUri();
+ if (contactUri != null) {
+ Log.d(LOG_TAG, "getDeviceContactUri: ims associated uri");
+ return contactUri;
+ }
+
+ TelephonyManager telephonyManager = getTelephonyManager(context, subId);
+ if (telephonyManager == null) {
+ Log.w(LOG_TAG, "getDeviceContactUri: TelephonyManager is null");
+ return null;
+ }
+
+ // Get the contact uri from ISIM.
+ contactUri = getContactUriFromIsim(telephonyManager);
+ if (contactUri != null) {
+ Log.d(LOG_TAG, "getDeviceContactUri: impu");
+ return contactUri;
+ } else {
+ Log.d(LOG_TAG, "getDeviceContactUri: line number");
+ return getContactUriFromLine1Number(telephonyManager);
+ }
+ }
+
+ /**
+ * Find all instances of sip/sips/tel URIs containing PII and replace them.
+ * <p>
+ * This is used for removing PII in logging.
+ * @param source The source string to remove the phone numbers from.
+ * @return A version of the given string with SIP URIs removed.
+ */
+ public static String removeNumbersFromUris(String source) {
+ // Replace only the number portion in the sip/sips/tel URI
+ return source.replaceAll("(?:sips?|tel):(\\+?[\\d\\-]+)", "[removed]");
+ }
+
+ private static Uri getContactUriFromIsim(TelephonyManager telephonyManager) {
+ // Get the home network domain and the array of the public user identities
+ String domain = telephonyManager.getIsimDomain();
+ String[] impus = telephonyManager.getIsimImpu();
+
+ if (TextUtils.isEmpty(domain) || impus == null) {
+ Log.d(LOG_TAG, "getContactUriFromIsim: domain is null=" + TextUtils.isEmpty(domain));
+ Log.d(LOG_TAG, "getContactUriFromIsim: impu is null=" +
+ ((impus == null || impus.length == 0) ? "true" : "false"));
+ return null;
+ }
+
+ for (String impu : impus) {
+ if (TextUtils.isEmpty(impu)) continue;
+ Uri impuUri = Uri.parse(impu);
+ String scheme = impuUri.getScheme();
+ String schemeSpecificPart = impuUri.getSchemeSpecificPart();
+ if (SCHEME_SIP.equals(scheme) && !TextUtils.isEmpty(schemeSpecificPart) &&
+ schemeSpecificPart.endsWith(domain)) {
+ return impuUri;
+ }
+ }
+ Log.d(LOG_TAG, "getContactUriFromIsim: there is no impu matching the domain");
+ return null;
+ }
+
+ private static Uri getContactUriFromLine1Number(TelephonyManager telephonyManager) {
+ String phoneNumber = formatPhoneNumber(telephonyManager.getLine1Number());
+ if (TextUtils.isEmpty(phoneNumber)) {
+ Log.w(LOG_TAG, "Cannot get the phone number");
+ return null;
+ }
+
+ String domain = telephonyManager.getIsimDomain();
+ if (!TextUtils.isEmpty(domain)) {
+ return Uri.fromParts(SCHEME_SIP, phoneNumber + DOMAIN_SEPARATOR + domain, null);
+ } else {
+ return Uri.fromParts(SCHEME_TEL, phoneNumber, null);
+ }
+ }
+
+ private static String formatPhoneNumber(final String phoneNumber) {
+ if (TextUtils.isEmpty(phoneNumber)) {
+ Log.w(LOG_TAG, "formatPhoneNumber: phone number is empty");
+ return null;
+ }
+ String number = PhoneNumberUtils.stripSeparators(phoneNumber);
+ return PhoneNumberUtils.normalizeNumber(number);
+ }
+
+ private static TelephonyManager getTelephonyManager(Context context, int subId) {
+ TelephonyManager telephonyManager = context.getSystemService(TelephonyManager.class);
+ if (telephonyManager == null) {
+ return null;
+ } else {
+ return telephonyManager.createForSubscriptionId(subId);
+ }
+ }
+
+ static @RcsImsCapabilityFlag int getCapabilityType(Context context, int subId) {
+ boolean isPresenceSupported = UceUtils.isPresenceSupported(context, subId);
+ boolean isSipOptionsSupported = UceUtils.isSipOptionsSupported(context, subId);
+ if (isPresenceSupported) {
+ return RcsImsCapabilities.CAPABILITY_TYPE_PRESENCE_UCE;
+ } else if (isSipOptionsSupported) {
+ return RcsImsCapabilities.CAPABILITY_TYPE_OPTIONS_UCE;
+ } else {
+ // Return NONE when neither OPTIONS nor PRESENCE is supported.
+ return RcsImsCapabilities.CAPABILITY_TYPE_NONE;
+ }
+ }
+}
diff --git a/src/java/com/android/ims/rcs/uce/presence/publish/ServiceDescription.java b/src/java/com/android/ims/rcs/uce/presence/publish/ServiceDescription.java
new file mode 100644
index 00000000..f0db7d96
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/presence/publish/ServiceDescription.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright (C) 2021 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.ims.rcs.uce.presence.publish;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.telephony.ims.RcsContactPresenceTuple;
+import android.text.TextUtils;
+
+import java.util.Objects;
+
+/**
+ * Represents the "service-description" element in the PIDF XML for SIP PUBLISH of RCS capabilities.
+ */
+public class ServiceDescription {
+
+ public static final ServiceDescription SERVICE_DESCRIPTION_CHAT_IM = new ServiceDescription(
+ RcsContactPresenceTuple.SERVICE_ID_CHAT_V1,
+ "1.0" /*version*/,
+ null /*description*/
+ );
+
+ public static final ServiceDescription SERVICE_DESCRIPTION_CHAT_SESSION =
+ new ServiceDescription(
+ RcsContactPresenceTuple.SERVICE_ID_CHAT_V2,
+ "2.0" /*version*/,
+ null /*description*/
+ );
+
+ public static final ServiceDescription SERVICE_DESCRIPTION_FT = new ServiceDescription(
+ RcsContactPresenceTuple.SERVICE_ID_FT,
+ "1.0" /*version*/,
+ null /*description*/
+ );
+
+ public static final ServiceDescription SERVICE_DESCRIPTION_FT_SMS = new ServiceDescription(
+ RcsContactPresenceTuple.SERVICE_ID_FT_OVER_SMS,
+ "1.0" /*version*/,
+ null /*description*/
+ );
+
+ public static final ServiceDescription SERVICE_DESCRIPTION_PRESENCE = new ServiceDescription(
+ RcsContactPresenceTuple.SERVICE_ID_PRESENCE,
+ "1.0" /*version*/,
+ "Capabilities Discovery Service" /*description*/
+ );
+
+ public static final ServiceDescription SERVICE_DESCRIPTION_MMTEL_VOICE = new ServiceDescription(
+ RcsContactPresenceTuple.SERVICE_ID_MMTEL,
+ "1.0" /*version*/,
+ "Voice Service" /*description*/
+ );
+
+ // No change except for description (service capabilities generated elsewhere).
+ public static final ServiceDescription SERVICE_DESCRIPTION_MMTEL_VOICE_VIDEO =
+ new ServiceDescription(
+ RcsContactPresenceTuple.SERVICE_ID_MMTEL,
+ "1.0" /*version*/,
+ "Voice and Video Service" /*description*/
+ );
+
+ public static final ServiceDescription SERVICE_DESCRIPTION_GEOPUSH = new ServiceDescription(
+ RcsContactPresenceTuple.SERVICE_ID_GEO_PUSH,
+ "1.0" /*version*/,
+ null /*description*/
+ );
+
+ public static final ServiceDescription SERVICE_DESCRIPTION_GEOPUSH_SMS = new ServiceDescription(
+ RcsContactPresenceTuple.SERVICE_ID_GEO_PUSH_VIA_SMS,
+ "1.0" /*version*/,
+ null /*description*/
+ );
+
+ public static final ServiceDescription SERVICE_DESCRIPTION_CALL_COMPOSER =
+ new ServiceDescription(
+ RcsContactPresenceTuple.SERVICE_ID_CALL_COMPOSER,
+ "1.0" /*version*/,
+ null /*description*/
+ );
+
+ public static final ServiceDescription SERVICE_DESCRIPTION_CALL_COMPOSER_MMTEL =
+ new ServiceDescription(
+ RcsContactPresenceTuple.SERVICE_ID_CALL_COMPOSER,
+ "2.0" /*version*/,
+ null /*description*/
+ );
+
+ public static final ServiceDescription SERVICE_DESCRIPTION_POST_CALL = new ServiceDescription(
+ RcsContactPresenceTuple.SERVICE_ID_POST_CALL,
+ "1.0" /*version*/,
+ null /*description*/
+ );
+
+ public static final ServiceDescription SERVICE_DESCRIPTION_SHARED_MAP = new ServiceDescription(
+ RcsContactPresenceTuple.SERVICE_ID_SHARED_MAP,
+ "1.0" /*version*/,
+ null /*description*/
+ );
+
+ public static final ServiceDescription SERVICE_DESCRIPTION_SHARED_SKETCH =
+ new ServiceDescription(
+ RcsContactPresenceTuple.SERVICE_ID_SHARED_SKETCH,
+ "1.0" /*version*/,
+ null /*description*/
+ );
+
+ public static final ServiceDescription SERVICE_DESCRIPTION_CHATBOT_SESSION =
+ new ServiceDescription(
+ RcsContactPresenceTuple.SERVICE_ID_CHATBOT,
+ "1.0" /*version*/,
+ null /*description*/
+ );
+
+ public static final ServiceDescription SERVICE_DESCRIPTION_CHATBOT_SESSION_V2 =
+ new ServiceDescription(
+ RcsContactPresenceTuple.SERVICE_ID_CHATBOT,
+ "2.0" /*version*/,
+ null /*description*/
+ );
+
+ public static final ServiceDescription SERVICE_DESCRIPTION_CHATBOT_SA_SESSION =
+ new ServiceDescription(
+ RcsContactPresenceTuple.SERVICE_ID_CHATBOT_STANDALONE,
+ "1.0" /*version*/,
+ null /*description*/
+ );
+
+ public static final ServiceDescription SERVICE_DESCRIPTION_CHATBOT_SA_SESSION_V2 =
+ new ServiceDescription(
+ RcsContactPresenceTuple.SERVICE_ID_CHATBOT_STANDALONE,
+ "2.0" /*version*/,
+ null /*description*/
+ );
+
+ public static final ServiceDescription SERVICE_DESCRIPTION_CHATBOT_ROLE =
+ new ServiceDescription(
+ RcsContactPresenceTuple.SERVICE_ID_CHATBOT_ROLE,
+ "1.0" /*version*/,
+ null /*description*/
+ );
+
+ /** Mandatory "service-id" element */
+ public final @NonNull String serviceId;
+ /** Mandatory "version" element */
+ public final @NonNull String version;
+ /** Optional "description" element */
+ public final @Nullable String description;
+
+ public ServiceDescription(String serviceId, String version, String description) {
+ this.serviceId = serviceId;
+ this.version = version;
+ this.description = description;
+ }
+
+ public RcsContactPresenceTuple.Builder getTupleBuilder() {
+ RcsContactPresenceTuple.Builder b = new RcsContactPresenceTuple.Builder(
+ RcsContactPresenceTuple.TUPLE_BASIC_STATUS_OPEN, serviceId, version);
+ if (!TextUtils.isEmpty(description)) {
+ b.setServiceDescription(description);
+ }
+ return b;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ ServiceDescription that = (ServiceDescription) o;
+ return serviceId.equals(that.serviceId)
+ && version.equals(that.version)
+ && Objects.equals(description, that.description);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(serviceId, version, description);
+ }
+
+ @Override
+ public String toString() {
+ return "(id=" + serviceId + ", v=" + version + ", d=" + description + ')';
+ }
+}
diff --git a/src/java/com/android/ims/rcs/uce/presence/subscribe/SubscribeController.java b/src/java/com/android/ims/rcs/uce/presence/subscribe/SubscribeController.java
new file mode 100644
index 00000000..83e864b4
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/presence/subscribe/SubscribeController.java
@@ -0,0 +1,38 @@
+/*
+ * 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.ims.rcs.uce.presence.subscribe;
+
+import android.annotation.NonNull;
+import android.net.Uri;
+import android.os.RemoteException;
+import android.telephony.ims.aidl.ISubscribeResponseCallback;
+
+import com.android.ims.rcs.uce.ControllerBase;
+
+import java.util.List;
+
+/**
+ * The interface related to the SUBSCRIBE request
+ */
+public interface SubscribeController extends ControllerBase {
+ /**
+ * Request the cached capabilities for the requested contacts if they exist. If not, perform
+ * a capability request on the network for the capabilities of these contacts.
+ */
+ void requestCapabilities(@NonNull List<Uri> contactUris, @NonNull ISubscribeResponseCallback c)
+ throws RemoteException;
+}
diff --git a/src/java/com/android/ims/rcs/uce/presence/subscribe/SubscribeControllerImpl.java b/src/java/com/android/ims/rcs/uce/presence/subscribe/SubscribeControllerImpl.java
new file mode 100644
index 00000000..be4bd744
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/presence/subscribe/SubscribeControllerImpl.java
@@ -0,0 +1,85 @@
+/*
+ * 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.ims.rcs.uce.presence.subscribe;
+
+import android.content.Context;
+import android.net.Uri;
+import android.os.RemoteException;
+import android.telephony.ims.aidl.ISubscribeResponseCallback;
+import android.telephony.ims.stub.RcsCapabilityExchangeImplBase;
+import android.util.Log;
+
+import com.android.ims.RcsFeatureManager;
+import com.android.ims.rcs.uce.util.UceUtils;
+
+import java.util.List;
+
+/**
+ * The implementation of the SubscribeController.
+ */
+public class SubscribeControllerImpl implements SubscribeController {
+
+ private static final String LOG_TAG = UceUtils.getLogPrefix() + "SubscribeController";
+
+ private final int mSubId;
+ private final Context mContext;
+ private volatile boolean mIsDestroyedFlag;
+ private volatile RcsFeatureManager mRcsFeatureManager;
+
+ public SubscribeControllerImpl(Context context, int subId) {
+ mSubId = subId;
+ mContext = context;
+ }
+
+ @Override
+ public void onRcsConnected(RcsFeatureManager manager) {
+ mRcsFeatureManager = manager;
+ }
+
+ @Override
+ public void onRcsDisconnected() {
+ mRcsFeatureManager = null;
+ }
+
+ @Override
+ public void onDestroy() {
+ mIsDestroyedFlag = true;
+ }
+
+ @Override
+ public void onCarrierConfigChanged() {
+ // Nothing Required Here.
+ }
+
+ @Override
+ public void requestCapabilities(List<Uri> contactUris, ISubscribeResponseCallback c)
+ throws RemoteException {
+
+ if (mIsDestroyedFlag) {
+ throw new RemoteException("Subscribe controller is destroyed");
+ }
+
+ RcsFeatureManager featureManager = mRcsFeatureManager;
+ if (featureManager == null) {
+ Log.w(LOG_TAG, "requestCapabilities: Service is unavailable");
+ c.onCommandError(RcsCapabilityExchangeImplBase.COMMAND_CODE_SERVICE_UNAVAILABLE);
+ return;
+ }
+
+ featureManager.requestCapabilities(contactUris, c);
+ }
+}
diff --git a/src/java/com/android/ims/rcs/uce/request/CapabilityRequest.java b/src/java/com/android/ims/rcs/uce/request/CapabilityRequest.java
new file mode 100644
index 00000000..f7a4acc6
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/request/CapabilityRequest.java
@@ -0,0 +1,261 @@
+/*
+ * Copyright (c) 2021 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.ims.rcs.uce.request;
+
+import android.net.Uri;
+import android.telephony.ims.RcsUceAdapter;
+import android.telephony.ims.RcsContactUceCapability;
+import android.util.Log;
+
+import com.android.ims.rcs.uce.UceDeviceState.DeviceStateResult;
+import com.android.ims.rcs.uce.eab.EabCapabilityResult;
+import com.android.ims.rcs.uce.request.UceRequestManager.RequestManagerCallback;
+import com.android.ims.rcs.uce.util.UceUtils;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+/**
+ * The base class of the UCE request to request the capabilities from the carrier network.
+ */
+public abstract class CapabilityRequest implements UceRequest {
+
+ private static final String LOG_TAG = UceUtils.getLogPrefix() + "CapabilityRequest";
+
+ protected final int mSubId;
+ protected final long mTaskId;
+ protected final List<Uri> mUriList;
+ protected final @UceRequestType int mRequestType;
+ protected final RequestManagerCallback mRequestManagerCallback;
+ protected final CapabilityRequestResponse mRequestResponse;
+
+ protected volatile long mCoordinatorId;
+ protected volatile boolean mIsFinished;
+ protected volatile boolean mSkipGettingFromCache;
+
+ public CapabilityRequest(int subId, @UceRequestType int type, RequestManagerCallback callback) {
+ mSubId = subId;
+ mRequestType = type;
+ mUriList = new ArrayList<>();
+ mRequestManagerCallback = callback;
+ mRequestResponse = new CapabilityRequestResponse();
+ mTaskId = UceUtils.generateTaskId();
+ }
+
+ @VisibleForTesting
+ public CapabilityRequest(int subId, @UceRequestType int type, RequestManagerCallback callback,
+ CapabilityRequestResponse requestResponse) {
+ mSubId = subId;
+ mRequestType = type;
+ mUriList = new ArrayList<>();
+ mRequestManagerCallback = callback;
+ mRequestResponse = requestResponse;
+ mTaskId = UceUtils.generateTaskId();
+ }
+
+ @Override
+ public void setRequestCoordinatorId(long coordinatorId) {
+ mCoordinatorId = coordinatorId;
+ }
+
+ @Override
+ public long getRequestCoordinatorId() {
+ return mCoordinatorId;
+ }
+
+ @Override
+ public long getTaskId() {
+ return mTaskId;
+ }
+
+ @Override
+ public void onFinish() {
+ mIsFinished = true;
+ // Remove the timeout timer of this request
+ mRequestManagerCallback.removeRequestTimeoutTimer(mTaskId);
+ }
+
+ @Override
+ public void setContactUri(List<Uri> uris) {
+ mUriList.addAll(uris);
+ mRequestResponse.setRequestContacts(uris);
+ }
+
+ public List<Uri> getContactUri() {
+ return Collections.unmodifiableList(mUriList);
+ }
+
+ /**
+ * Set to check if this request should be getting the capabilities from the cache. The flag is
+ * set when the request is triggered by the capability polling service. The contacts from the
+ * capability polling service are already expired, skip checking from the cache.
+ */
+ public void setSkipGettingFromCache(boolean skipFromCache) {
+ mSkipGettingFromCache = skipFromCache;
+ }
+
+ /**
+ * Return if the capabilities request should skip getting from the cache. The flag is set when
+ * the request is triggered by the capability polling service and the request doesn't need to
+ * check the cache again.
+ */
+ private boolean isSkipGettingFromCache() {
+ return mSkipGettingFromCache;
+ }
+
+ /**
+ * @return The RequestResponse instance associated with this request.
+ */
+ public CapabilityRequestResponse getRequestResponse() {
+ return mRequestResponse;
+ }
+
+ /**
+ * Start executing this request.
+ */
+ @Override
+ public void executeRequest() {
+ // Return if this request is not allowed to be executed.
+ if (!isRequestAllowed()) {
+ logd("executeRequest: The request is not allowed.");
+ mRequestManagerCallback.notifyRequestError(mCoordinatorId, mTaskId);
+ return;
+ }
+
+ // Get the capabilities from the cache.
+ final List<RcsContactUceCapability> cachedCapList
+ = isSkipGettingFromCache() ? Collections.EMPTY_LIST : getCapabilitiesFromCache();
+ mRequestResponse.addCachedCapabilities(cachedCapList);
+
+ logd("executeRequest: cached capabilities size=" + cachedCapList.size());
+
+ // Notify that the cached capabilities are updated.
+ if (!cachedCapList.isEmpty()) {
+ mRequestManagerCallback.notifyCachedCapabilitiesUpdated(mCoordinatorId, mTaskId);
+ }
+
+ // Get the rest contacts which need to request capabilities from the network.
+ final List<Uri> requestCapUris = getRequestingFromNetworkUris(cachedCapList);
+
+ logd("executeRequest: requestCapUris size=" + requestCapUris.size());
+
+ // Notify that it doesn't need to request capabilities from the network when all the
+ // requested capabilities can be retrieved from cache. Otherwise, it needs to request
+ // capabilities from the network for those contacts which cannot retrieve capabilities from
+ // the cache.
+ if (requestCapUris.isEmpty()) {
+ mRequestManagerCallback.notifyNoNeedRequestFromNetwork(mCoordinatorId, mTaskId);
+ } else {
+ requestCapabilities(requestCapUris);
+ }
+ }
+
+ // Check whether this request is allowed to be executed or not.
+ private boolean isRequestAllowed() {
+ if (mUriList == null || mUriList.isEmpty()) {
+ logw("isRequestAllowed: uri is empty");
+ mRequestResponse.setRequestInternalError(RcsUceAdapter.ERROR_GENERIC_FAILURE);
+ return false;
+ }
+
+ if (mIsFinished) {
+ logw("isRequestAllowed: This request is finished");
+ mRequestResponse.setRequestInternalError(RcsUceAdapter.ERROR_GENERIC_FAILURE);
+ return false;
+ }
+
+ DeviceStateResult deviceStateResult = mRequestManagerCallback.getDeviceState();
+ if (deviceStateResult.isRequestForbidden()) {
+ logw("isRequestAllowed: The device is disallowed.");
+ mRequestResponse.setRequestInternalError(
+ deviceStateResult.getErrorCode().orElse(RcsUceAdapter.ERROR_GENERIC_FAILURE));
+ return false;
+ }
+ return true;
+ }
+
+ // Get the cached capabilities by the given request type.
+ private List<RcsContactUceCapability> getCapabilitiesFromCache() {
+ List<EabCapabilityResult> resultList = null;
+ if (mRequestType == REQUEST_TYPE_CAPABILITY) {
+ resultList = mRequestManagerCallback.getCapabilitiesFromCache(mUriList);
+ } else if (mRequestType == REQUEST_TYPE_AVAILABILITY) {
+ // Always get the first element if the request type is availability.
+ Uri uri = mUriList.get(0);
+ EabCapabilityResult eabResult = mRequestManagerCallback.getAvailabilityFromCache(uri);
+ resultList = new ArrayList<>();
+ resultList.add(eabResult);
+ }
+ if (resultList == null) {
+ return Collections.emptyList();
+ }
+ return resultList.stream()
+ .filter(Objects::nonNull)
+ .filter(result -> result.getStatus() == EabCapabilityResult.EAB_QUERY_SUCCESSFUL)
+ .map(EabCapabilityResult::getContactCapabilities)
+ .filter(Objects::nonNull)
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * Get the contact uris which cannot retrieve capabilities from the cache.
+ * @param cachedCapList The capabilities which are already stored in the cache.
+ */
+ private List<Uri> getRequestingFromNetworkUris(List<RcsContactUceCapability> cachedCapList) {
+ return mUriList.stream()
+ .filter(uri -> cachedCapList.stream()
+ .noneMatch(cap -> cap.getContactUri().equals(uri)))
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * Set the timeout timer of this request.
+ */
+ protected void setupRequestTimeoutTimer() {
+ long timeoutAfterMs = UceUtils.getCapRequestTimeoutAfterMillis();
+ logd("setupRequestTimeoutTimer(ms): " + timeoutAfterMs);
+ mRequestManagerCallback.setRequestTimeoutTimer(mCoordinatorId, mTaskId, timeoutAfterMs);
+ }
+
+ /*
+ * Requests capabilities from IMS. The inherited request is required to override this method
+ * to define the behavior of requesting capabilities.
+ */
+ protected abstract void requestCapabilities(List<Uri> requestCapUris);
+
+ protected void logd(String log) {
+ Log.d(LOG_TAG, getLogPrefix().append(log).toString());
+ }
+
+ protected void logw(String log) {
+ Log.w(LOG_TAG, getLogPrefix().append(log).toString());
+ }
+
+ protected void logi(String log) {
+ Log.i(LOG_TAG, getLogPrefix().append(log).toString());
+ }
+
+ private StringBuilder getLogPrefix() {
+ StringBuilder builder = new StringBuilder("[");
+ builder.append(mSubId).append("][taskId=").append(mTaskId).append("] ");
+ return builder;
+ }
+}
diff --git a/src/java/com/android/ims/rcs/uce/request/CapabilityRequestResponse.java b/src/java/com/android/ims/rcs/uce/request/CapabilityRequestResponse.java
new file mode 100644
index 00000000..97371b8b
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/request/CapabilityRequestResponse.java
@@ -0,0 +1,485 @@
+/*
+ * 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.ims.rcs.uce.request;
+
+import android.net.Uri;
+import android.telephony.ims.RcsContactTerminatedReason;
+import android.telephony.ims.RcsContactUceCapability;
+import android.telephony.ims.RcsUceAdapter;
+import android.telephony.ims.RcsUceAdapter.ErrorCode;
+import android.telephony.ims.stub.RcsCapabilityExchangeImplBase;
+import android.telephony.ims.stub.RcsCapabilityExchangeImplBase.CommandCode;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.ims.rcs.uce.UceController;
+import com.android.ims.rcs.uce.presence.pidfparser.PidfParserUtils;
+import com.android.ims.rcs.uce.util.NetworkSipCode;
+import com.android.ims.rcs.uce.util.UceUtils;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * The container of the result of the capabilities request.
+ */
+public class CapabilityRequestResponse {
+
+ private static final String LOG_TAG = UceUtils.getLogPrefix() + "CapabilityRequestResp";
+
+ // The error code when the request encounters internal errors.
+ private @ErrorCode Optional<Integer> mRequestInternalError;
+
+ // The command error code of the request. It is assigned by the callback "onCommandError"
+ private @CommandCode Optional<Integer> mCommandError;
+
+ // The SIP code and reason of the network response.
+ private Optional<Integer> mNetworkRespSipCode;
+ private Optional<String> mReasonPhrase;
+
+ // The SIP code and the phrase read from the reason header
+ private Optional<Integer> mReasonHeaderCause;
+ private Optional<String> mReasonHeaderText;
+
+ // The reason why the this request was terminated and how long after it can be retried.
+ // This value is assigned by the callback "onTerminated"
+ private Optional<String> mTerminatedReason;
+ private Optional<Long> mRetryAfterMillis;
+
+ // The list of the valid capabilities which is retrieved from the cache.
+ private List<RcsContactUceCapability> mCachedCapabilityList;
+
+ // The list of the updated capabilities. This is assigned by the callback
+ // "onNotifyCapabilitiesUpdate"
+ private List<RcsContactUceCapability> mUpdatedCapabilityList;
+
+ // The list of the terminated resource. This is assigned by the callback
+ // "onResourceTerminated"
+ private List<RcsContactUceCapability> mTerminatedResource;
+
+ // The list of the remote contact's capability.
+ private Set<String> mRemoteCaps;
+
+ // The collection to record whether the request contacts have received the capabilities updated.
+ private Map<Uri, Boolean> mContactCapsReceived;
+
+ public CapabilityRequestResponse() {
+ mRequestInternalError = Optional.empty();
+ mCommandError = Optional.empty();
+ mNetworkRespSipCode = Optional.empty();
+ mReasonPhrase = Optional.empty();
+ mReasonHeaderCause = Optional.empty();
+ mReasonHeaderText = Optional.empty();
+ mTerminatedReason = Optional.empty();
+ mRetryAfterMillis = Optional.of(0L);
+ mTerminatedResource = new ArrayList<>();
+ mCachedCapabilityList = new ArrayList<>();
+ mUpdatedCapabilityList = new ArrayList<>();
+ mRemoteCaps = new HashSet<>();
+ mContactCapsReceived = new HashMap<>();
+ }
+
+ /**
+ * Set the request contacts which is expected to receive the capabilities updated.
+ */
+ public synchronized void setRequestContacts(List<Uri> contactUris) {
+ // Initialize the default value to FALSE. All the numbers have not received the
+ // capabilities updated.
+ contactUris.forEach(contact -> mContactCapsReceived.put(contact, Boolean.FALSE));
+ Log.d(LOG_TAG, "setRequestContacts: size=" + mContactCapsReceived.size());
+ }
+
+ /**
+ * Get the contacts that have not received the capabilities updated yet.
+ */
+ public synchronized List<Uri> getNotReceiveCapabilityUpdatedContact() {
+ return mContactCapsReceived.entrySet()
+ .stream()
+ .filter(entry -> Objects.equals(entry.getValue(), Boolean.FALSE))
+ .map(Map.Entry::getKey)
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * Set the request contacts which is expected to receive the capabilities updated.
+ */
+ public synchronized boolean haveAllRequestCapsUpdatedBeenReceived() {
+ return !(mContactCapsReceived.containsValue(Boolean.FALSE));
+ }
+
+ /**
+ * Set the error code when the request encounters internal unexpected errors.
+ * @param errorCode the error code of the internal request error.
+ */
+ public synchronized void setRequestInternalError(@ErrorCode int errorCode) {
+ mRequestInternalError = Optional.of(errorCode);
+ }
+
+ /**
+ * Get the request internal error code.
+ */
+ public synchronized Optional<Integer> getRequestInternalError() {
+ return mRequestInternalError;
+ }
+
+ /**
+ * Set the command error code which is sent from ImsService and set the capability error code.
+ */
+ public synchronized void setCommandError(@CommandCode int commandError) {
+ mCommandError = Optional.of(commandError);
+ }
+
+ /**
+ * Get the command error codeof this request.
+ */
+ public synchronized Optional<Integer> getCommandError() {
+ return mCommandError;
+ }
+
+ /**
+ * Set the network response of this request which is sent by the network.
+ */
+ public synchronized void setNetworkResponseCode(int sipCode, String reason) {
+ mNetworkRespSipCode = Optional.of(sipCode);
+ mReasonPhrase = Optional.ofNullable(reason);
+ }
+
+ /**
+ * Set the network response of this request which is sent by the network.
+ */
+ public synchronized void setNetworkResponseCode(int sipCode, String reasonPhrase,
+ int reasonHeaderCause, String reasonHeaderText) {
+ mNetworkRespSipCode = Optional.of(sipCode);
+ mReasonPhrase = Optional.ofNullable(reasonPhrase);
+ mReasonHeaderCause = Optional.of(reasonHeaderCause);
+ mReasonHeaderText = Optional.ofNullable(reasonHeaderText);
+ }
+
+ // Get the sip code of the network response.
+ public synchronized Optional<Integer> getNetworkRespSipCode() {
+ return mNetworkRespSipCode;
+ }
+
+ // Get the reason of the network response.
+ public synchronized Optional<String> getReasonPhrase() {
+ return mReasonPhrase;
+ }
+
+ // Get the response sip code from the reason header.
+ public synchronized Optional<Integer> getReasonHeaderCause() {
+ return mReasonHeaderCause;
+ }
+
+ // Get the response phrae from the reason header.
+ public synchronized Optional<String> getReasonHeaderText() {
+ return mReasonHeaderText;
+ }
+
+ public Optional<Integer> getResponseSipCode() {
+ if (mReasonHeaderCause.isPresent()) {
+ return mReasonHeaderCause;
+ } else {
+ return mNetworkRespSipCode;
+ }
+ }
+
+ public Optional<String> getResponseReason() {
+ if (mReasonPhrase.isPresent()) {
+ return mReasonPhrase;
+ } else {
+ return mReasonHeaderText;
+ }
+ }
+
+ /**
+ * Set the reason and retry-after info when the callback onTerminated is called.
+ * @param reason The reason why this request is terminated.
+ * @param retryAfterMillis How long to wait before retry this request.
+ */
+ public synchronized void setTerminated(String reason, long retryAfterMillis) {
+ mTerminatedReason = Optional.ofNullable(reason);
+ mRetryAfterMillis = Optional.of(retryAfterMillis);
+ }
+
+ /**
+ * @return The reason of terminating the subscription request. empty string if it has not
+ * been given.
+ */
+ public synchronized String getTerminatedReason() {
+ return mTerminatedReason.orElse("");
+ }
+
+ /**
+ * @return Return the retryAfterMillis, 0L if the value is not present.
+ */
+ public synchronized long getRetryAfterMillis() {
+ return mRetryAfterMillis.orElse(0L);
+ }
+
+ /**
+ * Add the capabilities which are retrieved from the cache.
+ */
+ public synchronized void addCachedCapabilities(List<RcsContactUceCapability> capabilityList) {
+ mCachedCapabilityList.addAll(capabilityList);
+
+ // Update the flag to indicate that these contacts have received the capabilities updated.
+ updateCapsReceivedFlag(capabilityList);
+ }
+
+ /**
+ * Update the flag to indicate that the given contacts have received the capabilities updated.
+ */
+ private synchronized void updateCapsReceivedFlag(List<RcsContactUceCapability> updatedCapList) {
+ for (RcsContactUceCapability updatedCap : updatedCapList) {
+ Uri updatedUri = updatedCap.getContactUri();
+ if (updatedUri == null) continue;
+ String updatedUriStr = updatedUri.toString();
+
+ for (Map.Entry<Uri, Boolean> contactCapEntry : mContactCapsReceived.entrySet()) {
+ String number = UceUtils.getContactNumber(contactCapEntry.getKey());
+ if (!TextUtils.isEmpty(number) && updatedUriStr.contains(number)) {
+ // Set the flag that this contact has received the capability updated.
+ contactCapEntry.setValue(true);
+ }
+ }
+ }
+ }
+
+ /**
+ * Clear the cached capabilities when the cached capabilities have been sent to client.
+ */
+ public synchronized void removeCachedContactCapabilities() {
+ mCachedCapabilityList.clear();
+ }
+
+ /**
+ * @return the cached capabilities.
+ */
+ public synchronized List<RcsContactUceCapability> getCachedContactCapability() {
+ return Collections.unmodifiableList(mCachedCapabilityList);
+ }
+
+ /**
+ * Add the updated contact capabilities which sent from ImsService.
+ */
+ public synchronized void addUpdatedCapabilities(List<RcsContactUceCapability> capabilityList) {
+ mUpdatedCapabilityList.addAll(capabilityList);
+
+ // Update the flag to indicate that these contacts have received the capabilities updated.
+ updateCapsReceivedFlag(capabilityList);
+ }
+
+ /**
+ * Remove the given capabilities from the UpdatedCapabilityList when these capabilities have
+ * updated to the requester.
+ */
+ public synchronized void removeUpdatedCapabilities(List<RcsContactUceCapability> capList) {
+ mUpdatedCapabilityList.removeAll(capList);
+ }
+
+ /**
+ * Get all the updated capabilities to trigger the capability receive callback.
+ */
+ public synchronized List<RcsContactUceCapability> getUpdatedContactCapability() {
+ return Collections.unmodifiableList(mUpdatedCapabilityList);
+ }
+
+ /**
+ * Add the terminated resources which sent from ImsService.
+ */
+ public synchronized void addTerminatedResource(List<RcsContactTerminatedReason> resourceList) {
+ // Convert the RcsContactTerminatedReason to RcsContactUceCapability
+ List<RcsContactUceCapability> capabilityList = resourceList.stream()
+ .filter(Objects::nonNull)
+ .map(reason -> PidfParserUtils.getTerminatedCapability(
+ reason.getContactUri(), reason.getReason())).collect(Collectors.toList());
+
+ // Save the terminated resource.
+ mTerminatedResource.addAll(capabilityList);
+
+ // Update the flag to indicate that these contacts have received the capabilities updated.
+ updateCapsReceivedFlag(capabilityList);
+ }
+
+ /*
+ * Remove the given capabilities from the mTerminatedResource when these capabilities have
+ * updated to the requester.
+ */
+ public synchronized void removeTerminatedResources(List<RcsContactUceCapability> resourceList) {
+ mTerminatedResource.removeAll(resourceList);
+ }
+
+ /**
+ * Get the terminated resources which sent from ImsService.
+ */
+ public synchronized List<RcsContactUceCapability> getTerminatedResources() {
+ return Collections.unmodifiableList(mTerminatedResource);
+ }
+
+ /**
+ * Set the remote's capabilities which are sent from the network.
+ */
+ public synchronized void setRemoteCapabilities(Set<String> remoteCaps) {
+ if (remoteCaps != null) {
+ remoteCaps.stream().filter(Objects::nonNull).forEach(capability ->
+ mRemoteCaps.add(capability));
+ }
+ }
+
+ /**
+ * Get the remote capability feature tags.
+ */
+ public synchronized Set<String> getRemoteCapability() {
+ return Collections.unmodifiableSet(mRemoteCaps);
+ }
+
+ /**
+ * Check if the network response is success.
+ * @return true if the network response code is OK or Accepted and the Reason header cause
+ * is either not present or OK.
+ */
+ public synchronized boolean isNetworkResponseOK() {
+ final int sipCodeOk = NetworkSipCode.SIP_CODE_OK;
+ final int sipCodeAccepted = NetworkSipCode.SIP_CODE_ACCEPTED;
+ Optional<Integer> respSipCode = getNetworkRespSipCode();
+ if (respSipCode.filter(c -> (c == sipCodeOk || c == sipCodeAccepted)).isPresent()
+ && (!getReasonHeaderCause().isPresent()
+ || getReasonHeaderCause().filter(c -> c == sipCodeOk).isPresent())) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Check whether the request is forbidden or not.
+ * @return true if the Reason header sip code is 403(Forbidden) or the response sip code is 403.
+ */
+ public synchronized boolean isRequestForbidden() {
+ final int sipCodeForbidden = NetworkSipCode.SIP_CODE_FORBIDDEN;
+ if (getReasonHeaderCause().isPresent()) {
+ return getReasonHeaderCause().filter(c -> c == sipCodeForbidden).isPresent();
+ } else {
+ return getNetworkRespSipCode().filter(c -> c == sipCodeForbidden).isPresent();
+ }
+ }
+
+ /**
+ * Check the contacts of the request is not found.
+ * @return true if the sip code of the network response is one of NOT_FOUND(404),
+ * SIP_CODE_METHOD_NOT_ALLOWED(405) or DOES_NOT_EXIST_ANYWHERE(604)
+ */
+ public synchronized boolean isNotFound() {
+ Optional<Integer> respSipCode = Optional.empty();
+ if (getReasonHeaderCause().isPresent()) {
+ respSipCode = getReasonHeaderCause();
+ } else if (getNetworkRespSipCode().isPresent()) {
+ respSipCode = getNetworkRespSipCode();
+ }
+
+ if (respSipCode.isPresent()) {
+ int sipCode = respSipCode.get();
+ if (sipCode == NetworkSipCode.SIP_CODE_NOT_FOUND ||
+ sipCode == NetworkSipCode.SIP_CODE_METHOD_NOT_ALLOWED ||
+ sipCode == NetworkSipCode.SIP_CODE_DOES_NOT_EXIST_ANYWHERE) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * This method convert from the command error code which are defined in the
+ * RcsCapabilityExchangeImplBase to the Capabilities error code which are defined in the
+ * RcsUceAdapter.
+ */
+ public static int getCapabilityErrorFromCommandError(@CommandCode int cmdError) {
+ int uceError;
+ switch (cmdError) {
+ case RcsCapabilityExchangeImplBase.COMMAND_CODE_SERVICE_UNKNOWN:
+ case RcsCapabilityExchangeImplBase.COMMAND_CODE_GENERIC_FAILURE:
+ case RcsCapabilityExchangeImplBase.COMMAND_CODE_INVALID_PARAM:
+ case RcsCapabilityExchangeImplBase.COMMAND_CODE_FETCH_ERROR:
+ case RcsCapabilityExchangeImplBase.COMMAND_CODE_NOT_SUPPORTED:
+ case RcsCapabilityExchangeImplBase.COMMAND_CODE_NO_CHANGE:
+ uceError = RcsUceAdapter.ERROR_GENERIC_FAILURE;
+ break;
+ case RcsCapabilityExchangeImplBase.COMMAND_CODE_NOT_FOUND:
+ uceError = RcsUceAdapter.ERROR_NOT_FOUND;
+ break;
+ case RcsCapabilityExchangeImplBase.COMMAND_CODE_REQUEST_TIMEOUT:
+ uceError = RcsUceAdapter.ERROR_REQUEST_TIMEOUT;
+ break;
+ case RcsCapabilityExchangeImplBase.COMMAND_CODE_INSUFFICIENT_MEMORY:
+ uceError = RcsUceAdapter.ERROR_INSUFFICIENT_MEMORY;
+ break;
+ case RcsCapabilityExchangeImplBase.COMMAND_CODE_LOST_NETWORK_CONNECTION:
+ uceError = RcsUceAdapter.ERROR_LOST_NETWORK;
+ break;
+ case RcsCapabilityExchangeImplBase.COMMAND_CODE_SERVICE_UNAVAILABLE:
+ uceError = RcsUceAdapter.ERROR_SERVER_UNAVAILABLE;
+ break;
+ default:
+ uceError = RcsUceAdapter.ERROR_GENERIC_FAILURE;
+ break;
+ }
+ return uceError;
+ }
+
+ /**
+ * Convert the SIP error code which sent by ImsService to the capability error code.
+ */
+ public static int getCapabilityErrorFromSipCode(CapabilityRequestResponse response) {
+ int sipError;
+ String respReason;
+ // Check the sip code in the Reason header first if the Reason Header is present.
+ if (response.getReasonHeaderCause().isPresent()) {
+ sipError = response.getReasonHeaderCause().get();
+ respReason = response.getReasonHeaderText().orElse("");
+ } else {
+ sipError = response.getNetworkRespSipCode().orElse(-1);
+ respReason = response.getReasonPhrase().orElse("");
+ }
+ return NetworkSipCode.getCapabilityErrorFromSipCode(sipError, respReason,
+ UceController.REQUEST_TYPE_CAPABILITY);
+ }
+
+ @Override
+ public synchronized String toString() {
+ StringBuilder builder = new StringBuilder();
+ return builder.append("RequestInternalError=").append(mRequestInternalError.orElse(-1))
+ .append(", CommandErrorCode=").append(mCommandError.orElse(-1))
+ .append(", NetworkResponseCode=").append(mNetworkRespSipCode.orElse(-1))
+ .append(", NetworkResponseReason=").append(mReasonPhrase.orElse(""))
+ .append(", ReasonHeaderCause=").append(mReasonHeaderCause.orElse(-1))
+ .append(", ReasonHeaderText=").append(mReasonHeaderText.orElse(""))
+ .append(", TerminatedReason=").append(mTerminatedReason.orElse(""))
+ .append(", RetryAfterMillis=").append(mRetryAfterMillis.orElse(0L))
+ .append(", Terminated resource size=" + mTerminatedResource.size())
+ .append(", cached capability size=" + mCachedCapabilityList.size())
+ .append(", Updated capability size=" + mUpdatedCapabilityList.size())
+ .append(", RemoteCaps size=" + mRemoteCaps.size())
+ .toString();
+ }
+}
diff --git a/src/java/com/android/ims/rcs/uce/request/OptionsRequest.java b/src/java/com/android/ims/rcs/uce/request/OptionsRequest.java
new file mode 100644
index 00000000..df5cebbb
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/request/OptionsRequest.java
@@ -0,0 +1,189 @@
+/*
+ * Copyright (c) 2021 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.ims.rcs.uce.request;
+
+import static android.telephony.ims.RcsContactUceCapability.SOURCE_TYPE_NETWORK;
+
+import android.annotation.NonNull;
+import android.net.Uri;
+import android.os.RemoteException;
+import android.telephony.ims.RcsContactUceCapability;
+import android.telephony.ims.RcsUceAdapter;
+import android.telephony.ims.aidl.IOptionsResponseCallback;
+import android.telephony.ims.stub.RcsCapabilityExchangeImplBase.CommandCode;
+
+import com.android.ims.rcs.uce.options.OptionsController;
+import com.android.ims.rcs.uce.request.UceRequestManager.RequestManagerCallback;
+import com.android.ims.rcs.uce.util.NetworkSipCode;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * The UceRequest to request the capabilities when the OPTIONS mechanism is supported by the
+ * network.
+ */
+public class OptionsRequest extends CapabilityRequest {
+
+ // The result callback of the capabilities request from the IMS service.
+ private IOptionsResponseCallback mResponseCallback = new IOptionsResponseCallback.Stub() {
+ @Override
+ public void onCommandError(int code) {
+ OptionsRequest.this.onCommandError(code);
+ }
+
+ @Override
+ public void onNetworkResponse(int sipCode, String reason, List<String> remoteCaps) {
+ OptionsRequest.this.onNetworkResponse(sipCode, reason, remoteCaps);
+ }
+ };
+
+ private Uri mContactUri;
+ private OptionsController mOptionsController;
+
+ public OptionsRequest(int subId, @UceRequestType int requestType,
+ RequestManagerCallback taskMgrCallback, OptionsController optionsController) {
+ super(subId, requestType, taskMgrCallback);
+ mOptionsController = optionsController;
+ logd("OptionsRequest created");
+ }
+
+ @VisibleForTesting
+ public OptionsRequest(int subId, @UceRequestType int requestType,
+ RequestManagerCallback taskMgrCallback, OptionsController optionsController,
+ CapabilityRequestResponse requestResponse) {
+ super(subId, requestType, taskMgrCallback, requestResponse);
+ mOptionsController = optionsController;
+ }
+
+ @Override
+ public void onFinish() {
+ mOptionsController = null;
+ super.onFinish();
+ logd("OptionsRequest finish");
+ }
+
+ @Override
+ public void requestCapabilities(@NonNull List<Uri> requestCapUris) {
+ OptionsController optionsController = mOptionsController;
+ if (optionsController == null) {
+ logw("requestCapabilities: request is finished");
+ mRequestResponse.setRequestInternalError(RcsUceAdapter.ERROR_GENERIC_FAILURE);
+ mRequestManagerCallback.notifyRequestError(mCoordinatorId, mTaskId);
+ return;
+ }
+
+ // Get the device's capabilities to send to the remote client.
+ RcsContactUceCapability deviceCap = mRequestManagerCallback.getDeviceCapabilities(
+ RcsContactUceCapability.CAPABILITY_MECHANISM_OPTIONS);
+ if (deviceCap == null) {
+ logw("requestCapabilities: Cannot get device capabilities");
+ mRequestResponse.setRequestInternalError(RcsUceAdapter.ERROR_GENERIC_FAILURE);
+ mRequestManagerCallback.notifyRequestError(mCoordinatorId, mTaskId);
+ return;
+ }
+
+ mContactUri = requestCapUris.get(0);
+ Set<String> featureTags = deviceCap.getFeatureTags();
+
+ logi("requestCapabilities: featureTag size=" + featureTags.size());
+ try {
+ // Send the capabilities request.
+ optionsController.sendCapabilitiesRequest(mContactUri, featureTags, mResponseCallback);
+ // Setup the timeout timer.
+ setupRequestTimeoutTimer();
+ } catch (RemoteException e) {
+ logw("requestCapabilities exception: " + e);
+ mRequestResponse.setRequestInternalError(RcsUceAdapter.ERROR_GENERIC_FAILURE);
+ mRequestManagerCallback.notifyRequestError(mCoordinatorId, mTaskId);
+ }
+ }
+
+ // Receive the command error callback which is triggered by IOptionsResponseCallback.
+ private void onCommandError(@CommandCode int cmdError) {
+ logd("onCommandError: error code=" + cmdError);
+ if (mIsFinished) {
+ logw("onCommandError: The request is already finished");
+ return;
+ }
+ mRequestResponse.setCommandError(cmdError);
+ mRequestManagerCallback.notifyCommandError(mCoordinatorId, mTaskId);
+ }
+
+ // Receive the network response callback which is triggered by IOptionsResponseCallback.
+ private void onNetworkResponse(int sipCode, String reason, List<String> remoteCaps) {
+ logd("onNetworkResponse: sipCode=" + sipCode + ", reason=" + reason
+ + ", remoteCap size=" + ((remoteCaps == null) ? "null" : remoteCaps.size()));
+ if (mIsFinished) {
+ logw("onNetworkResponse: The request is already finished");
+ return;
+ }
+
+ if (remoteCaps == null) {
+ remoteCaps = Collections.EMPTY_LIST;
+ }
+
+ // Set the all the results to the request response.
+ mRequestResponse.setNetworkResponseCode(sipCode, reason);
+ mRequestResponse.setRemoteCapabilities(new HashSet<>(remoteCaps));
+ RcsContactUceCapability contactCapabilities = getContactCapabilities(mContactUri, sipCode,
+ new HashSet<>(remoteCaps));
+ mRequestResponse.addUpdatedCapabilities(Collections.singletonList(contactCapabilities));
+
+ // Notify that the network response is received.
+ mRequestManagerCallback.notifyNetworkResponse(mCoordinatorId, mTaskId);
+ }
+
+ /**
+ * Convert the remote capabilities from string list type to RcsContactUceCapability.
+ */
+ private RcsContactUceCapability getContactCapabilities(Uri contact, int sipCode,
+ Set<String> featureTags) {
+ int requestResult = RcsContactUceCapability.REQUEST_RESULT_FOUND;
+ if (!mRequestResponse.isNetworkResponseOK()) {
+ switch (sipCode) {
+ case NetworkSipCode.SIP_CODE_REQUEST_TIMEOUT:
+ // Intentional fallthrough
+ case NetworkSipCode.SIP_CODE_TEMPORARILY_UNAVAILABLE:
+ requestResult = RcsContactUceCapability.REQUEST_RESULT_NOT_ONLINE;
+ break;
+ case NetworkSipCode.SIP_CODE_NOT_FOUND:
+ // Intentional fallthrough
+ case NetworkSipCode.SIP_CODE_DOES_NOT_EXIST_ANYWHERE:
+ requestResult = RcsContactUceCapability.REQUEST_RESULT_NOT_FOUND;
+ break;
+ default:
+ requestResult = RcsContactUceCapability.REQUEST_RESULT_NOT_FOUND;
+ break;
+ }
+ }
+
+ RcsContactUceCapability.OptionsBuilder optionsBuilder
+ = new RcsContactUceCapability.OptionsBuilder(contact, SOURCE_TYPE_NETWORK);
+ optionsBuilder.setRequestResult(requestResult);
+ optionsBuilder.addFeatureTags(featureTags);
+ return optionsBuilder.build();
+ }
+
+ @VisibleForTesting
+ public IOptionsResponseCallback getResponseCallback() {
+ return mResponseCallback;
+ }
+}
diff --git a/src/java/com/android/ims/rcs/uce/request/OptionsRequestCoordinator.java b/src/java/com/android/ims/rcs/uce/request/OptionsRequestCoordinator.java
new file mode 100644
index 00000000..a150dd6d
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/request/OptionsRequestCoordinator.java
@@ -0,0 +1,360 @@
+/*
+ * Copyright (c) 2021 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.ims.rcs.uce.request;
+
+import static android.telephony.ims.stub.RcsCapabilityExchangeImplBase.COMMAND_CODE_GENERIC_FAILURE;
+
+import android.os.RemoteException;
+import android.telephony.ims.RcsContactUceCapability;
+import android.telephony.ims.RcsUceAdapter;
+import android.telephony.ims.aidl.IRcsUceControllerCallback;
+
+import com.android.ims.rcs.uce.request.UceRequestManager.RequestManagerCallback;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * Responsible for the communication and interaction between OptionsRequests and triggering
+ * the callback to notify the result of the capabilities request.
+ */
+public class OptionsRequestCoordinator extends UceRequestCoordinator {
+ /**
+ * The builder of the OptionsRequestCoordinator.
+ */
+ public static final class Builder {
+ private OptionsRequestCoordinator mRequestCoordinator;
+
+ public Builder(int subId, Collection<UceRequest> requests,
+ RequestManagerCallback callback) {
+ mRequestCoordinator = new OptionsRequestCoordinator(subId, requests, callback);
+ }
+
+ public Builder setCapabilitiesCallback(IRcsUceControllerCallback callback) {
+ mRequestCoordinator.setCapabilitiesCallback(callback);
+ return this;
+ }
+
+ public OptionsRequestCoordinator build() {
+ return mRequestCoordinator;
+ }
+ }
+
+ /**
+ * Different request updated events will create different {@link RequestResult}. Define the
+ * interface to get the {@link RequestResult} instance according to the given task ID and
+ * {@link CapabilityRequestResponse}.
+ */
+ @FunctionalInterface
+ private interface RequestResultCreator {
+ RequestResult createRequestResult(long taskId, CapabilityRequestResponse response);
+ }
+
+ // The RequestResult creator of the request error.
+ private static final RequestResultCreator sRequestErrorCreator = (taskId, response) -> {
+ int errorCode = response.getRequestInternalError().orElse(DEFAULT_ERROR_CODE);
+ long retryAfter = response.getRetryAfterMillis();
+ return RequestResult.createFailedResult(taskId, errorCode, retryAfter);
+ };
+
+ // The RequestResult creator of the request command error.
+ private static final RequestResultCreator sCommandErrorCreator = (taskId, response) -> {
+ int cmdError = response.getCommandError().orElse(COMMAND_CODE_GENERIC_FAILURE);
+ int errorCode = CapabilityRequestResponse.getCapabilityErrorFromCommandError(cmdError);
+ long retryAfter = response.getRetryAfterMillis();
+ return RequestResult.createFailedResult(taskId, errorCode, retryAfter);
+ };
+
+ // The RequestResult creator of the network response.
+ private static final RequestResultCreator sNetworkRespCreator = (taskId, response) -> {
+ if (response.isNetworkResponseOK()) {
+ return RequestResult.createSuccessResult(taskId);
+ } else {
+ int errorCode = CapabilityRequestResponse.getCapabilityErrorFromSipCode(response);
+ long retryAfter = response.getRetryAfterMillis();
+ return RequestResult.createFailedResult(taskId, errorCode, retryAfter);
+ }
+ };
+
+ // The RequestResult creator for does not need to request from the network.
+ private static final RequestResultCreator sNotNeedRequestFromNetworkCreator =
+ (taskId, response) -> RequestResult.createSuccessResult(taskId);
+
+ // The RequestResult creator of the request timeout.
+ private static final RequestResultCreator sRequestTimeoutCreator =
+ (taskId, response) -> RequestResult.createFailedResult(taskId,
+ RcsUceAdapter.ERROR_REQUEST_TIMEOUT, 0L);
+
+ // The callback to notify the result of the capabilities request.
+ private IRcsUceControllerCallback mCapabilitiesCallback;
+
+ private OptionsRequestCoordinator(int subId, Collection<UceRequest> requests,
+ RequestManagerCallback requestMgrCallback) {
+ super(subId, requests, requestMgrCallback);
+ logd("OptionsRequestCoordinator: created");
+ }
+
+ private void setCapabilitiesCallback(IRcsUceControllerCallback callback) {
+ mCapabilitiesCallback = callback;
+ }
+
+ @Override
+ public void onFinish() {
+ logd("OptionsRequestCoordinator: onFinish");
+ mCapabilitiesCallback = null;
+ super.onFinish();
+ }
+
+ @Override
+ public void onRequestUpdated(long taskId, @UceRequestUpdate int event) {
+ if (mIsFinished) return;
+ OptionsRequest request = (OptionsRequest) getUceRequest(taskId);
+ if (request == null) {
+ logw("onRequestUpdated: Cannot find OptionsRequest taskId=" + taskId);
+ return;
+ }
+
+ logd("onRequestUpdated(OptionsRequest): taskId=" + taskId + ", event=" +
+ REQUEST_EVENT_DESC.get(event));
+
+ switch (event) {
+ case REQUEST_UPDATE_ERROR:
+ handleRequestError(request);
+ break;
+ case REQUEST_UPDATE_COMMAND_ERROR:
+ handleCommandError(request);
+ break;
+ case REQUEST_UPDATE_NETWORK_RESPONSE:
+ handleNetworkResponse(request);
+ break;
+ case REQUEST_UPDATE_CACHED_CAPABILITY_UPDATE:
+ handleCachedCapabilityUpdated(request);
+ break;
+ case REQUEST_UPDATE_NO_NEED_REQUEST_FROM_NETWORK:
+ handleNoNeedRequestFromNetwork(request);
+ break;
+ case REQUEST_UPDATE_TIMEOUT:
+ handleRequestTimeout(request);
+ break;
+ default:
+ logw("onRequestUpdated(OptionsRequest): invalid event " + event);
+ break;
+ }
+
+ // End this instance if all the UceRequests in the coordinator are finished.
+ checkAndFinishRequestCoordinator();
+ }
+
+ /**
+ * Finish the OptionsRequest because it has encountered error.
+ */
+ private void handleRequestError(OptionsRequest request) {
+ CapabilityRequestResponse response = request.getRequestResponse();
+ logd("handleRequestError: " + request.toString());
+
+ // Finish this request.
+ request.onFinish();
+
+ // Remove this request from the activated collection and notify RequestManager.
+ Long taskId = request.getTaskId();
+ RequestResult requestResult = sRequestErrorCreator.createRequestResult(taskId, response);
+ moveRequestToFinishedCollection(taskId, requestResult);
+ }
+
+ /**
+ * This method is called when the given OptionsRequest received the onCommandError callback
+ * from the ImsService.
+ */
+ private void handleCommandError(OptionsRequest request) {
+ CapabilityRequestResponse response = request.getRequestResponse();
+ logd("handleCommandError: " + request.toString());
+
+ // Finish this request.
+ request.onFinish();
+
+ // Remove this request from the activated collection and notify RequestManager.
+ Long taskId = request.getTaskId();
+ RequestResult requestResult = sCommandErrorCreator.createRequestResult(taskId, response);
+ moveRequestToFinishedCollection(taskId, requestResult);
+ }
+
+ /**
+ * This method is called when the given OptionsRequest received the onNetworkResponse
+ * callback from the ImsService.
+ */
+ private void handleNetworkResponse(OptionsRequest request) {
+ CapabilityRequestResponse response = request.getRequestResponse();
+ logd("handleNetworkResponse: " + response.toString());
+
+ List<RcsContactUceCapability> updatedCapList = response.getUpdatedContactCapability();
+ if (!updatedCapList.isEmpty()) {
+ // Save the capabilities and trigger the capabilities callback
+ mRequestManagerCallback.saveCapabilities(updatedCapList);
+ triggerCapabilitiesReceivedCallback(updatedCapList);
+ response.removeUpdatedCapabilities(updatedCapList);
+ }
+
+ // Finish this request.
+ request.onFinish();
+
+ // Remove this request from the activated collection and notify RequestManager.
+ Long taskId = request.getTaskId();
+ RequestResult requestResult = sNetworkRespCreator.createRequestResult(taskId, response);
+ moveRequestToFinishedCollection(taskId, requestResult);
+ }
+
+ /**
+ * This method is called when the OptionsRequest retrieves the capabilities from cache.
+ */
+ private void handleCachedCapabilityUpdated(OptionsRequest request) {
+ CapabilityRequestResponse response = request.getRequestResponse();
+ Long taskId = request.getTaskId();
+ List<RcsContactUceCapability> cachedCapList = response.getCachedContactCapability();
+ logd("handleCachedCapabilityUpdated: taskId=" + taskId + ", CapRequestResp=" + response);
+
+ if (cachedCapList.isEmpty()) {
+ return;
+ }
+
+ // Trigger the capabilities updated callback.
+ triggerCapabilitiesReceivedCallback(cachedCapList);
+ response.removeCachedContactCapabilities();
+ }
+
+ /**
+ * This method is called when all the capabilities can be retrieved from the cached and it does
+ * not need to request capabilities from the network.
+ */
+ private void handleNoNeedRequestFromNetwork(OptionsRequest request) {
+ CapabilityRequestResponse response = request.getRequestResponse();
+ logd("handleNoNeedRequestFromNetwork: " + response.toString());
+
+ // Finish this request.
+ request.onFinish();
+
+ // Remove this request from the activated collection and notify RequestManager.
+ long taskId = request.getTaskId();
+ RequestResult requestResult = sNotNeedRequestFromNetworkCreator.createRequestResult(taskId,
+ response);
+ moveRequestToFinishedCollection(taskId, requestResult);
+ }
+
+ /**
+ * This method is called when the framework does not receive receive the result for
+ * capabilities request.
+ */
+ private void handleRequestTimeout(OptionsRequest request) {
+ CapabilityRequestResponse response = request.getRequestResponse();
+ logd("handleRequestTimeout: " + response.toString());
+
+ // Finish this request.
+ request.onFinish();
+
+ // Remove this request from the activated collection and notify RequestManager.
+ long taskId = request.getTaskId();
+ RequestResult requestResult = sRequestTimeoutCreator.createRequestResult(taskId,
+ response);
+ moveRequestToFinishedCollection(taskId, requestResult);
+ }
+
+ /**
+ * Trigger the capabilities updated callback.
+ */
+ private void triggerCapabilitiesReceivedCallback(List<RcsContactUceCapability> capList) {
+ try {
+ logd("triggerCapabilitiesCallback: size=" + capList.size());
+ mCapabilitiesCallback.onCapabilitiesReceived(capList);
+ } catch (RemoteException e) {
+ logw("triggerCapabilitiesCallback exception: " + e);
+ } finally {
+ logd("triggerCapabilitiesCallback: done");
+ }
+ }
+
+ /**
+ * Trigger the onComplete callback to notify the request is completed.
+ */
+ private void triggerCompletedCallback() {
+ try {
+ logd("triggerCompletedCallback");
+ mCapabilitiesCallback.onComplete();
+ } catch (RemoteException e) {
+ logw("triggerCompletedCallback exception: " + e);
+ } finally {
+ logd("triggerCompletedCallback: done");
+ }
+ }
+
+ /**
+ * Trigger the onError callback to notify the request is failed.
+ */
+ private void triggerErrorCallback(int errorCode, long retryAfterMillis) {
+ try {
+ logd("triggerErrorCallback: errorCode=" + errorCode + ", retry=" + retryAfterMillis);
+ mCapabilitiesCallback.onError(errorCode, retryAfterMillis);
+ } catch (RemoteException e) {
+ logw("triggerErrorCallback exception: " + e);
+ } finally {
+ logd("triggerErrorCallback: done");
+ }
+ }
+
+ private void checkAndFinishRequestCoordinator() {
+ synchronized (mCollectionLock) {
+ // Return because there are requests running.
+ if (!mActivatedRequests.isEmpty()) {
+ return;
+ }
+
+ // All the requests has finished, find the request which has the max retryAfter time.
+ // If the result is empty, it means all the request are success.
+ Optional<RequestResult> optRequestResult =
+ mFinishedRequests.values().stream()
+ .filter(result -> !result.isRequestSuccess())
+ .max(Comparator.comparingLong(result ->
+ result.getRetryMillis().orElse(-1L)));
+
+ // Trigger the callback
+ if (optRequestResult.isPresent()) {
+ RequestResult result = optRequestResult.get();
+ int errorCode = result.getErrorCode().orElse(DEFAULT_ERROR_CODE);
+ long retryAfter = result.getRetryMillis().orElse(0L);
+ triggerErrorCallback(errorCode, retryAfter);
+ } else {
+ triggerCompletedCallback();
+ }
+
+ // Notify UceRequestManager to remove this instance from the collection.
+ mRequestManagerCallback.notifyRequestCoordinatorFinished(mCoordinatorId);
+
+ logd("checkAndFinishRequestCoordinator(OptionsRequest) done, id=" + mCoordinatorId);
+ }
+ }
+
+ @VisibleForTesting
+ public Collection<UceRequest> getActivatedRequest() {
+ return mActivatedRequests.values();
+ }
+
+ @VisibleForTesting
+ public Collection<RequestResult> getFinishedRequest() {
+ return mFinishedRequests.values();
+ }
+}
diff --git a/src/java/com/android/ims/rcs/uce/request/RemoteOptionsCoordinator.java b/src/java/com/android/ims/rcs/uce/request/RemoteOptionsCoordinator.java
new file mode 100644
index 00000000..c8aa3f77
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/request/RemoteOptionsCoordinator.java
@@ -0,0 +1,187 @@
+/*
+ * Copyright (c) 2021 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.ims.rcs.uce.request;
+
+import static com.android.ims.rcs.uce.util.NetworkSipCode.SIP_CODE_SERVER_INTERNAL_ERROR;
+import static com.android.ims.rcs.uce.util.NetworkSipCode.SIP_SERVICE_UNAVAILABLE;
+
+import android.os.RemoteException;
+import android.telephony.ims.RcsContactUceCapability;
+import android.telephony.ims.aidl.IOptionsRequestCallback;
+
+import com.android.ims.rcs.uce.request.RemoteOptionsRequest.RemoteOptResponse;
+import com.android.ims.rcs.uce.request.UceRequestManager.RequestManagerCallback;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.Collection;
+
+/**
+ * Responsible for the manager the remote options request and triggering the callback to notify
+ * the result of the request.
+ */
+public class RemoteOptionsCoordinator extends UceRequestCoordinator {
+ /**
+ * The builder of the RemoteOptionsCoordinator.
+ */
+ public static final class Builder {
+ RemoteOptionsCoordinator mRemoteOptionsCoordinator;
+
+ public Builder(int subId, Collection<UceRequest> requests, RequestManagerCallback c) {
+ mRemoteOptionsCoordinator = new RemoteOptionsCoordinator(subId, requests, c);
+ }
+
+ public Builder setOptionsRequestCallback(IOptionsRequestCallback callback) {
+ mRemoteOptionsCoordinator.setOptionsRequestCallback(callback);
+ return this;
+ }
+
+ public RemoteOptionsCoordinator build() {
+ return mRemoteOptionsCoordinator;
+ }
+ }
+
+ /**
+ * Different request updated events will create different {@link RequestResult}. Define the
+ * interface to get the {@link RequestResult} instance according to the given task ID and
+ * {@link RemoteOptResponse}.
+ */
+ @FunctionalInterface
+ private interface RequestResultCreator {
+ RequestResult createRequestResult(long taskId, RemoteOptResponse response);
+ }
+
+ // The RequestResult creator of the remote options response.
+ private static final RequestResultCreator sRemoteResponseCreator = (taskId, response) -> {
+ RcsContactUceCapability capability = response.getRcsContactCapability();
+ if (capability != null) {
+ return RequestResult.createSuccessResult(taskId);
+ } else {
+ int errorCode = response.getErrorSipCode().orElse(SIP_CODE_SERVER_INTERNAL_ERROR);
+ return RequestResult.createFailedResult(taskId, errorCode, 0L);
+ }
+ };
+
+ // The callback to notify the result of the remote options request.
+ private IOptionsRequestCallback mOptionsReqCallback;
+
+ private RemoteOptionsCoordinator(int subId, Collection<UceRequest> requests,
+ RequestManagerCallback requestMgrCallback) {
+ super(subId, requests, requestMgrCallback);
+ logd("RemoteOptionsCoordinator: created");
+ }
+
+ public void setOptionsRequestCallback(IOptionsRequestCallback callback) {
+ mOptionsReqCallback = callback;
+ }
+
+ @Override
+ public void onFinish() {
+ logd("RemoteOptionsCoordinator: onFinish");
+ mOptionsReqCallback = null;
+ super.onFinish();
+ }
+
+ @Override
+ public void onRequestUpdated(long taskId, int event) {
+ if (mIsFinished) return;
+ RemoteOptionsRequest request = (RemoteOptionsRequest) getUceRequest(taskId);
+ if (request == null) {
+ logw("onRequestUpdated: Cannot find RemoteOptionsRequest taskId=" + taskId);
+ return;
+ }
+
+ logd("onRequestUpdated: taskId=" + taskId + ", event=" + REQUEST_EVENT_DESC.get(event));
+ switch (event) {
+ case REQUEST_UPDATE_REMOTE_REQUEST_DONE:
+ handleRemoteRequestDone(request);
+ break;
+ default:
+ logw("onRequestUpdated: invalid event " + event);
+ break;
+ }
+
+ // End this instance if all the UceRequests in the coordinator are finished.
+ checkAndFinishRequestCoordinator();
+ }
+
+ private void handleRemoteRequestDone(RemoteOptionsRequest request) {
+ // Trigger the options request callback
+ RemoteOptResponse response = request.getRemoteOptResponse();
+ RcsContactUceCapability capability = response.getRcsContactCapability();
+ if (capability != null) {
+ boolean isNumberBlocked = response.isNumberBlocked();
+ triggerOptionsReqCallback(capability, isNumberBlocked);
+ } else {
+ int errorCode = response.getErrorSipCode().orElse(SIP_CODE_SERVER_INTERNAL_ERROR);
+ String reason = response.getErrorReason().orElse(SIP_SERVICE_UNAVAILABLE);
+ triggerOptionsReqWithErrorCallback(errorCode, reason);
+ }
+
+ // Finish this request.
+ request.onFinish();
+
+ // Remove this request from the activated collection and notify RequestManager.
+ Long taskId = request.getTaskId();
+ RequestResult requestResult = sRemoteResponseCreator.createRequestResult(taskId, response);
+ moveRequestToFinishedCollection(taskId, requestResult);
+ }
+
+ private void triggerOptionsReqCallback(RcsContactUceCapability deviceCaps,
+ boolean isRemoteNumberBlocked) {
+ try {
+ logd("triggerOptionsReqCallback: start");
+ mOptionsReqCallback.respondToCapabilityRequest(deviceCaps, isRemoteNumberBlocked);
+ } catch (RemoteException e) {
+ logw("triggerOptionsReqCallback exception: " + e);
+ } finally {
+ logd("triggerOptionsReqCallback: done");
+ }
+ }
+
+ private void triggerOptionsReqWithErrorCallback(int errorCode, String reason) {
+ try {
+ logd("triggerOptionsReqWithErrorCallback: start");
+ mOptionsReqCallback.respondToCapabilityRequestWithError(errorCode, reason);
+ } catch (RemoteException e) {
+ logw("triggerOptionsReqWithErrorCallback exception: " + e);
+ } finally {
+ logd("triggerOptionsReqWithErrorCallback: done");
+ }
+ }
+
+ private void checkAndFinishRequestCoordinator() {
+ synchronized (mCollectionLock) {
+ // Return because there are requests running.
+ if (!mActivatedRequests.isEmpty()) {
+ return;
+ }
+ // Notify UceRequestManager to remove this instance from the collection.
+ mRequestManagerCallback.notifyRequestCoordinatorFinished(mCoordinatorId);
+ logd("checkAndFinishRequestCoordinator: id=" + mCoordinatorId);
+ }
+ }
+
+ @VisibleForTesting
+ public Collection<UceRequest> getActivatedRequest() {
+ return mActivatedRequests.values();
+ }
+
+ @VisibleForTesting
+ public Collection<RequestResult> getFinishedRequest() {
+ return mFinishedRequests.values();
+ }
+}
diff --git a/src/java/com/android/ims/rcs/uce/request/RemoteOptionsRequest.java b/src/java/com/android/ims/rcs/uce/request/RemoteOptionsRequest.java
new file mode 100644
index 00000000..17e59ef1
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/request/RemoteOptionsRequest.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright (c) 2021 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.ims.rcs.uce.request;
+
+import static android.telephony.ims.RcsContactUceCapability.CAPABILITY_MECHANISM_OPTIONS;
+import static android.telephony.ims.RcsContactUceCapability.SOURCE_TYPE_NETWORK;
+
+import android.net.Uri;
+import android.telephony.ims.RcsContactUceCapability;
+import android.util.Log;
+
+import com.android.ims.rcs.uce.request.UceRequestManager.RequestManagerCallback;
+import com.android.ims.rcs.uce.util.FeatureTags;
+import com.android.ims.rcs.uce.util.NetworkSipCode;
+import com.android.ims.rcs.uce.util.UceUtils;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * Handle the OPTIONS request from the network.
+ */
+public class RemoteOptionsRequest implements UceRequest {
+
+ private static final String LOG_TAG = UceUtils.getLogPrefix() + "RemoteOptRequest";
+
+ /**
+ * The response of the remote capability request.
+ */
+ public static class RemoteOptResponse {
+ private boolean mIsNumberBlocked;
+ private RcsContactUceCapability mRcsContactCapability;
+ private Optional<Integer> mErrorSipCode;
+ private Optional<String> mErrorReason;
+
+ public RemoteOptResponse() {
+ mErrorSipCode = Optional.empty();
+ mErrorReason = Optional.empty();
+ }
+
+ void setRespondToRequest(RcsContactUceCapability capability, boolean isBlocked) {
+ mIsNumberBlocked = isBlocked;
+ mRcsContactCapability = capability;
+ }
+
+ void setRespondToRequestWithError(int code, String reason) {
+ mErrorSipCode = Optional.of(code);
+ mErrorReason = Optional.of(reason);
+ }
+
+ public boolean isNumberBlocked() {
+ return mIsNumberBlocked;
+ }
+
+ public RcsContactUceCapability getRcsContactCapability() {
+ return mRcsContactCapability;
+ }
+
+ public Optional<Integer> getErrorSipCode() {
+ return mErrorSipCode;
+ }
+
+ public Optional<String> getErrorReason() {
+ return mErrorReason;
+ }
+ }
+
+ private final int mSubId;
+ private final long mTaskId;
+ private volatile long mCoordinatorId;
+ private volatile boolean mIsFinished;
+ private volatile boolean mIsRemoteNumberBlocked;
+
+ private List<Uri> mUriList;
+ private final List<String> mRemoteFeatureTags;
+ private final RemoteOptResponse mRemoteOptResponse;
+ private final RequestManagerCallback mRequestManagerCallback;
+
+ public RemoteOptionsRequest(int subId, RequestManagerCallback requestMgrCallback) {
+ mSubId = subId;
+ mTaskId = UceUtils.generateTaskId();
+ mRemoteFeatureTags = new ArrayList<>();
+ mRemoteOptResponse = new RemoteOptResponse();
+ mRequestManagerCallback = requestMgrCallback;
+ logd("created");
+ }
+
+ @Override
+ public void setRequestCoordinatorId(long coordinatorId) {
+ mCoordinatorId = coordinatorId;
+ }
+
+ @Override
+ public long getRequestCoordinatorId() {
+ return mCoordinatorId;
+ }
+
+ @Override
+ public long getTaskId() {
+ return mTaskId;
+ }
+
+ @Override
+ public void onFinish() {
+ mIsFinished = true;
+ }
+
+ @Override
+ public void setContactUri(List<Uri> uris) {
+ mUriList = uris;
+ }
+
+ public void setRemoteFeatureTags(List<String> remoteFeatureTags) {
+ remoteFeatureTags.forEach(mRemoteFeatureTags::add);
+ }
+
+ public void setIsRemoteNumberBlocked(boolean isBlocked) {
+ mIsRemoteNumberBlocked = isBlocked;
+ }
+
+ /**
+ * @return The response of this request.
+ */
+ public RemoteOptResponse getRemoteOptResponse() {
+ return mRemoteOptResponse;
+ }
+
+ @Override
+ public void executeRequest() {
+ logd("executeRequest");
+ try {
+ executeRequestInternal();
+ } catch (Exception e) {
+ logw("executeRequest: exception " + e);
+ setResponseWithError(NetworkSipCode.SIP_CODE_SERVER_INTERNAL_ERROR,
+ NetworkSipCode.SIP_INTERNAL_SERVER_ERROR);
+ } finally {
+ mRequestManagerCallback.notifyRemoteRequestDone(mCoordinatorId, mTaskId);
+ }
+ }
+
+ private void executeRequestInternal() {
+ if (mUriList == null || mUriList.isEmpty()) {
+ logw("executeRequest: uri is empty");
+ setResponseWithError(NetworkSipCode.SIP_CODE_BAD_REQUEST,
+ NetworkSipCode.SIP_BAD_REQUEST);
+ return;
+ }
+
+ if (mIsFinished) {
+ logw("executeRequest: This request is finished");
+ setResponseWithError(NetworkSipCode.SIP_CODE_SERVICE_UNAVAILABLE,
+ NetworkSipCode.SIP_SERVICE_UNAVAILABLE);
+ return;
+ }
+
+ // Store the remote capabilities
+ Uri contactUri = mUriList.get(0);
+ RcsContactUceCapability remoteCaps = FeatureTags.getContactCapability(contactUri,
+ SOURCE_TYPE_NETWORK, mRemoteFeatureTags);
+ mRequestManagerCallback.saveCapabilities(Collections.singletonList(remoteCaps));
+
+ // Get the device's capabilities and trigger the request callback
+ RcsContactUceCapability deviceCaps = mRequestManagerCallback.getDeviceCapabilities(
+ CAPABILITY_MECHANISM_OPTIONS);
+ if (deviceCaps == null) {
+ logw("executeRequest: The device's capabilities is empty");
+ setResponseWithError(NetworkSipCode.SIP_CODE_SERVER_INTERNAL_ERROR,
+ NetworkSipCode.SIP_INTERNAL_SERVER_ERROR);
+ } else {
+ logd("executeRequest: Respond to capability request, blocked="
+ + mIsRemoteNumberBlocked);
+ setResponse(deviceCaps, mIsRemoteNumberBlocked);
+ }
+ }
+
+ private void setResponse(RcsContactUceCapability deviceCaps,
+ boolean isRemoteNumberBlocked) {
+ mRemoteOptResponse.setRespondToRequest(deviceCaps, isRemoteNumberBlocked);
+ }
+
+ private void setResponseWithError(int errorCode, String reason) {
+ mRemoteOptResponse.setRespondToRequestWithError(errorCode, reason);
+ }
+
+ private void logd(String log) {
+ Log.d(LOG_TAG, getLogPrefix().append(log).toString());
+ }
+
+ private void logw(String log) {
+ Log.d(LOG_TAG, getLogPrefix().append(log).toString());
+ }
+
+ private StringBuilder getLogPrefix() {
+ StringBuilder builder = new StringBuilder("[");
+ builder.append(mSubId).append("][taskId=").append(mTaskId).append("] ");
+ return builder;
+ }
+}
diff --git a/src/java/com/android/ims/rcs/uce/request/SubscribeRequest.java b/src/java/com/android/ims/rcs/uce/request/SubscribeRequest.java
new file mode 100644
index 00000000..2b5e91a9
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/request/SubscribeRequest.java
@@ -0,0 +1,234 @@
+/*
+ * 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.ims.rcs.uce.request;
+
+import android.annotation.NonNull;
+import android.net.Uri;
+import android.os.RemoteException;
+import android.telephony.ims.RcsContactTerminatedReason;
+import android.telephony.ims.RcsContactUceCapability;
+import android.telephony.ims.RcsUceAdapter;
+import android.telephony.ims.aidl.ISubscribeResponseCallback;
+import android.telephony.ims.stub.RcsCapabilityExchangeImplBase.CommandCode;
+
+import com.android.ims.rcs.uce.presence.pidfparser.PidfParser;
+import com.android.ims.rcs.uce.presence.pidfparser.PidfParserUtils;
+import com.android.ims.rcs.uce.presence.subscribe.SubscribeController;
+import com.android.ims.rcs.uce.request.UceRequestManager.RequestManagerCallback;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+/**
+ * The UceRequest to request the capabilities when the presence mechanism is supported by the
+ * network.
+ */
+public class SubscribeRequest extends CapabilityRequest {
+
+ // The result callback of the capabilities request from IMS service.
+ private final ISubscribeResponseCallback mResponseCallback =
+ new ISubscribeResponseCallback.Stub() {
+ @Override
+ public void onCommandError(int code) {
+ SubscribeRequest.this.onCommandError(code);
+ }
+ @Override
+ public void onNetworkResponse(int code, String reason) {
+ SubscribeRequest.this.onNetworkResponse(code, reason);
+ }
+ @Override
+ public void onNetworkRespHeader(int code, String reasonPhrase,
+ int reasonHeaderCause, String reasonHeaderText) {
+ SubscribeRequest.this.onNetworkResponse(code, reasonPhrase, reasonHeaderCause,
+ reasonHeaderText);
+ }
+ @Override
+ public void onNotifyCapabilitiesUpdate(List<String> pidfXmls) {
+ SubscribeRequest.this.onCapabilitiesUpdate(pidfXmls);
+ }
+ @Override
+ public void onResourceTerminated(List<RcsContactTerminatedReason> terminatedList) {
+ SubscribeRequest.this.onResourceTerminated(terminatedList);
+ }
+ @Override
+ public void onTerminated(String reason, long retryAfterMillis) {
+ SubscribeRequest.this.onTerminated(reason, retryAfterMillis);
+ }
+ };
+
+ private SubscribeController mSubscribeController;
+
+ public SubscribeRequest(int subId, @UceRequestType int requestType,
+ RequestManagerCallback taskMgrCallback, SubscribeController subscribeController) {
+ super(subId, requestType, taskMgrCallback);
+ mSubscribeController = subscribeController;
+ logd("SubscribeRequest created");
+ }
+
+ @VisibleForTesting
+ public SubscribeRequest(int subId, @UceRequestType int requestType,
+ RequestManagerCallback taskMgrCallback, SubscribeController subscribeController,
+ CapabilityRequestResponse requestResponse) {
+ super(subId, requestType, taskMgrCallback, requestResponse);
+ mSubscribeController = subscribeController;
+ }
+
+ @Override
+ public void onFinish() {
+ mSubscribeController = null;
+ super.onFinish();
+ logd("SubscribeRequest finish");
+ }
+
+ @Override
+ public void requestCapabilities(@NonNull List<Uri> requestCapUris) {
+ SubscribeController subscribeController = mSubscribeController;
+ if (subscribeController == null) {
+ logw("requestCapabilities: request is finished");
+ mRequestResponse.setRequestInternalError(RcsUceAdapter.ERROR_GENERIC_FAILURE);
+ mRequestManagerCallback.notifyRequestError(mCoordinatorId, mTaskId);
+ return;
+ }
+
+ logi("requestCapabilities: size=" + requestCapUris.size());
+ try {
+ // Send the capabilities request.
+ subscribeController.requestCapabilities(requestCapUris, mResponseCallback);
+ // Setup the timeout timer.
+ setupRequestTimeoutTimer();
+ } catch (RemoteException e) {
+ logw("requestCapabilities exception: " + e);
+ mRequestResponse.setRequestInternalError(RcsUceAdapter.ERROR_GENERIC_FAILURE);
+ mRequestManagerCallback.notifyRequestError(mCoordinatorId, mTaskId);
+ }
+ }
+
+ // Receive the command error callback which is triggered by ISubscribeResponseCallback.
+ private void onCommandError(@CommandCode int cmdError) {
+ logd("onCommandError: error code=" + cmdError);
+ if (mIsFinished) {
+ logw("onCommandError: request is already finished");
+ return;
+ }
+ mRequestResponse.setCommandError(cmdError);
+ mRequestManagerCallback.notifyCommandError(mCoordinatorId, mTaskId);
+ }
+
+ // Receive the network response callback which is triggered by ISubscribeResponseCallback.
+ private void onNetworkResponse(int sipCode, String reason) {
+ logd("onNetworkResponse: code=" + sipCode + ", reason=" + reason);
+ if (mIsFinished) {
+ logw("onNetworkResponse: request is already finished");
+ return;
+ }
+ mRequestResponse.setNetworkResponseCode(sipCode, reason);
+ mRequestManagerCallback.notifyNetworkResponse(mCoordinatorId, mTaskId);
+ }
+
+ // Receive the network response callback which is triggered by ISubscribeResponseCallback.
+ private void onNetworkResponse(int sipCode, String reasonPhrase,
+ int reasonHeaderCause, String reasonHeaderText) {
+ logd("onNetworkResponse: code=" + sipCode + ", reasonPhrase=" + reasonPhrase +
+ ", reasonHeaderCause=" + reasonHeaderCause +
+ ", reasonHeaderText=" + reasonHeaderText);
+ if (mIsFinished) {
+ logw("onNetworkResponse: request is already finished");
+ return;
+ }
+ mRequestResponse.setNetworkResponseCode(sipCode, reasonPhrase, reasonHeaderCause,
+ reasonHeaderText);
+ mRequestManagerCallback.notifyNetworkResponse(mCoordinatorId, mTaskId);
+ }
+
+ // Receive the resource terminated callback which is triggered by ISubscribeResponseCallback.
+ private void onResourceTerminated(List<RcsContactTerminatedReason> terminatedResource) {
+ if (mIsFinished) {
+ logw("onResourceTerminated: request is already finished");
+ return;
+ }
+
+ if (terminatedResource == null) {
+ logw("onResourceTerminated: the parameter is null");
+ terminatedResource = Collections.emptyList();
+ }
+
+ logd("onResourceTerminated: size=" + terminatedResource.size());
+
+ // Add the terminated resource into the RequestResponse and notify the RequestManager
+ // to process the RcsContactUceCapabilities update.
+ mRequestResponse.addTerminatedResource(terminatedResource);
+ mRequestManagerCallback.notifyResourceTerminated(mCoordinatorId, mTaskId);
+ }
+
+ // Receive the capabilities update callback which is triggered by ISubscribeResponseCallback.
+ private void onCapabilitiesUpdate(List<String> pidfXml) {
+ if (mIsFinished) {
+ logw("onCapabilitiesUpdate: request is already finished");
+ return;
+ }
+
+ if (pidfXml == null) {
+ logw("onCapabilitiesUpdate: The parameter is null");
+ pidfXml = Collections.EMPTY_LIST;
+ }
+
+ // Convert from the pidf xml to the list of RcsContactUceCapability
+ List<RcsContactUceCapability> capabilityList = pidfXml.stream()
+ .map(pidf -> PidfParser.getRcsContactUceCapability(pidf))
+ .filter(Objects::nonNull)
+ .collect(Collectors.toList());
+
+ // When the given PIDF xml is empty, set the contacts who have not received the
+ // capabilities updated as non-RCS user.
+ if (capabilityList.isEmpty()) {
+ logd("onCapabilitiesUpdate: The capabilities list is empty, Set to non-RCS user.");
+ List<Uri> notReceiveCapUpdatedContactList =
+ mRequestResponse.getNotReceiveCapabilityUpdatedContact();
+ capabilityList = notReceiveCapUpdatedContactList.stream()
+ .map(PidfParserUtils::getNotFoundContactCapabilities)
+ .filter(Objects::nonNull)
+ .collect(Collectors.toList());
+ }
+
+ logd("onCapabilitiesUpdate: PIDF size=" + pidfXml.size()
+ + ", contact capability size=" + capabilityList.size());
+
+ // Add these updated RcsContactUceCapability into the RequestResponse and notify
+ // the RequestManager to process the RcsContactUceCapabilities updated.
+ mRequestResponse.addUpdatedCapabilities(capabilityList);
+ mRequestManagerCallback.notifyCapabilitiesUpdated(mCoordinatorId, mTaskId);
+ }
+
+ // Receive the terminated callback which is triggered by ISubscribeResponseCallback.
+ private void onTerminated(String reason, long retryAfterMillis) {
+ logd("onTerminated: reason=" + reason + ", retryAfter=" + retryAfterMillis);
+ if (mIsFinished) {
+ logd("onTerminated: This request is already finished");
+ return;
+ }
+ mRequestResponse.setTerminated(reason, retryAfterMillis);
+ mRequestManagerCallback.notifyTerminated(mCoordinatorId, mTaskId);
+ }
+
+ @VisibleForTesting
+ public ISubscribeResponseCallback getResponseCallback() {
+ return mResponseCallback;
+ }
+}
diff --git a/src/java/com/android/ims/rcs/uce/request/SubscribeRequestCoordinator.java b/src/java/com/android/ims/rcs/uce/request/SubscribeRequestCoordinator.java
new file mode 100644
index 00000000..ee6bd356
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/request/SubscribeRequestCoordinator.java
@@ -0,0 +1,523 @@
+/*
+ * Copyright (c) 2021 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.ims.rcs.uce.request;
+
+import static android.telephony.ims.stub.RcsCapabilityExchangeImplBase.COMMAND_CODE_GENERIC_FAILURE;
+
+import android.net.Uri;
+import android.os.RemoteException;
+import android.telephony.ims.RcsContactUceCapability;
+import android.telephony.ims.RcsUceAdapter;
+import android.telephony.ims.aidl.IRcsUceControllerCallback;
+
+import com.android.ims.rcs.uce.UceDeviceState.DeviceStateResult;
+import com.android.ims.rcs.uce.presence.pidfparser.PidfParserUtils;
+import com.android.ims.rcs.uce.request.SubscriptionTerminatedHelper.TerminatedResult;
+import com.android.ims.rcs.uce.request.UceRequestManager.RequestManagerCallback;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+/**
+ * Responsible for the communication and interaction between SubscribeRequests and triggering
+ * the callback to notify the result of the capabilities request.
+ */
+public class SubscribeRequestCoordinator extends UceRequestCoordinator {
+ /**
+ * The builder of the SubscribeRequestCoordinator.
+ */
+ public static final class Builder {
+ private SubscribeRequestCoordinator mRequestCoordinator;
+
+ /**
+ * The builder of the SubscribeRequestCoordinator class.
+ */
+ public Builder(int subId, Collection<UceRequest> requests, RequestManagerCallback c) {
+ mRequestCoordinator = new SubscribeRequestCoordinator(subId, requests, c);
+ }
+
+ /**
+ * Set the callback to receive the request updated.
+ */
+ public Builder setCapabilitiesCallback(IRcsUceControllerCallback callback) {
+ mRequestCoordinator.setCapabilitiesCallback(callback);
+ return this;
+ }
+
+ /**
+ * Get the SubscribeRequestCoordinator instance.
+ */
+ public SubscribeRequestCoordinator build() {
+ return mRequestCoordinator;
+ }
+ }
+
+ /**
+ * Different request updated events will create different {@link RequestResult}. Define the
+ * interface to get the {@link RequestResult} instance according to the given task ID and
+ * {@link CapabilityRequestResponse}.
+ */
+ @FunctionalInterface
+ private interface RequestResultCreator {
+ RequestResult createRequestResult(long taskId, CapabilityRequestResponse response,
+ RequestManagerCallback requestMgrCallback);
+ }
+
+ // The RequestResult creator of the request error.
+ private static final RequestResultCreator sRequestErrorCreator = (taskId, response,
+ requestMgrCallback) -> {
+ int errorCode = response.getRequestInternalError().orElse(DEFAULT_ERROR_CODE);
+ long retryAfter = response.getRetryAfterMillis();
+ return RequestResult.createFailedResult(taskId, errorCode, retryAfter);
+ };
+
+ // The RequestResult creator of the command error.
+ private static final RequestResultCreator sCommandErrorCreator = (taskId, response,
+ requestMgrCallback) -> {
+ int cmdError = response.getCommandError().orElse(COMMAND_CODE_GENERIC_FAILURE);
+ int errorCode = CapabilityRequestResponse.getCapabilityErrorFromCommandError(cmdError);
+ long retryAfter = response.getRetryAfterMillis();
+ return RequestResult.createFailedResult(taskId, errorCode, retryAfter);
+ };
+
+ // The RequestResult creator of the network response error.
+ private static final RequestResultCreator sNetworkRespErrorCreator = (taskId, response,
+ requestMgrCallback) -> {
+ DeviceStateResult deviceState = requestMgrCallback.getDeviceState();
+ if (deviceState.isRequestForbidden()) {
+ int errorCode = deviceState.getErrorCode().orElse(RcsUceAdapter.ERROR_FORBIDDEN);
+ long retryAfter = deviceState.getRequestRetryAfterMillis();
+ return RequestResult.createFailedResult(taskId, errorCode, retryAfter);
+ } else {
+ int errorCode = CapabilityRequestResponse.getCapabilityErrorFromSipCode(response);
+ long retryAfter = response.getRetryAfterMillis();
+ return RequestResult.createFailedResult(taskId, errorCode, retryAfter);
+ }
+ };
+
+ // The RequestResult creator of the network response is not 200 OK, however, we can to treat
+ // it as a successful result and finish the request
+ private static final RequestResultCreator sNetworkRespSuccessfulCreator = (taskId, response,
+ requestMgrCallback) -> RequestResult.createSuccessResult(taskId);
+
+ // The RequestResult creator of the request terminated.
+ private static final RequestResultCreator sTerminatedCreator = (taskId, response,
+ requestMgrCallback) -> {
+ // Check the given terminated reason to determine whether clients should retry or not.
+ TerminatedResult terminatedResult = SubscriptionTerminatedHelper.getAnalysisResult(
+ response.getTerminatedReason(), response.getRetryAfterMillis(),
+ response.haveAllRequestCapsUpdatedBeenReceived());
+ if (terminatedResult.getErrorCode().isPresent()) {
+ // If the terminated error code is present, it means that the request is failed.
+ int errorCode = terminatedResult.getErrorCode().get();
+ long terminatedRetry = terminatedResult.getRetryAfterMillis();
+ return RequestResult.createFailedResult(taskId, errorCode, terminatedRetry);
+ } else if (!response.isNetworkResponseOK() || response.getRetryAfterMillis() > 0L) {
+ // If the network response is failed or the retryAfter is not 0, this request is failed.
+ long retryAfterMillis = response.getRetryAfterMillis();
+ int errorCode = CapabilityRequestResponse.getCapabilityErrorFromSipCode(response);
+ return RequestResult.createFailedResult(taskId, errorCode, retryAfterMillis);
+ } else {
+ return RequestResult.createSuccessResult(taskId);
+ }
+ };
+
+ // The RequestResult creator for does not need to request from the network.
+ private static final RequestResultCreator sNotNeedRequestFromNetworkCreator =
+ (taskId, response, requestMgrCallback) -> RequestResult.createSuccessResult(taskId);
+
+ // The RequestResult creator of the request timeout.
+ private static final RequestResultCreator sRequestTimeoutCreator =
+ (taskId, response, requestMgrCallback) -> RequestResult.createFailedResult(taskId,
+ RcsUceAdapter.ERROR_REQUEST_TIMEOUT, 0L);
+
+ // The callback to notify the result of the capabilities request.
+ private volatile IRcsUceControllerCallback mCapabilitiesCallback;
+
+ private SubscribeRequestCoordinator(int subId, Collection<UceRequest> requests,
+ RequestManagerCallback requestMgrCallback) {
+ super(subId, requests, requestMgrCallback);
+ logd("SubscribeRequestCoordinator: created");
+ }
+
+ private void setCapabilitiesCallback(IRcsUceControllerCallback callback) {
+ mCapabilitiesCallback = callback;
+ }
+
+ @Override
+ public void onFinish() {
+ logd("SubscribeRequestCoordinator: onFinish");
+ mCapabilitiesCallback = null;
+ super.onFinish();
+ }
+
+ @Override
+ public void onRequestUpdated(long taskId, @UceRequestUpdate int event) {
+ if (mIsFinished) return;
+ SubscribeRequest request = (SubscribeRequest) getUceRequest(taskId);
+ if (request == null) {
+ logw("onRequestUpdated: Cannot find SubscribeRequest taskId=" + taskId);
+ return;
+ }
+
+ logd("onRequestUpdated(SubscribeRequest): taskId=" + taskId + ", event=" +
+ REQUEST_EVENT_DESC.get(event));
+
+ switch (event) {
+ case REQUEST_UPDATE_ERROR:
+ handleRequestError(request);
+ break;
+ case REQUEST_UPDATE_COMMAND_ERROR:
+ handleCommandError(request);
+ break;
+ case REQUEST_UPDATE_NETWORK_RESPONSE:
+ handleNetworkResponse(request);
+ break;
+ case REQUEST_UPDATE_CAPABILITY_UPDATE:
+ handleCapabilitiesUpdated(request);
+ break;
+ case REQUEST_UPDATE_RESOURCE_TERMINATED:
+ handleResourceTerminated(request);
+ break;
+ case REQUEST_UPDATE_CACHED_CAPABILITY_UPDATE:
+ handleCachedCapabilityUpdated(request);
+ break;
+ case REQUEST_UPDATE_TERMINATED:
+ handleTerminated(request);
+ break;
+ case REQUEST_UPDATE_NO_NEED_REQUEST_FROM_NETWORK:
+ handleNoNeedRequestFromNetwork(request);
+ break;
+ case REQUEST_UPDATE_TIMEOUT:
+ handleRequestTimeout(request);
+ break;
+ default:
+ logw("onRequestUpdated(SubscribeRequest): invalid event " + event);
+ break;
+ }
+
+ // End this instance if all the UceRequests in the coordinator are finished.
+ checkAndFinishRequestCoordinator();
+ }
+
+ /**
+ * Finish the SubscribeRequest because it has encountered error.
+ */
+ private void handleRequestError(SubscribeRequest request) {
+ CapabilityRequestResponse response = request.getRequestResponse();
+ logd("handleRequestError: " + request.toString());
+
+ // Finish this request.
+ request.onFinish();
+
+ // Remove this request from the activated collection and notify RequestManager.
+ Long taskId = request.getTaskId();
+ RequestResult requestResult = sRequestErrorCreator.createRequestResult(taskId, response,
+ mRequestManagerCallback);
+ moveRequestToFinishedCollection(taskId, requestResult);
+ }
+
+ /**
+ * This method is called when the given SubscribeRequest received the onCommandError callback
+ * from the ImsService.
+ */
+ private void handleCommandError(SubscribeRequest request) {
+ CapabilityRequestResponse response = request.getRequestResponse();
+ logd("handleCommandError: " + request.toString());
+
+ // Finish this request.
+ request.onFinish();
+
+ // Remove this request from the activated collection and notify RequestManager.
+ Long taskId = request.getTaskId();
+ RequestResult requestResult = sCommandErrorCreator.createRequestResult(taskId, response,
+ mRequestManagerCallback);
+ moveRequestToFinishedCollection(taskId, requestResult);
+ }
+
+ /**
+ * This method is called when the given SubscribeRequest received the onNetworkResponse
+ * callback from the ImsService.
+ */
+ private void handleNetworkResponse(SubscribeRequest request) {
+ CapabilityRequestResponse response = request.getRequestResponse();
+ logd("handleNetworkResponse: " + response.toString());
+
+ // Refresh the device state with the request result.
+ response.getResponseSipCode().ifPresent(sipCode -> {
+ String reason = response.getResponseReason().orElse("");
+ mRequestManagerCallback.refreshDeviceState(sipCode, reason);
+ });
+
+ // When the network response is unsuccessful, there is no subsequent callback for this
+ // request. Check the forbidden state and finish this request. Otherwise, keep waiting for
+ // the subsequent callback of this request.
+ if (!response.isNetworkResponseOK()) {
+ // Handle the network response not OK cases and get the request result to finish this
+ // request.
+ RequestResult requestResult = handleNetworkResponseFailed(request);
+
+ // Trigger capabilities updated callback if there is any.
+ List<RcsContactUceCapability> updatedCapList = response.getUpdatedContactCapability();
+ if (!updatedCapList.isEmpty()) {
+ mRequestManagerCallback.saveCapabilities(updatedCapList);
+ triggerCapabilitiesReceivedCallback(updatedCapList);
+ response.removeUpdatedCapabilities(updatedCapList);
+ }
+
+ // Finish this request.
+ request.onFinish();
+
+ // Remove this request from the activated collection and notify RequestManager.
+ moveRequestToFinishedCollection(request.getTaskId(), requestResult);
+ }
+ }
+
+ private RequestResult handleNetworkResponseFailed(SubscribeRequest request) {
+ final long taskId = request.getTaskId();
+ final CapabilityRequestResponse response = request.getRequestResponse();
+ RequestResult requestResult = null;
+
+ if (response.isNotFound()) {
+ // In the network response with the not found case, we won't receive the capabilities
+ // updated callback from the ImsService afterward. Therefore, we create the capabilities
+ // with the result REQUEST_RESULT_NOT_FOUND by ourself and will trigger the
+ // capabilities received callback to the clients later.
+ List<Uri> uriList = request.getContactUri();
+ List<RcsContactUceCapability> capabilityList = uriList.stream().map(uri ->
+ PidfParserUtils.getNotFoundContactCapabilities(uri))
+ .collect(Collectors.toList());
+ response.addUpdatedCapabilities(capabilityList);
+
+ // We treat the NOT FOUND is a successful result.
+ requestResult = sNetworkRespSuccessfulCreator.createRequestResult(taskId, response,
+ mRequestManagerCallback);
+ }
+
+ if (requestResult == null) {
+ requestResult = sNetworkRespErrorCreator.createRequestResult(taskId, response,
+ mRequestManagerCallback);
+ }
+ return requestResult;
+ }
+
+ /**
+ * This method is called when the given SubscribeRequest received the onNotifyCapabilitiesUpdate
+ * callback from the ImsService.
+ */
+ private void handleCapabilitiesUpdated(SubscribeRequest request) {
+ CapabilityRequestResponse response = request.getRequestResponse();
+ Long taskId = request.getTaskId();
+ List<RcsContactUceCapability> updatedCapList = response.getUpdatedContactCapability();
+ logd("handleCapabilitiesUpdated: taskId=" + taskId + ", size=" + updatedCapList.size());
+
+ if (updatedCapList.isEmpty()) {
+ return;
+ }
+
+ // Save the updated capabilities to the cache.
+ mRequestManagerCallback.saveCapabilities(updatedCapList);
+
+ // Trigger the capabilities updated callback and remove the given capabilities that have
+ // executed the callback onCapabilitiesReceived.
+ triggerCapabilitiesReceivedCallback(updatedCapList);
+ response.removeUpdatedCapabilities(updatedCapList);
+ }
+
+ /**
+ * This method is called when the given SubscribeRequest received the onResourceTerminated
+ * callback from the ImsService.
+ */
+ private void handleResourceTerminated(SubscribeRequest request) {
+ CapabilityRequestResponse response = request.getRequestResponse();
+ Long taskId = request.getTaskId();
+ List<RcsContactUceCapability> terminatedResources = response.getTerminatedResources();
+ logd("handleResourceTerminated: taskId=" + taskId + ", size=" + terminatedResources.size());
+
+ if (terminatedResources.isEmpty()) {
+ return;
+ }
+
+ // Save the terminated capabilities to the cache.
+ mRequestManagerCallback.saveCapabilities(terminatedResources);
+
+ // Trigger the capabilities updated callback and remove the given capabilities from the
+ // resource terminated list.
+ triggerCapabilitiesReceivedCallback(terminatedResources);
+ response.removeTerminatedResources(terminatedResources);
+ }
+
+ /**
+ * This method is called when the given SubscribeRequest retrieve the cached capabilities.
+ */
+ private void handleCachedCapabilityUpdated(SubscribeRequest request) {
+ CapabilityRequestResponse response = request.getRequestResponse();
+ Long taskId = request.getTaskId();
+ List<RcsContactUceCapability> cachedCapList = response.getCachedContactCapability();
+ logd("handleCachedCapabilityUpdated: taskId=" + taskId + ", size=" + cachedCapList.size());
+
+ if (cachedCapList.isEmpty()) {
+ return;
+ }
+
+ // Trigger the capabilities updated callback.
+ triggerCapabilitiesReceivedCallback(cachedCapList);
+ response.removeCachedContactCapabilities();
+ }
+
+ /**
+ * This method is called when the given SubscribeRequest received the onTerminated callback
+ * from the ImsService.
+ */
+ private void handleTerminated(SubscribeRequest request) {
+ CapabilityRequestResponse response = request.getRequestResponse();
+ logd("handleTerminated: " + response.toString());
+
+ // Finish this request.
+ request.onFinish();
+
+ // Remove this request from the activated collection and notify RequestManager.
+ Long taskId = request.getTaskId();
+ RequestResult requestResult = sTerminatedCreator.createRequestResult(taskId, response,
+ mRequestManagerCallback);
+ moveRequestToFinishedCollection(taskId, requestResult);
+ }
+
+ /**
+ * This method is called when all the capabilities can be retrieved from the cached and it does
+ * not need to request capabilities from the network.
+ */
+ private void handleNoNeedRequestFromNetwork(SubscribeRequest request) {
+ CapabilityRequestResponse response = request.getRequestResponse();
+ logd("handleNoNeedRequestFromNetwork: " + response.toString());
+
+ // Finish this request.
+ request.onFinish();
+
+ // Remove this request from the activated collection and notify RequestManager.
+ long taskId = request.getTaskId();
+ RequestResult requestResult = sNotNeedRequestFromNetworkCreator.createRequestResult(taskId,
+ response, mRequestManagerCallback);
+ moveRequestToFinishedCollection(taskId, requestResult);
+ }
+
+ /**
+ * This method is called when the framework does not receive receive the result for
+ * capabilities request.
+ */
+ private void handleRequestTimeout(SubscribeRequest request) {
+ CapabilityRequestResponse response = request.getRequestResponse();
+ logd("handleRequestTimeout: " + response);
+
+ // Finish this request
+ request.onFinish();
+
+ // Remove this request from the activated collection and notify RequestManager.
+ long taskId = request.getTaskId();
+ RequestResult requestResult = sRequestTimeoutCreator.createRequestResult(taskId,
+ response, mRequestManagerCallback);
+ moveRequestToFinishedCollection(taskId, requestResult);
+ }
+
+ private void checkAndFinishRequestCoordinator() {
+ synchronized (mCollectionLock) {
+ // Return because there are requests running.
+ if (!mActivatedRequests.isEmpty()) {
+ return;
+ }
+
+ // All the requests has finished, find the request which has the max retryAfter time.
+ // If the result is empty, it means all the request are success.
+ Optional<RequestResult> optRequestResult =
+ mFinishedRequests.values().stream()
+ .filter(result -> !result.isRequestSuccess())
+ .max(Comparator.comparingLong(result ->
+ result.getRetryMillis().orElse(-1L)));
+
+ // Trigger the callback
+ if (optRequestResult.isPresent()) {
+ RequestResult result = optRequestResult.get();
+ int errorCode = result.getErrorCode().orElse(DEFAULT_ERROR_CODE);
+ long retryAfter = result.getRetryMillis().orElse(0L);
+ triggerErrorCallback(errorCode, retryAfter);
+ } else {
+ triggerCompletedCallback();
+ }
+
+ // Notify UceRequestManager to remove this instance from the collection.
+ mRequestManagerCallback.notifyRequestCoordinatorFinished(mCoordinatorId);
+
+ logd("checkAndFinishRequestCoordinator(SubscribeRequest) done, id=" + mCoordinatorId);
+ }
+ }
+
+ /**
+ * Trigger the capabilities updated callback.
+ */
+ private void triggerCapabilitiesReceivedCallback(List<RcsContactUceCapability> capList) {
+ try {
+ logd("triggerCapabilitiesCallback: size=" + capList.size());
+ mCapabilitiesCallback.onCapabilitiesReceived(capList);
+ } catch (RemoteException e) {
+ logw("triggerCapabilitiesCallback exception: " + e);
+ } finally {
+ logd("triggerCapabilitiesCallback: done");
+ }
+ }
+
+ /**
+ * Trigger the onComplete callback to notify the request is completed.
+ */
+ private void triggerCompletedCallback() {
+ try {
+ logd("triggerCompletedCallback");
+ mCapabilitiesCallback.onComplete();
+ } catch (RemoteException e) {
+ logw("triggerCompletedCallback exception: " + e);
+ } finally {
+ logd("triggerCompletedCallback: done");
+ }
+ }
+
+ /**
+ * Trigger the onError callback to notify the request is failed.
+ */
+ private void triggerErrorCallback(int errorCode, long retryAfterMillis) {
+ try {
+ logd("triggerErrorCallback: errorCode=" + errorCode + ", retry=" + retryAfterMillis);
+ mCapabilitiesCallback.onError(errorCode, retryAfterMillis);
+ } catch (RemoteException e) {
+ logw("triggerErrorCallback exception: " + e);
+ } finally {
+ logd("triggerErrorCallback: done");
+ }
+ }
+
+ @VisibleForTesting
+ public Collection<UceRequest> getActivatedRequest() {
+ return mActivatedRequests.values();
+ }
+
+ @VisibleForTesting
+ public Collection<RequestResult> getFinishedRequest() {
+ return mFinishedRequests.values();
+ }
+}
diff --git a/src/java/com/android/ims/rcs/uce/request/SubscriptionTerminatedHelper.java b/src/java/com/android/ims/rcs/uce/request/SubscriptionTerminatedHelper.java
new file mode 100644
index 00000000..074d6e5b
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/request/SubscriptionTerminatedHelper.java
@@ -0,0 +1,182 @@
+/*
+ * Copyright (c) 2021 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.ims.rcs.uce.request;
+
+import android.telephony.ims.RcsUceAdapter;
+import android.telephony.ims.RcsUceAdapter.ErrorCode;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.ims.rcs.uce.util.UceUtils;
+
+import java.util.Optional;
+
+/**
+ * The helper class to analyze the result of the callback onTerminated to determine whether the
+ * subscription request should be retried or not.
+ */
+public class SubscriptionTerminatedHelper {
+
+ private static final String LOG_TAG = UceUtils.getLogPrefix() + "SubscriptionTerminated";
+
+ // The terminated reasons defined in RFC 3265 3.2.4
+ private static final String REASON_DEACTIVATED = "deactivated";
+ private static final String REASON_PROBATION = "probation";
+ private static final String REASON_REJECTED = "rejected";
+ private static final String REASON_TIMEOUT = "timeout";
+ private static final String REASON_GIVEUP = "giveup";
+ private static final String REASON_NORESOURCE = "noresource";
+
+ /**
+ * The analysis result of the callback onTerminated.
+ */
+ static class TerminatedResult {
+ private final @ErrorCode Optional<Integer> mErrorCode;
+ private final long mRetryAfterMillis;
+
+ public TerminatedResult(@ErrorCode Optional<Integer> errorCode, long retryAfterMillis) {
+ mErrorCode = errorCode;
+ mRetryAfterMillis = retryAfterMillis;
+ }
+
+ /**
+ * @return the error code when the request is failed. Optional.empty if the request is
+ * successful.
+ */
+ public Optional<Integer> getErrorCode() {
+ return mErrorCode;
+ }
+
+ public long getRetryAfterMillis() {
+ return mRetryAfterMillis;
+ }
+
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ builder.append("TerminatedResult: ")
+ .append("errorCode=").append(mErrorCode)
+ .append(", retryAfterMillis=").append(mRetryAfterMillis);
+ return builder.toString();
+ }
+ }
+
+ /**
+ * According to the RFC 3265, Check the given reason to see whether clients should retry the
+ * subscribe request.
+ * <p>
+ * See RFC 3265 3.2.4 for the detail.
+ *
+ * @param reason The reason why the subscribe request is terminated. The reason is given by the
+ * network and it could be empty.
+ * @param retryAfterMillis How long should clients wait before retrying.
+ * @param allCapsHaveReceived Whether all the request contact capabilities have been received.
+ */
+ public static TerminatedResult getAnalysisResult(String reason, long retryAfterMillis,
+ boolean allCapsHaveReceived) {
+ TerminatedResult result = null;
+ if (TextUtils.isEmpty(reason)) {
+ /*
+ * When the value of retryAfterMillis is larger then zero, the client should retry.
+ */
+ if (retryAfterMillis > 0L) {
+ result = new TerminatedResult(Optional.of(RcsUceAdapter.ERROR_GENERIC_FAILURE),
+ retryAfterMillis);
+ }
+ } else if (REASON_DEACTIVATED.equalsIgnoreCase(reason)) {
+ /*
+ * When the reason is "deactivated", clients should retry immediately.
+ */
+ long retry = getRequestRetryAfterMillis(retryAfterMillis);
+ result = new TerminatedResult(Optional.of(RcsUceAdapter.ERROR_GENERIC_FAILURE), retry);
+ } else if (REASON_PROBATION.equalsIgnoreCase(reason)) {
+ /*
+ * When the reason is "probation", it means that the subscription has been terminated,
+ * but the client should retry at some later time.
+ */
+ long retry = getRequestRetryAfterMillis(retryAfterMillis);
+ result = new TerminatedResult(Optional.of(RcsUceAdapter.ERROR_GENERIC_FAILURE), retry);
+ } else if (REASON_REJECTED.equalsIgnoreCase(reason)) {
+ /*
+ * When the reason is "rejected", it means that the subscription has been terminated
+ * due to chang in authorization policy. Clients should NOT retry.
+ */
+ result = new TerminatedResult(Optional.of(RcsUceAdapter.ERROR_NOT_AUTHORIZED), 0L);
+ } else if (REASON_TIMEOUT.equalsIgnoreCase(reason)) {
+ if (retryAfterMillis > 0L) {
+ /*
+ * When the parameter "retryAfterMillis" is greater than zero, it means that the
+ * ImsService requires clients should retry later.
+ */
+ long retry = getRequestRetryAfterMillis(retryAfterMillis);
+ result = new TerminatedResult(Optional.of(RcsUceAdapter.ERROR_REQUEST_TIMEOUT),
+ retry);
+ } else if (!allCapsHaveReceived) {
+ /*
+ * The ImsService does not require to retry when the parameter "retryAfterMillis"
+ * is zero. However, the request is still failed because it has not received all
+ * the capabilities updated from the network.
+ */
+ result = new TerminatedResult(Optional.of(RcsUceAdapter.ERROR_REQUEST_TIMEOUT), 0L);
+ } else {
+ /*
+ * The subscribe request is successfully when the parameter retryAfter is zero and
+ * all the request capabilities have been received.
+ */
+ result = new TerminatedResult(Optional.empty(), 0L);
+ }
+ } else if (REASON_GIVEUP.equalsIgnoreCase(reason)) {
+ /*
+ * The subscription has been terminated because the notifier could no obtain
+ * authorization in a timely fashion. Clients could retry the subscribe request.
+ */
+ long retry = getRequestRetryAfterMillis(retryAfterMillis);
+ result = new TerminatedResult(Optional.of(RcsUceAdapter.ERROR_NOT_AUTHORIZED), retry);
+ } else if (REASON_NORESOURCE.equalsIgnoreCase(reason)) {
+ /*
+ * The subscription has been terminated because the resource is no longer exists.
+ * Clients should NOT retry.
+ */
+ result = new TerminatedResult(Optional.of(RcsUceAdapter.ERROR_NOT_FOUND), 0L);
+ } else if (retryAfterMillis > 0L) {
+ /*
+ * Even if the reason is not listed above, clients should retry the request as long as
+ * the value of retry is non-zero.
+ */
+ long retry = getRequestRetryAfterMillis(retryAfterMillis);
+ result = new TerminatedResult(Optional.of(RcsUceAdapter.ERROR_GENERIC_FAILURE), retry);
+ }
+
+ // The request should be successful. when the terminated is not in the above cases
+ if (result == null) {
+ result = new TerminatedResult(Optional.empty(), 0L);
+ }
+
+ Log.d(LOG_TAG, "getAnalysisResult: reason=" + reason + ", retry=" + retryAfterMillis +
+ ", allCapsHaveReceived=" + allCapsHaveReceived + ", " + result);
+ return result;
+ }
+
+ /*
+ * Get the appropriated retryAfterMillis for the subscribe request.
+ */
+ private static long getRequestRetryAfterMillis(long retryAfterMillis) {
+ // Return the minimum retry after millis if the given retryAfterMillis is less than the
+ // minimum value.
+ long minRetryAfterMillis = UceUtils.getMinimumRequestRetryAfterMillis();
+ return (retryAfterMillis < minRetryAfterMillis) ? minRetryAfterMillis : retryAfterMillis;
+ }
+}
diff --git a/src/java/com/android/ims/rcs/uce/request/UceRequest.java b/src/java/com/android/ims/rcs/uce/request/UceRequest.java
new file mode 100644
index 00000000..197f4ba4
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/request/UceRequest.java
@@ -0,0 +1,73 @@
+/*
+ * 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.ims.rcs.uce.request;
+
+import android.annotation.IntDef;
+import android.net.Uri;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.List;
+
+/**
+ * The interface of the UCE request to request the capabilities from the carrier network.
+ */
+public interface UceRequest {
+ /** The request type: CAPABILITY */
+ int REQUEST_TYPE_CAPABILITY = 1;
+
+ /** The request type: AVAILABILITY */
+ int REQUEST_TYPE_AVAILABILITY = 2;
+
+ /**@hide*/
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = "REQUEST_TYPE_", value = {
+ REQUEST_TYPE_CAPABILITY,
+ REQUEST_TYPE_AVAILABILITY
+ })
+ @interface UceRequestType {}
+
+ /**
+ * Set the UceRequestCoordinator ID associated with this request.
+ */
+ void setRequestCoordinatorId(long coordinatorId);
+
+ /**
+ * @return Return the UceRequestCoordinator ID associated with this request.
+ */
+ long getRequestCoordinatorId();
+
+ /**
+ * @return Return the task ID of this request.
+ */
+ long getTaskId();
+
+ /**
+ * Notify that the request is finish.
+ */
+ void onFinish();
+
+ /**
+ * Set the contact URIs associated with this request.
+ */
+ void setContactUri(List<Uri> uris);
+
+ /**
+ * Execute the request.
+ */
+ void executeRequest();
+}
diff --git a/src/java/com/android/ims/rcs/uce/request/UceRequestCoordinator.java b/src/java/com/android/ims/rcs/uce/request/UceRequestCoordinator.java
new file mode 100644
index 00000000..eea4fbe3
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/request/UceRequestCoordinator.java
@@ -0,0 +1,293 @@
+/*
+ * Copyright (c) 2021 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.ims.rcs.uce.request;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.telephony.ims.RcsUceAdapter;
+import android.util.Log;
+
+import com.android.ims.rcs.uce.request.UceRequestManager.RequestManagerCallback;
+import com.android.ims.rcs.uce.util.UceUtils;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+/**
+ * The base class that is responsible for the communication and interaction between the UceRequests.
+ */
+public abstract class UceRequestCoordinator {
+
+ private static final String LOG_TAG = UceUtils.getLogPrefix() + "ReqCoordinator";
+
+ /**
+ * The UceRequest encountered error.
+ */
+ public static final int REQUEST_UPDATE_ERROR = 0;
+
+ /**
+ * The UceRequest received the onCommandError callback.
+ */
+ public static final int REQUEST_UPDATE_COMMAND_ERROR = 1;
+
+ /**
+ * The UceRequest received the onNetworkResponse callback.
+ */
+ public static final int REQUEST_UPDATE_NETWORK_RESPONSE = 2;
+
+ /**
+ * The UceRequest received the onNotifyCapabilitiesUpdate callback.
+ */
+ public static final int REQUEST_UPDATE_CAPABILITY_UPDATE = 3;
+
+ /**
+ * The UceRequest received the onResourceTerminated callback.
+ */
+ public static final int REQUEST_UPDATE_RESOURCE_TERMINATED = 4;
+
+ /**
+ * The UceRequest retrieve the valid capabilities from the cache.
+ */
+ public static final int REQUEST_UPDATE_CACHED_CAPABILITY_UPDATE = 5;
+
+ /**
+ * The UceRequest receive the onTerminated callback.
+ */
+ public static final int REQUEST_UPDATE_TERMINATED = 6;
+
+ /**
+ * The UceRequest does not need to request capabilities to network because all the capabilities
+ * can be retrieved from the cache.
+ */
+ public static final int REQUEST_UPDATE_NO_NEED_REQUEST_FROM_NETWORK = 7;
+
+ /**
+ * The remote options request is done.
+ */
+ public static final int REQUEST_UPDATE_REMOTE_REQUEST_DONE = 8;
+
+ /**
+ * The capabilities request is timeout.
+ */
+ public static final int REQUEST_UPDATE_TIMEOUT = 9;
+
+ @IntDef(value = {
+ REQUEST_UPDATE_ERROR,
+ REQUEST_UPDATE_COMMAND_ERROR,
+ REQUEST_UPDATE_NETWORK_RESPONSE,
+ REQUEST_UPDATE_TERMINATED,
+ REQUEST_UPDATE_RESOURCE_TERMINATED,
+ REQUEST_UPDATE_CAPABILITY_UPDATE,
+ REQUEST_UPDATE_CACHED_CAPABILITY_UPDATE,
+ REQUEST_UPDATE_NO_NEED_REQUEST_FROM_NETWORK,
+ REQUEST_UPDATE_REMOTE_REQUEST_DONE,
+ REQUEST_UPDATE_TIMEOUT,
+ }, prefix="REQUEST_UPDATE_")
+ @Retention(RetentionPolicy.SOURCE)
+ @interface UceRequestUpdate {}
+
+ protected static Map<Integer, String> REQUEST_EVENT_DESC = new HashMap<>();
+ static {
+ REQUEST_EVENT_DESC.put(REQUEST_UPDATE_ERROR, "REQUEST_ERROR");
+ REQUEST_EVENT_DESC.put(REQUEST_UPDATE_COMMAND_ERROR, "RETRIEVE_COMMAND_ERROR");
+ REQUEST_EVENT_DESC.put(REQUEST_UPDATE_NETWORK_RESPONSE, "REQUEST_NETWORK_RESPONSE");
+ REQUEST_EVENT_DESC.put(REQUEST_UPDATE_TERMINATED, "REQUEST_TERMINATED");
+ REQUEST_EVENT_DESC.put(REQUEST_UPDATE_RESOURCE_TERMINATED, "REQUEST_RESOURCE_TERMINATED");
+ REQUEST_EVENT_DESC.put(REQUEST_UPDATE_CAPABILITY_UPDATE, "REQUEST_CAPABILITY_UPDATE");
+ REQUEST_EVENT_DESC.put(REQUEST_UPDATE_CACHED_CAPABILITY_UPDATE, "REQUEST_CACHE_CAP_UPDATE");
+ REQUEST_EVENT_DESC.put(REQUEST_UPDATE_NO_NEED_REQUEST_FROM_NETWORK, "NO_NEED_REQUEST");
+ REQUEST_EVENT_DESC.put(REQUEST_UPDATE_REMOTE_REQUEST_DONE, "REMOTE_REQUEST_DONE");
+ REQUEST_EVENT_DESC.put(REQUEST_UPDATE_TIMEOUT, "REQUEST_TIMEOUT");
+ }
+
+ /**
+ * The result of the UceRequest. This is the used by the RequestCoordinator to record the
+ * result of each sub-requests.
+ */
+ static class RequestResult {
+ /**
+ * Create a RequestResult that successfully completes the request.
+ * @param taskId the task id of the UceRequest
+ */
+ public static RequestResult createSuccessResult(long taskId) {
+ return new RequestResult(taskId);
+ }
+
+ /**
+ * Create a RequestResult for the failed request.
+ * @param taskId the task id of the UceRequest
+ * @param errorCode the error code of the failed request
+ * @param retry When the request can be retried.
+ */
+ public static RequestResult createFailedResult(long taskId, int errorCode, long retry) {
+ return new RequestResult(taskId, errorCode, retry);
+ }
+
+ private final Long mTaskId;
+ private final Boolean mIsSuccess;
+ private final Optional<Integer> mErrorCode;
+ private final Optional<Long> mRetryMillis;
+
+ /**
+ * The private constructor for the successful request.
+ */
+ private RequestResult(long taskId) {
+ mTaskId = taskId;
+ mIsSuccess = true;
+ mErrorCode = Optional.empty();
+ mRetryMillis = Optional.empty();
+ }
+
+ /**
+ * The private constructor for the failed request.
+ */
+ private RequestResult(long taskId, int errorCode, long retryMillis) {
+ mTaskId = taskId;
+ mIsSuccess = false;
+ mErrorCode = Optional.of(errorCode);
+ mRetryMillis = Optional.of(retryMillis);
+ }
+
+ public long getTaskId() {
+ return mTaskId;
+ }
+
+ public boolean isRequestSuccess() {
+ return mIsSuccess;
+ }
+
+ public Optional<Integer> getErrorCode() {
+ return mErrorCode;
+ }
+
+ public Optional<Long> getRetryMillis() {
+ return mRetryMillis;
+ }
+ }
+
+ // The default capability error code.
+ protected static final int DEFAULT_ERROR_CODE = RcsUceAdapter.ERROR_GENERIC_FAILURE;
+
+ protected final int mSubId;
+ protected final long mCoordinatorId;
+ protected volatile boolean mIsFinished;
+
+ // The collection of activated requests.
+ protected final Map<Long, UceRequest> mActivatedRequests;
+ // The collection of the finished requests.
+ protected final Map<Long, RequestResult> mFinishedRequests;
+ // The lock of the activated and finished collection.
+ protected final Object mCollectionLock = new Object();
+
+ // The callback to communicate with UceRequestManager
+ protected final RequestManagerCallback mRequestManagerCallback;
+
+ public UceRequestCoordinator(int subId, Collection<UceRequest> requests,
+ RequestManagerCallback requestMgrCallback) {
+ mSubId = subId;
+ mCoordinatorId = UceUtils.generateRequestCoordinatorId();
+ mRequestManagerCallback = requestMgrCallback;
+
+ // Set the coordinatorId to all the given UceRequests
+ requests.forEach(request -> request.setRequestCoordinatorId(mCoordinatorId));
+
+ // All the given requests are put in the activated request at the beginning.
+ mFinishedRequests = new HashMap<>();
+ mActivatedRequests = requests.stream().collect(
+ Collectors.toMap(UceRequest::getTaskId, request -> request));
+ }
+
+ /**
+ * @return Get the request coordinator ID.
+ */
+ public long getCoordinatorId() {
+ return mCoordinatorId;
+ }
+
+ /**
+ * @return Get the collection of task ID of all the activated requests.
+ */
+ public @NonNull List<Long> getActivatedRequestTaskIds() {
+ synchronized (mCollectionLock) {
+ return mActivatedRequests.values().stream()
+ .map(request -> request.getTaskId())
+ .collect(Collectors.toList());
+ }
+ }
+
+ /**
+ * @return Get the UceRequest associated with the given taskId from the activated requests.
+ */
+ public @Nullable UceRequest getUceRequest(Long taskId) {
+ synchronized (mCollectionLock) {
+ return mActivatedRequests.get(taskId);
+ }
+ }
+
+ /**
+ * Remove the UceRequest associated with the given taskId from the activated collection and
+ * add the {@link RequestResult} into the finished request collection. This method is called by
+ * the coordinator instance when it receives the request updated event and judges this request
+ * is finished.
+ */
+ protected void moveRequestToFinishedCollection(Long taskId, RequestResult requestResult) {
+ synchronized (mCollectionLock) {
+ mActivatedRequests.remove(taskId);
+ mFinishedRequests.put(taskId, requestResult);
+ mRequestManagerCallback.notifyUceRequestFinished(getCoordinatorId(), taskId);
+ }
+ }
+
+ /**
+ * Notify this coordinator instance is finished. This method sets the finish flag and clear all
+ * the UceRequest collections and it can be used anymore after the method is called.
+ */
+ public void onFinish() {
+ mIsFinished = true;
+ synchronized (mCollectionLock) {
+ mActivatedRequests.forEach((taskId, request) -> request.onFinish());
+ mActivatedRequests.clear();
+ mFinishedRequests.clear();
+ }
+ }
+
+ /**
+ * Notify the UceRequest associated with the given taskId in the coordinator is updated.
+ */
+ public abstract void onRequestUpdated(long taskId, @UceRequestUpdate int event);
+
+ protected void logd(String log) {
+ Log.d(LOG_TAG, getLogPrefix().append(log).toString());
+ }
+
+ protected void logw(String log) {
+ Log.w(LOG_TAG, getLogPrefix().append(log).toString());
+ }
+
+ private StringBuilder getLogPrefix() {
+ StringBuilder builder = new StringBuilder("[");
+ builder.append(mSubId).append("][coordId=").append(mCoordinatorId).append("] ");
+ return builder;
+ }
+}
diff --git a/src/java/com/android/ims/rcs/uce/request/UceRequestDispatcher.java b/src/java/com/android/ims/rcs/uce/request/UceRequestDispatcher.java
new file mode 100644
index 00000000..76bde85a
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/request/UceRequestDispatcher.java
@@ -0,0 +1,233 @@
+/*
+ * Copyright (C) 2021 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.ims.rcs.uce.request;
+
+import android.util.Log;
+
+import com.android.ims.rcs.uce.request.UceRequestManager.RequestManagerCallback;
+import com.android.ims.rcs.uce.util.UceUtils;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * Calculate network carry capabilities and dispatcher the UceRequests.
+ */
+public class UceRequestDispatcher {
+
+ private static final String LOG_TAG = UceUtils.getLogPrefix() + "RequestDispatcher";
+
+ /**
+ * Record the request timestamp.
+ */
+ private static class Request {
+ private final long mTaskId;
+ private final long mCoordinatorId;
+ private Optional<Instant> mExecutingTime;
+
+ public Request(long coordinatorId, long taskId) {
+ mTaskId = taskId;
+ mCoordinatorId = coordinatorId;
+ mExecutingTime = Optional.empty();
+ }
+
+ public long getCoordinatorId() {
+ return mCoordinatorId;
+ }
+
+ public long getTaskId() {
+ return mTaskId;
+ }
+
+ public void setExecutingTime(Instant instant) {
+ mExecutingTime = Optional.of(instant);
+ }
+
+ public Optional<Instant> getExecutingTime() {
+ return mExecutingTime;
+ }
+ }
+
+ private final int mSubId;
+
+ // The interval milliseconds for each request.
+ private long mIntervalTime = 100;
+
+ // The number of requests that the network can process at the same time.
+ private int mMaxConcurrentNum = 1;
+
+ // The collection of all requests waiting to be executed.
+ private final List<Request> mWaitingRequests = new ArrayList<>();
+
+ // The collection of all executing requests.
+ private final List<Request> mExecutingRequests = new ArrayList<>();
+
+ // The callback to communicate with UceRequestManager
+ private RequestManagerCallback mRequestManagerCallback;
+
+ public UceRequestDispatcher(int subId, RequestManagerCallback callback) {
+ mSubId = subId;
+ mRequestManagerCallback = callback;
+ }
+
+ /**
+ * Clear all the collections when the instance is destroyed.
+ */
+ public synchronized void onDestroy() {
+ mWaitingRequests.clear();
+ mExecutingRequests.clear();
+ mRequestManagerCallback = null;
+ }
+
+ /**
+ * Add new requests to the waiting collection and trigger sending request if the network is
+ * capable of processing the given requests.
+ */
+ public synchronized void addRequest(long coordinatorId, List<Long> taskIds) {
+ taskIds.stream().forEach(taskId -> {
+ Request request = new Request(coordinatorId, taskId);
+ mWaitingRequests.add(request);
+ });
+ onRequestUpdated();
+ }
+
+ /**
+ * Notify that the request with the given taskId is finished.
+ */
+ public synchronized void onRequestFinished(Long taskId) {
+ logd("onRequestFinished: taskId=" + taskId);
+ mExecutingRequests.removeIf(request -> request.getTaskId() == taskId);
+ onRequestUpdated();
+ }
+
+ private synchronized void onRequestUpdated() {
+ logd("onRequestUpdated: waiting=" + mWaitingRequests.size()
+ + ", executing=" + mExecutingRequests.size());
+
+ // Return if there is no waiting request.
+ if (mWaitingRequests.isEmpty()) {
+ return;
+ }
+
+ // Check how many more requests can be executed and return if the size of executing
+ // requests have reached the maximum number.
+ int numCapacity = mMaxConcurrentNum - mExecutingRequests.size();
+ if (numCapacity <= 0) {
+ return;
+ }
+
+ List<Request> requestList = getRequestFromWaitingCollection(numCapacity);
+ if (!requestList.isEmpty()) {
+ notifyStartOfRequest(requestList);
+ }
+ }
+
+ /*
+ * Retrieve the given number of requests from the WaitingRequestList.
+ */
+ private List<Request> getRequestFromWaitingCollection(int numCapacity) {
+ // The number of the requests cannot more than the waiting requests.
+ int numRequests = (numCapacity < mWaitingRequests.size()) ?
+ numCapacity : mWaitingRequests.size();
+
+ List<Request> requestList = new ArrayList<>();
+ for (int i = 0; i < numRequests; i++) {
+ requestList.add(mWaitingRequests.get(i));
+ }
+
+ mWaitingRequests.removeAll(requestList);
+ return requestList;
+ }
+
+ /**
+ * Notify start of the UceRequest.
+ */
+ private void notifyStartOfRequest(List<Request> requestList) {
+ RequestManagerCallback callback = mRequestManagerCallback;
+ if (callback == null) {
+ logd("notifyStartOfRequest: The instance is destroyed");
+ return;
+ }
+
+ Instant lastRequestTime = getLastRequestTime();
+ Instant baseTime;
+ if (lastRequestTime.plusMillis(mIntervalTime).isAfter(Instant.now())) {
+ baseTime = lastRequestTime.plusMillis(mIntervalTime);
+ } else {
+ baseTime = Instant.now();
+ }
+
+ StringBuilder builder = new StringBuilder("notifyStartOfRequest: taskId=");
+ for (int i = 0; i < requestList.size(); i++) {
+ Instant startExecutingTime = baseTime.plusMillis((mIntervalTime * i));
+ Request request = requestList.get(i);
+ request.setExecutingTime(startExecutingTime);
+
+ // Add the request to the executing collection
+ mExecutingRequests.add(request);
+
+ // Notify RequestManager to execute this task.
+ long taskId = request.getTaskId();
+ long coordId = request.getCoordinatorId();
+ long delayTime = getDelayTime(startExecutingTime);
+ mRequestManagerCallback.notifySendingRequest(coordId, taskId, delayTime);
+
+ builder.append(request.getTaskId() + ", ");
+ }
+ builder.append("ExecutingRequests size=" + mExecutingRequests.size());
+ logd(builder.toString());
+ }
+
+ private Instant getLastRequestTime() {
+ if (mExecutingRequests.isEmpty()) {
+ return Instant.MIN;
+ }
+
+ Instant lastTime = Instant.MIN;
+ for (Request request : mExecutingRequests) {
+ if (!request.getExecutingTime().isPresent()) continue;
+ Instant executingTime = request.getExecutingTime().get();
+ if (executingTime.isAfter(lastTime)) {
+ lastTime = executingTime;
+ }
+ }
+ return lastTime;
+ }
+
+ private long getDelayTime(Instant executingTime) {
+ long delayTime = Duration.between(executingTime, Instant.now()).toMillis();
+ if (delayTime < 0L) {
+ delayTime = 0;
+ }
+ return delayTime;
+ }
+
+ private void logd(String log) {
+ Log.d(LOG_TAG, getLogPrefix().append(log).toString());
+ }
+
+ private StringBuilder getLogPrefix() {
+ StringBuilder builder = new StringBuilder("[");
+ builder.append(mSubId);
+ builder.append("] ");
+ return builder;
+ }
+}
+
diff --git a/src/java/com/android/ims/rcs/uce/request/UceRequestManager.java b/src/java/com/android/ims/rcs/uce/request/UceRequestManager.java
new file mode 100644
index 00000000..3e12ba30
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/request/UceRequestManager.java
@@ -0,0 +1,829 @@
+/*
+ * 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.ims.rcs.uce.request;
+
+import android.content.Context;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+import android.telephony.ims.RcsContactUceCapability;
+import android.telephony.ims.RcsContactUceCapability.CapabilityMechanism;
+import android.telephony.ims.RcsUceAdapter;
+import android.telephony.ims.aidl.IOptionsRequestCallback;
+import android.telephony.ims.aidl.IRcsUceControllerCallback;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.ims.rcs.uce.UceController;
+import com.android.ims.rcs.uce.UceController.UceControllerCallback;
+import com.android.ims.rcs.uce.UceDeviceState;
+import com.android.ims.rcs.uce.UceDeviceState.DeviceStateResult;
+import com.android.ims.rcs.uce.eab.EabCapabilityResult;
+import com.android.ims.rcs.uce.options.OptionsController;
+import com.android.ims.rcs.uce.presence.subscribe.SubscribeController;
+import com.android.ims.rcs.uce.request.UceRequest.UceRequestType;
+import com.android.ims.rcs.uce.request.UceRequestCoordinator.UceRequestUpdate;
+import com.android.ims.rcs.uce.util.UceUtils;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.os.SomeArgs;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+/**
+ * Managers the capabilities requests and the availability requests from UceController.
+ */
+public class UceRequestManager {
+
+ private static final String LOG_TAG = UceUtils.getLogPrefix() + "UceRequestManager";
+
+ /**
+ * Testing interface used to mock UceUtils in testing.
+ */
+ @VisibleForTesting
+ public interface UceUtilsProxy {
+ /**
+ * The interface for {@link UceUtils#isPresenceCapExchangeEnabled(Context, int)} used for
+ * testing.
+ */
+ boolean isPresenceCapExchangeEnabled(Context context, int subId);
+
+ /**
+ * The interface for {@link UceUtils#isPresenceSupported(Context, int)} used for testing.
+ */
+ boolean isPresenceSupported(Context context, int subId);
+
+ /**
+ * The interface for {@link UceUtils#isSipOptionsSupported(Context, int)} used for testing.
+ */
+ boolean isSipOptionsSupported(Context context, int subId);
+
+ /**
+ * @return true when the Presence group subscribe is enabled.
+ */
+ boolean isPresenceGroupSubscribeEnabled(Context context, int subId);
+
+ /**
+ * Retrieve the maximum number of contacts that can be included in a request.
+ */
+ int getRclMaxNumberEntries(int subId);
+
+ /**
+ * @return true if the given phone number is blocked by the network.
+ */
+ boolean isNumberBlocked(Context context, String phoneNumber);
+ }
+
+ private static UceUtilsProxy sUceUtilsProxy = new UceUtilsProxy() {
+ @Override
+ public boolean isPresenceCapExchangeEnabled(Context context, int subId) {
+ return UceUtils.isPresenceCapExchangeEnabled(context, subId);
+ }
+
+ @Override
+ public boolean isPresenceSupported(Context context, int subId) {
+ return UceUtils.isPresenceSupported(context, subId);
+ }
+
+ @Override
+ public boolean isSipOptionsSupported(Context context, int subId) {
+ return UceUtils.isSipOptionsSupported(context, subId);
+ }
+
+ @Override
+ public boolean isPresenceGroupSubscribeEnabled(Context context, int subId) {
+ return UceUtils.isPresenceGroupSubscribeEnabled(context, subId);
+ }
+
+ @Override
+ public int getRclMaxNumberEntries(int subId) {
+ return UceUtils.getRclMaxNumberEntries(subId);
+ }
+
+ @Override
+ public boolean isNumberBlocked(Context context, String phoneNumber) {
+ return UceUtils.isNumberBlocked(context, phoneNumber);
+ }
+ };
+
+ @VisibleForTesting
+ public void setsUceUtilsProxy(UceUtilsProxy uceUtilsProxy) {
+ sUceUtilsProxy = uceUtilsProxy;
+ }
+
+ /**
+ * The callback interface to receive the request and the result from the UceRequest.
+ */
+ public interface RequestManagerCallback {
+ /**
+ * Notify sending the UceRequest
+ */
+ void notifySendingRequest(long coordinator, long taskId, long delayTimeMs);
+
+ /**
+ * Retrieve the contact capabilities from the cache.
+ */
+ List<EabCapabilityResult> getCapabilitiesFromCache(List<Uri> uriList);
+
+ /**
+ * Retrieve the contact availability from the cache.
+ */
+ EabCapabilityResult getAvailabilityFromCache(Uri uri);
+
+ /**
+ * Store the given contact capabilities to the cache.
+ */
+ void saveCapabilities(List<RcsContactUceCapability> contactCapabilities);
+
+ /**
+ * Retrieve the device's capabilities.
+ */
+ RcsContactUceCapability getDeviceCapabilities(@CapabilityMechanism int capMechanism);
+
+ /**
+ * Get the device state to check whether the device is disallowed by the network or not.
+ */
+ DeviceStateResult getDeviceState();
+
+ /**
+ * Refresh the device state. It is called when receive the UCE request response.
+ */
+ void refreshDeviceState(int sipCode, String reason);
+
+ /**
+ * Notify that the UceRequest associated with the given taskId encounters error.
+ */
+ void notifyRequestError(long requestCoordinatorId, long taskId);
+
+ /**
+ * Notify that the UceRequest received the onCommandError callback from the ImsService.
+ */
+ void notifyCommandError(long requestCoordinatorId, long taskId);
+
+ /**
+ * Notify that the UceRequest received the onNetworkResponse callback from the ImsService.
+ */
+ void notifyNetworkResponse(long requestCoordinatorId, long taskId);
+
+ /**
+ * Notify that the UceRequest received the onTerminated callback from the ImsService.
+ */
+ void notifyTerminated(long requestCoordinatorId, long taskId);
+
+ /**
+ * Notify that some contacts are not RCS anymore. It will updated the cached capabilities
+ * and trigger the callback IRcsUceControllerCallback#onCapabilitiesReceived
+ */
+ void notifyResourceTerminated(long requestCoordinatorId, long taskId);
+
+ /**
+ * Notify that the capabilities updates. It will update the cached and trigger the callback
+ * IRcsUceControllerCallback#onCapabilitiesReceived
+ */
+ void notifyCapabilitiesUpdated(long requestCoordinatorId, long taskId);
+
+ /**
+ * Notify that some of the request capabilities can be retrieved from the cached.
+ */
+ void notifyCachedCapabilitiesUpdated(long requestCoordinatorId, long taskId);
+
+ /**
+ * Notify that all the requested capabilities can be retrieved from the cache. It does not
+ * need to request capabilities from the network.
+ */
+ void notifyNoNeedRequestFromNetwork(long requestCoordinatorId, long taskId);
+
+ /**
+ * Notify that the remote options request is done. This is sent by RemoteOptionsRequest and
+ * it will notify the RemoteOptionsCoordinator to handle it.
+ */
+ void notifyRemoteRequestDone(long requestCoordinatorId, long taskId);
+
+ /**
+ * Set the timer for the request timeout. It will cancel the request when the time is up.
+ */
+ void setRequestTimeoutTimer(long requestCoordinatorId, long taskId, long timeoutAfterMs);
+
+ /**
+ * Remove the timeout timer of the capabilities request.
+ */
+ void removeRequestTimeoutTimer(long taskId);
+
+ /**
+ * Notify that the UceRequest has finished. This is sent by UceRequestCoordinator.
+ */
+ void notifyUceRequestFinished(long requestCoordinatorId, long taskId);
+
+ /**
+ * Notify that the RequestCoordinator has finished. This is sent by UceRequestCoordinator
+ * to remove the coordinator from the UceRequestRepository.
+ */
+ void notifyRequestCoordinatorFinished(long requestCoordinatorId);
+ }
+
+ private RequestManagerCallback mRequestMgrCallback = new RequestManagerCallback() {
+ @Override
+ public void notifySendingRequest(long coordinatorId, long taskId, long delayTimeMs) {
+ mHandler.sendRequestMessage(coordinatorId, taskId, delayTimeMs);
+ }
+
+ @Override
+ public List<EabCapabilityResult> getCapabilitiesFromCache(List<Uri> uriList) {
+ return mControllerCallback.getCapabilitiesFromCache(uriList);
+ }
+
+ @Override
+ public EabCapabilityResult getAvailabilityFromCache(Uri uri) {
+ return mControllerCallback.getAvailabilityFromCache(uri);
+ }
+
+ @Override
+ public void saveCapabilities(List<RcsContactUceCapability> contactCapabilities) {
+ mControllerCallback.saveCapabilities(contactCapabilities);
+ }
+
+ @Override
+ public RcsContactUceCapability getDeviceCapabilities(@CapabilityMechanism int mechanism) {
+ return mControllerCallback.getDeviceCapabilities(mechanism);
+ }
+
+ @Override
+ public DeviceStateResult getDeviceState() {
+ return mControllerCallback.getDeviceState();
+ }
+
+ @Override
+ public void refreshDeviceState(int sipCode, String reason) {
+ mControllerCallback.refreshDeviceState(sipCode, reason,
+ UceController.REQUEST_TYPE_CAPABILITY);
+ }
+
+ @Override
+ public void notifyRequestError(long requestCoordinatorId, long taskId) {
+ mHandler.sendRequestUpdatedMessage(requestCoordinatorId, taskId,
+ UceRequestCoordinator.REQUEST_UPDATE_ERROR);
+ }
+
+ @Override
+ public void notifyCommandError(long requestCoordinatorId, long taskId) {
+ mHandler.sendRequestUpdatedMessage(requestCoordinatorId, taskId,
+ UceRequestCoordinator.REQUEST_UPDATE_COMMAND_ERROR);
+ }
+
+ @Override
+ public void notifyNetworkResponse(long requestCoordinatorId, long taskId) {
+ mHandler.sendRequestUpdatedMessage(requestCoordinatorId, taskId,
+ UceRequestCoordinator.REQUEST_UPDATE_NETWORK_RESPONSE);
+ }
+ @Override
+ public void notifyTerminated(long requestCoordinatorId, long taskId) {
+ mHandler.sendRequestUpdatedMessage(requestCoordinatorId, taskId,
+ UceRequestCoordinator.REQUEST_UPDATE_TERMINATED);
+ }
+ @Override
+ public void notifyResourceTerminated(long requestCoordinatorId, long taskId) {
+ mHandler.sendRequestUpdatedMessage(requestCoordinatorId, taskId,
+ UceRequestCoordinator.REQUEST_UPDATE_RESOURCE_TERMINATED);
+ }
+ @Override
+ public void notifyCapabilitiesUpdated(long requestCoordinatorId, long taskId) {
+ mHandler.sendRequestUpdatedMessage(requestCoordinatorId, taskId,
+ UceRequestCoordinator.REQUEST_UPDATE_CAPABILITY_UPDATE);
+ }
+
+ @Override
+ public void notifyCachedCapabilitiesUpdated(long requestCoordinatorId, long taskId) {
+ mHandler.sendRequestUpdatedMessage(requestCoordinatorId, taskId,
+ UceRequestCoordinator.REQUEST_UPDATE_CACHED_CAPABILITY_UPDATE);
+ }
+
+ @Override
+ public void notifyNoNeedRequestFromNetwork(long requestCoordinatorId, long taskId) {
+ mHandler.sendRequestUpdatedMessage(requestCoordinatorId, taskId,
+ UceRequestCoordinator.REQUEST_UPDATE_NO_NEED_REQUEST_FROM_NETWORK);
+ }
+
+ @Override
+ public void notifyRemoteRequestDone(long requestCoordinatorId, long taskId) {
+ mHandler.sendRequestUpdatedMessage(requestCoordinatorId, taskId,
+ UceRequestCoordinator.REQUEST_UPDATE_REMOTE_REQUEST_DONE);
+ }
+
+ @Override
+ public void setRequestTimeoutTimer(long coordinatorId, long taskId, long timeoutAfterMs) {
+ mHandler.sendRequestTimeoutTimerMessage(coordinatorId, taskId, timeoutAfterMs);
+ }
+
+ @Override
+ public void removeRequestTimeoutTimer(long taskId) {
+ mHandler.removeRequestTimeoutTimer(taskId);
+ }
+
+ @Override
+ public void notifyUceRequestFinished(long requestCoordinatorId, long taskId) {
+ mHandler.sendRequestFinishedMessage(requestCoordinatorId, taskId);
+ }
+
+ @Override
+ public void notifyRequestCoordinatorFinished(long requestCoordinatorId) {
+ mHandler.sendRequestCoordinatorFinishedMessage(requestCoordinatorId);
+ }
+ };
+
+ private final int mSubId;
+ private final Context mContext;
+ private final UceRequestHandler mHandler;
+ private final UceRequestRepository mRequestRepository;
+ private volatile boolean mIsDestroyed;
+
+ private OptionsController mOptionsCtrl;
+ private SubscribeController mSubscribeCtrl;
+ private UceControllerCallback mControllerCallback;
+
+ public UceRequestManager(Context context, int subId, Looper looper, UceControllerCallback c) {
+ mSubId = subId;
+ mContext = context;
+ mControllerCallback = c;
+ mHandler = new UceRequestHandler(this, looper);
+ mRequestRepository = new UceRequestRepository(subId, mRequestMgrCallback);
+ logi("create");
+ }
+
+ @VisibleForTesting
+ public UceRequestManager(Context context, int subId, Looper looper, UceControllerCallback c,
+ UceRequestRepository requestRepository) {
+ mSubId = subId;
+ mContext = context;
+ mControllerCallback = c;
+ mHandler = new UceRequestHandler(this, looper);
+ mRequestRepository = requestRepository;
+ }
+
+ /**
+ * Set the OptionsController for requestiong capabilities by OPTIONS mechanism.
+ */
+ public void setOptionsController(OptionsController controller) {
+ mOptionsCtrl = controller;
+ }
+
+ /**
+ * Set the SubscribeController for requesting capabilities by Subscribe mechanism.
+ */
+ public void setSubscribeController(SubscribeController controller) {
+ mSubscribeCtrl = controller;
+ }
+
+ /**
+ * Notify that the request manager instance is destroyed.
+ */
+ public void onDestroy() {
+ logi("onDestroy");
+ mIsDestroyed = true;
+ mHandler.onDestroy();
+ mRequestRepository.onDestroy();
+ }
+
+ /**
+ * Send a new capability request. It is called by UceController.
+ */
+ public void sendCapabilityRequest(List<Uri> uriList, boolean skipFromCache,
+ IRcsUceControllerCallback callback) throws RemoteException {
+ if (mIsDestroyed) {
+ callback.onError(RcsUceAdapter.ERROR_GENERIC_FAILURE, 0L);
+ return;
+ }
+ sendRequestInternal(UceRequest.REQUEST_TYPE_CAPABILITY, uriList, skipFromCache, callback);
+ }
+
+ /**
+ * Send a new availability request. It is called by UceController.
+ */
+ public void sendAvailabilityRequest(Uri uri, IRcsUceControllerCallback callback)
+ throws RemoteException {
+ if (mIsDestroyed) {
+ callback.onError(RcsUceAdapter.ERROR_GENERIC_FAILURE, 0L);
+ return;
+ }
+ sendRequestInternal(UceRequest.REQUEST_TYPE_AVAILABILITY,
+ Collections.singletonList(uri), false /* skipFromCache */, callback);
+ }
+
+ private void sendRequestInternal(@UceRequestType int type, List<Uri> uriList,
+ boolean skipFromCache, IRcsUceControllerCallback callback) throws RemoteException {
+ UceRequestCoordinator requestCoordinator = null;
+ if (sUceUtilsProxy.isPresenceCapExchangeEnabled(mContext, mSubId) &&
+ sUceUtilsProxy.isPresenceSupported(mContext, mSubId)) {
+ requestCoordinator = createSubscribeRequestCoordinator(type, uriList, skipFromCache,
+ callback);
+ } else if (sUceUtilsProxy.isSipOptionsSupported(mContext, mSubId)) {
+ requestCoordinator = createOptionsRequestCoordinator(type, uriList, callback);
+ }
+
+ if (requestCoordinator == null) {
+ logw("sendRequestInternal: Neither Presence nor OPTIONS are supported");
+ callback.onError(RcsUceAdapter.ERROR_NOT_ENABLED, 0L);
+ return;
+ }
+
+ StringBuilder builder = new StringBuilder("sendRequestInternal: ");
+ builder.append("requestType=").append(type)
+ .append(", requestCoordinatorId=").append(requestCoordinator.getCoordinatorId())
+ .append(", taskId={")
+ .append(requestCoordinator.getActivatedRequestTaskIds().stream()
+ .map(Object::toString).collect(Collectors.joining(","))).append("}");
+ logd(builder.toString());
+
+ // Add this RequestCoordinator to the UceRequestRepository.
+ addRequestCoordinator(requestCoordinator);
+ }
+
+ private UceRequestCoordinator createSubscribeRequestCoordinator(final @UceRequestType int type,
+ final List<Uri> uriList, boolean skipFromCache, IRcsUceControllerCallback callback) {
+ SubscribeRequestCoordinator.Builder builder;
+
+ if (!sUceUtilsProxy.isPresenceGroupSubscribeEnabled(mContext, mSubId)) {
+ // When the group subscribe is disabled, each contact is required to be encapsulated
+ // into individual UceRequest.
+ List<UceRequest> requestList = new ArrayList<>();
+ uriList.forEach(uri -> {
+ List<Uri> individualUri = Collections.singletonList(uri);
+ UceRequest request = createSubscribeRequest(type, individualUri, skipFromCache);
+ requestList.add(request);
+ });
+ builder = new SubscribeRequestCoordinator.Builder(mSubId, requestList,
+ mRequestMgrCallback);
+ builder.setCapabilitiesCallback(callback);
+ } else {
+ // Even when the group subscribe is supported by the network, the number of contacts in
+ // a UceRequest still cannot exceed the maximum.
+ List<UceRequest> requestList = new ArrayList<>();
+ final int rclMaxNumber = sUceUtilsProxy.getRclMaxNumberEntries(mSubId);
+ int numRequestCoordinators = uriList.size() / rclMaxNumber;
+ for (int count = 0; count < numRequestCoordinators; count++) {
+ List<Uri> subUriList = new ArrayList<>();
+ for (int index = 0; index < rclMaxNumber; index++) {
+ subUriList.add(uriList.get(count * rclMaxNumber + index));
+ }
+ requestList.add(createSubscribeRequest(type, subUriList, skipFromCache));
+ }
+
+ List<Uri> subUriList = new ArrayList<>();
+ for (int i = numRequestCoordinators * rclMaxNumber; i < uriList.size(); i++) {
+ subUriList.add(uriList.get(i));
+ }
+ requestList.add(createSubscribeRequest(type, subUriList, skipFromCache));
+
+ builder = new SubscribeRequestCoordinator.Builder(mSubId, requestList,
+ mRequestMgrCallback);
+ builder.setCapabilitiesCallback(callback);
+ }
+ return builder.build();
+ }
+
+ private UceRequestCoordinator createOptionsRequestCoordinator(@UceRequestType int type,
+ List<Uri> uriList, IRcsUceControllerCallback callback) {
+ OptionsRequestCoordinator.Builder builder;
+ List<UceRequest> requestList = new ArrayList<>();
+ uriList.forEach(uri -> {
+ List<Uri> individualUri = Collections.singletonList(uri);
+ UceRequest request = createOptionsRequest(type, individualUri, false);
+ requestList.add(request);
+ });
+ builder = new OptionsRequestCoordinator.Builder(mSubId, requestList, mRequestMgrCallback);
+ builder.setCapabilitiesCallback(callback);
+ return builder.build();
+ }
+
+ private CapabilityRequest createSubscribeRequest(int type, List<Uri> uriList,
+ boolean skipFromCache) {
+ CapabilityRequest request = new SubscribeRequest(mSubId, type, mRequestMgrCallback,
+ mSubscribeCtrl);
+ request.setContactUri(uriList);
+ request.setSkipGettingFromCache(skipFromCache);
+ return request;
+ }
+
+ private CapabilityRequest createOptionsRequest(int type, List<Uri> uriList,
+ boolean skipFromCache) {
+ CapabilityRequest request = new OptionsRequest(mSubId, type, mRequestMgrCallback,
+ mOptionsCtrl);
+ request.setContactUri(uriList);
+ request.setSkipGettingFromCache(skipFromCache);
+ return request;
+ }
+
+ /**
+ * Retrieve the device's capabilities. This request is from the ImsService to send the
+ * capabilities to the remote side.
+ */
+ public void retrieveCapabilitiesForRemote(Uri contactUri, List<String> remoteCapabilities,
+ IOptionsRequestCallback requestCallback) {
+ RemoteOptionsRequest request = new RemoteOptionsRequest(mSubId, mRequestMgrCallback);
+ request.setContactUri(Collections.singletonList(contactUri));
+ request.setRemoteFeatureTags(remoteCapabilities);
+
+ // If the remote number is blocked, do not send capabilities back.
+ String number = getNumberFromUri(contactUri);
+ if (!TextUtils.isEmpty(number)) {
+ request.setIsRemoteNumberBlocked(sUceUtilsProxy.isNumberBlocked(mContext, number));
+ }
+
+ // Create the RemoteOptionsCoordinator instance
+ RemoteOptionsCoordinator.Builder CoordBuilder = new RemoteOptionsCoordinator.Builder(
+ mSubId, Collections.singletonList(request), mRequestMgrCallback);
+ CoordBuilder.setOptionsRequestCallback(requestCallback);
+ RemoteOptionsCoordinator requestCoordinator = CoordBuilder.build();
+
+ StringBuilder builder = new StringBuilder("retrieveCapabilitiesForRemote: ");
+ builder.append("requestCoordinatorId ").append(requestCoordinator.getCoordinatorId())
+ .append(", taskId={")
+ .append(requestCoordinator.getActivatedRequestTaskIds().stream()
+ .map(Object::toString).collect(Collectors.joining(","))).append("}");
+ logd(builder.toString());
+
+ // Add this RequestCoordinator to the UceRequestRepository.
+ addRequestCoordinator(requestCoordinator);
+ }
+
+ private static class UceRequestHandler extends Handler {
+ private static final int EVENT_EXECUTE_REQUEST = 1;
+ private static final int EVENT_REQUEST_UPDATED = 2;
+ private static final int EVENT_REQUEST_TIMEOUT = 3;
+ private static final int EVENT_REQUEST_FINISHED = 4;
+ private static final int EVENT_COORDINATOR_FINISHED = 5;
+
+ private final Map<Long, SomeArgs> mRequestTimeoutTimers;
+ private final WeakReference<UceRequestManager> mUceRequestMgrRef;
+
+ public UceRequestHandler(UceRequestManager requestManager, Looper looper) {
+ super(looper);
+ mRequestTimeoutTimers = new HashMap<>();
+ mUceRequestMgrRef = new WeakReference<>(requestManager);
+ }
+
+ /**
+ * Send the capabilities request message.
+ */
+ public void sendRequestMessage(Long coordinatorId, Long taskId, long delayTimeMs) {
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = coordinatorId;
+ args.arg2 = taskId;
+
+ Message message = obtainMessage();
+ message.what = EVENT_EXECUTE_REQUEST;
+ message.obj = args;
+ sendMessageDelayed(message, delayTimeMs);
+ }
+
+ /**
+ * Send the Uce request updated message.
+ */
+ public void sendRequestUpdatedMessage(Long coordinatorId, Long taskId,
+ @UceRequestUpdate int requestEvent) {
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = coordinatorId;
+ args.arg2 = taskId;
+ args.argi1 = requestEvent;
+
+ Message message = obtainMessage();
+ message.what = EVENT_REQUEST_UPDATED;
+ message.obj = args;
+ sendMessage(message);
+ }
+
+ /**
+ * Set the timeout timer to cancel the capabilities request.
+ */
+ public void sendRequestTimeoutTimerMessage(Long coordId, Long taskId, Long timeoutAfterMs) {
+ synchronized (mRequestTimeoutTimers) {
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = coordId;
+ args.arg2 = taskId;
+
+ // Add the message object to the collection. It can be used to find this message
+ // when the request is completed and remove the timeout timer.
+ mRequestTimeoutTimers.put(taskId, args);
+
+ Message message = obtainMessage();
+ message.what = EVENT_REQUEST_TIMEOUT;
+ message.obj = args;
+ sendMessageDelayed(message, timeoutAfterMs);
+ }
+ }
+
+ /**
+ * Remove the timeout timer because the capabilities request is finished.
+ */
+ public void removeRequestTimeoutTimer(Long taskId) {
+ synchronized (mRequestTimeoutTimers) {
+ SomeArgs args = mRequestTimeoutTimers.remove(taskId);
+ if (args == null) {
+ return;
+ }
+ Log.d(LOG_TAG, "removeRequestTimeoutTimer: taskId=" + taskId);
+ removeMessages(EVENT_REQUEST_TIMEOUT, args);
+ args.recycle();
+ }
+ }
+
+ public void sendRequestFinishedMessage(Long coordinatorId, Long taskId) {
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = coordinatorId;
+ args.arg2 = taskId;
+
+ Message message = obtainMessage();
+ message.what = EVENT_REQUEST_FINISHED;
+ message.obj = args;
+ sendMessage(message);
+ }
+
+ /**
+ * Finish the UceRequestCoordinator associated with the given id.
+ */
+ public void sendRequestCoordinatorFinishedMessage(Long coordinatorId) {
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = coordinatorId;
+
+ Message message = obtainMessage();
+ message.what = EVENT_COORDINATOR_FINISHED;
+ message.obj = args;
+ sendMessage(message);
+ }
+
+ /**
+ * Remove all the messages from the handler
+ */
+ public void onDestroy() {
+ removeCallbacksAndMessages(null);
+ // Recycle all the arguments in the mRequestTimeoutTimers
+ synchronized (mRequestTimeoutTimers) {
+ mRequestTimeoutTimers.forEach((taskId, args) -> {
+ try {
+ args.recycle();
+ } catch (Exception e) {}
+ });
+ mRequestTimeoutTimers.clear();
+ }
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ UceRequestManager requestManager = mUceRequestMgrRef.get();
+ if (requestManager == null) {
+ return;
+ }
+ SomeArgs args = (SomeArgs) msg.obj;
+ final Long coordinatorId = (Long) args.arg1;
+ final Long taskId = (Long) Optional.ofNullable(args.arg2).orElse(-1L);
+ final Integer requestEvent = Optional.of(args.argi1).orElse(-1);
+ args.recycle();
+
+ requestManager.logd("handleMessage: " + EVENT_DESCRIPTION.get(msg.what)
+ + ", coordinatorId=" + coordinatorId + ", taskId=" + taskId);
+ switch (msg.what) {
+ case EVENT_EXECUTE_REQUEST: {
+ UceRequest request = requestManager.getUceRequest(taskId);
+ if (request == null) {
+ requestManager.logw("handleMessage: cannot find request, taskId=" + taskId);
+ return;
+ }
+ request.executeRequest();
+ break;
+ }
+ case EVENT_REQUEST_UPDATED: {
+ UceRequestCoordinator requestCoordinator =
+ requestManager.getRequestCoordinator(coordinatorId);
+ if (requestCoordinator == null) {
+ requestManager.logw("handleMessage: cannot find UceRequestCoordinator");
+ return;
+ }
+ requestCoordinator.onRequestUpdated(taskId, requestEvent);
+ break;
+ }
+ case EVENT_REQUEST_TIMEOUT: {
+ UceRequestCoordinator requestCoordinator =
+ requestManager.getRequestCoordinator(coordinatorId);
+ if (requestCoordinator == null) {
+ requestManager.logw("handleMessage: cannot find UceRequestCoordinator");
+ return;
+ }
+ // The timeout timer is triggered, remove this record from the collection.
+ synchronized (mRequestTimeoutTimers) {
+ mRequestTimeoutTimers.remove(taskId);
+ }
+ // Notify that the request is timeout.
+ requestCoordinator.onRequestUpdated(taskId,
+ UceRequestCoordinator.REQUEST_UPDATE_TIMEOUT);
+ break;
+ }
+ case EVENT_REQUEST_FINISHED: {
+ // Notify the repository that the request is finished.
+ requestManager.notifyRepositoryRequestFinished(taskId);
+ break;
+ }
+ case EVENT_COORDINATOR_FINISHED: {
+ UceRequestCoordinator requestCoordinator =
+ requestManager.removeRequestCoordinator(coordinatorId);
+ if (requestCoordinator != null) {
+ requestCoordinator.onFinish();
+ }
+ break;
+ }
+ default: {
+ break;
+ }
+ }
+ }
+
+ private static Map<Integer, String> EVENT_DESCRIPTION = new HashMap<>();
+ static {
+ EVENT_DESCRIPTION.put(EVENT_EXECUTE_REQUEST, "EXECUTE_REQUEST");
+ EVENT_DESCRIPTION.put(EVENT_REQUEST_UPDATED, "REQUEST_UPDATE");
+ EVENT_DESCRIPTION.put(EVENT_REQUEST_TIMEOUT, "REQUEST_TIMEOUT");
+ EVENT_DESCRIPTION.put(EVENT_REQUEST_FINISHED, "REQUEST_FINISHED");
+ EVENT_DESCRIPTION.put(EVENT_COORDINATOR_FINISHED, "REMOVE_COORDINATOR");
+ }
+ }
+
+ private void addRequestCoordinator(UceRequestCoordinator coordinator) {
+ mRequestRepository.addRequestCoordinator(coordinator);
+ }
+
+ private UceRequestCoordinator removeRequestCoordinator(Long coordinatorId) {
+ return mRequestRepository.removeRequestCoordinator(coordinatorId);
+ }
+
+ private UceRequestCoordinator getRequestCoordinator(Long coordinatorId) {
+ return mRequestRepository.getRequestCoordinator(coordinatorId);
+ }
+
+ private UceRequest getUceRequest(Long taskId) {
+ return mRequestRepository.getUceRequest(taskId);
+ }
+
+ private void notifyRepositoryRequestFinished(Long taskId) {
+ mRequestRepository.notifyRequestFinished(taskId);
+ }
+
+ @VisibleForTesting
+ public UceRequestHandler getUceRequestHandler() {
+ return mHandler;
+ }
+
+ @VisibleForTesting
+ public RequestManagerCallback getRequestManagerCallback() {
+ return mRequestMgrCallback;
+ }
+
+ private void logi(String log) {
+ Log.i(LOG_TAG, getLogPrefix().append(log).toString());
+ }
+
+ private void logd(String log) {
+ Log.d(LOG_TAG, getLogPrefix().append(log).toString());
+ }
+
+ private void logw(String log) {
+ Log.w(LOG_TAG, getLogPrefix().append(log).toString());
+ }
+
+ private StringBuilder getLogPrefix() {
+ StringBuilder builder = new StringBuilder("[");
+ builder.append(mSubId);
+ builder.append("] ");
+ return builder;
+ }
+
+ private String getNumberFromUri(Uri uri) {
+ if (uri == null) return null;
+ String number = uri.getSchemeSpecificPart();
+ String[] numberParts = number.split("[@;:]");
+
+ if (numberParts.length == 0) {
+ return null;
+ }
+ return numberParts[0];
+ }
+}
diff --git a/src/java/com/android/ims/rcs/uce/request/UceRequestRepository.java b/src/java/com/android/ims/rcs/uce/request/UceRequestRepository.java
new file mode 100644
index 00000000..1d2c1e86
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/request/UceRequestRepository.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2021 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.ims.rcs.uce.request;
+
+import com.android.ims.rcs.uce.request.UceRequestManager.RequestManagerCallback;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * This class is responsible for storing the capabilities request.
+ */
+public class UceRequestRepository {
+
+ // Dispatch the UceRequest to be executed.
+ private final UceRequestDispatcher mDispatcher;
+
+ // Store all the capabilities requests
+ private final Map<Long, UceRequestCoordinator> mRequestCoordinators;
+
+ private volatile boolean mDestroyed = false;
+
+ public UceRequestRepository(int subId, RequestManagerCallback callback) {
+ mRequestCoordinators = new HashMap<>();
+ mDispatcher = new UceRequestDispatcher(subId, callback);
+ }
+
+ /**
+ * Clear the collection when the instance is destroyed.
+ */
+ public synchronized void onDestroy() {
+ mDestroyed = true;
+ mDispatcher.onDestroy();
+ mRequestCoordinators.forEach((taskId, requestCoord) -> requestCoord.onFinish());
+ mRequestCoordinators.clear();
+ }
+
+ /**
+ * Add new UceRequestCoordinator and notify the RequestDispatcher to check whether the given
+ * requests can be executed or not.
+ */
+ public synchronized void addRequestCoordinator(UceRequestCoordinator coordinator) {
+ if (mDestroyed) return;
+ mRequestCoordinators.put(coordinator.getCoordinatorId(), coordinator);
+ mDispatcher.addRequest(coordinator.getCoordinatorId(),
+ coordinator.getActivatedRequestTaskIds());
+ }
+
+ /**
+ * Remove the RequestCoordinator from the RequestCoordinator collection.
+ */
+ public synchronized UceRequestCoordinator removeRequestCoordinator(Long coordinatorId) {
+ return mRequestCoordinators.remove(coordinatorId);
+
+ }
+
+ /**
+ * Retrieve the RequestCoordinator associated with the given coordinatorId.
+ */
+ public synchronized UceRequestCoordinator getRequestCoordinator(Long coordinatorId) {
+ return mRequestCoordinators.get(coordinatorId);
+ }
+
+ public synchronized UceRequest getUceRequest(Long taskId) {
+ for (UceRequestCoordinator coordinator : mRequestCoordinators.values()) {
+ UceRequest request = coordinator.getUceRequest(taskId);
+ if (request != null) {
+ return request;
+ }
+ }
+ return null;
+ }
+
+ // Notify that the task is finished.
+ public synchronized void notifyRequestFinished(Long taskId) {
+ mDispatcher.onRequestFinished(taskId);
+ }
+}
diff --git a/src/java/com/android/ims/rcs/uce/util/FeatureTags.java b/src/java/com/android/ims/rcs/uce/util/FeatureTags.java
new file mode 100644
index 00000000..bba51fb0
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/util/FeatureTags.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2021 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.ims.rcs.uce.util;
+
+import android.net.Uri;
+import android.telephony.ims.RcsContactUceCapability;
+import android.telephony.ims.RcsContactUceCapability.OptionsBuilder;
+import android.telephony.ims.RcsContactUceCapability.SourceType;
+
+import java.util.List;
+import java.util.Set;
+
+/**
+ * The util class of the feature tags.
+ */
+public class FeatureTags {
+
+ public static final String FEATURE_TAG_STANDALONE_MSG =
+ "+g.3gpp.icsi-ref=\"urn%3Aurn-7%3A3gpp-"
+ + "service.ims.icsi.oma.cpm.msg,urn%3Aurn-7%3A3gpp-"
+ + "service.ims.icsi.oma.cpm.largemsg,urn%3Aurn-7%3A3gpp-"
+ + "service.ims.icsi.oma.cpm.deferred\";+g.gsma.rcs.cpm.pager-large";
+
+ public static final String FEATURE_TAG_CHAT_IM =
+ "+g.3gpp.iari-ref=\"urn%3Aurn-7%3A3gpp-application.ims.iari.rcse.im\"";
+
+ public static final String FEATURE_TAG_CHAT_SESSION =
+ "+g.3gpp.icsi-ref=\"urn%3Aurn-7%3A3gpp-service.ims.icsi.oma.cpm.session\"";
+
+ public static final String FEATURE_TAG_FILE_TRANSFER =
+ "+g.3gpp.iari-ref=\"urn%3Aurn-7%3A3gpp-application.ims.iari.rcs.fthttp\"";
+
+ public static final String FEATURE_TAG_FILE_TRANSFER_VIA_SMS =
+ "+g.3gpp.iari-ref=\"urn%3Aurn-7%3A3gpp-application.ims.iari.rcs.ftsms\"";
+
+ public static final String FEATURE_TAG_CALL_COMPOSER_ENRICHED_CALLING =
+ "+g.3gpp.icsi-ref=\"urn%3Aurn-7%3A3gpp-service.ims.icsi.gsma.callcomposer\"";
+
+ public static final String FEATURE_TAG_CALL_COMPOSER_VIA_TELEPHONY = "+g.gsma.callcomposer";
+
+ public static final String FEATURE_TAG_POST_CALL =
+ "+g.3gpp.icsi-ref=\"urn%3Aurn-7%3A3gpp-service.ims.icsi.gsma.callunanswered\"";
+
+ public static final String FEATURE_TAG_SHARED_MAP =
+ "+g.3gpp.icsi-ref=\"urn%3Aurn-7%3A3gpp-service.ims.icsi.gsma.sharedmap\"";
+
+ public static final String FEATURE_TAG_SHARED_SKETCH =
+ "+g.3gpp.icsi-ref=\"urn%3Aurn-7%3A3gpp-service.ims.icsi.gsma.sharedsketch\"";
+
+ public static final String FEATURE_TAG_GEO_PUSH =
+ "+g.3gpp.iari-ref=\"urn%3Aurn-7%3A3gpp-application.ims.iari.rcs.geopush\"";
+
+ public static final String FEATURE_TAG_GEO_PUSH_VIA_SMS =
+ "+g.3gpp.iari-ref=\"urn%3Aurn-7%3A3gpp-application.ims.iari.rcs.geosms\"";
+
+ public static final String FEATURE_TAG_CHATBOT_COMMUNICATION_USING_SESSION =
+ "+g.3gpp.iari-ref=\"urn%3Aurn-7%3A3gpp-application.ims.iari.rcs.chatbot\"";
+
+ public static final String FEATURE_TAG_CHATBOT_COMMUNICATION_USING_STANDALONE_MSG =
+ "+g.3gpp.iari-ref=\"urn%3Aurn-7%3A3gpp-application.ims.iari.rcs.chatbot.sa\"";
+
+ public static final String FEATURE_TAG_CHATBOT_VERSION_SUPPORTED =
+ "+g.gsma.rcs.botversion=\"#=1,#=2\"";
+
+ public static final String FEATURE_TAG_CHATBOT_ROLE = "+g.gsma.rcs.isbot";
+
+ public static final String FEATURE_TAG_MMTEL =
+ "+g.3gpp.icsi-ref=\"urn%3Aurn-7%3A3gpp-service.ims.icsi.mmtel\"";
+
+ public static final String FEATURE_TAG_VIDEO = "video";
+
+ public static final String FEATURE_TAG_PRESENCE =
+ "+g.3gpp.iari-ref=\"urn%3Aurn-7%3A3gpp-application.ims.iari.rcse.dp\"";
+
+ /**
+ * Add the feature tags to the given RcsContactUceCapability OPTIONS builder.
+ * @param optionsBuilder The OptionsBuilder to add the feature tags
+ * @param mmtelAudioSupport If the audio capability is supported
+ * @param mmtelVideoSupport If the video capability is supported
+ * @param presenceSupport If presence is also supported
+ * @param callComposerSupport If call composer via telephony is supported
+ * @param registrationTags The other feature tags included in the IMS registration.
+ */
+ public static void addFeatureTags(final OptionsBuilder optionsBuilder,
+ boolean mmtelAudioSupport, boolean mmtelVideoSupport,
+ boolean presenceSupport, boolean callComposerSupport, Set<String> registrationTags) {
+ if (presenceSupport) {
+ registrationTags.add(FEATURE_TAG_PRESENCE);
+ } else {
+ registrationTags.remove(FEATURE_TAG_PRESENCE);
+ }
+ if (mmtelAudioSupport && mmtelVideoSupport) {
+ registrationTags.add(FEATURE_TAG_MMTEL);
+ registrationTags.add(FEATURE_TAG_VIDEO);
+ } else if (mmtelAudioSupport) {
+ registrationTags.add(FEATURE_TAG_MMTEL);
+ registrationTags.remove(FEATURE_TAG_VIDEO);
+ } else {
+ registrationTags.remove(FEATURE_TAG_MMTEL);
+ registrationTags.remove(FEATURE_TAG_VIDEO);
+ }
+ if (callComposerSupport) {
+ registrationTags.add(FEATURE_TAG_CALL_COMPOSER_VIA_TELEPHONY);
+ } else {
+ registrationTags.remove(FEATURE_TAG_CALL_COMPOSER_VIA_TELEPHONY);
+ }
+ if (!registrationTags.isEmpty()) {
+ optionsBuilder.addFeatureTags(registrationTags);
+ }
+ }
+
+ /**
+ * Get RcsContactUceCapabilities from the given feature tags.
+ */
+ public static RcsContactUceCapability getContactCapability(Uri contact,
+ @SourceType int sourceType, List<String> featureTags) {
+ OptionsBuilder builder = new OptionsBuilder(contact, sourceType);
+ builder.setRequestResult(RcsContactUceCapability.REQUEST_RESULT_FOUND);
+ featureTags.forEach(feature -> builder.addFeatureTag(feature));
+ return builder.build();
+ }
+}
diff --git a/src/java/com/android/ims/rcs/uce/util/NetworkSipCode.java b/src/java/com/android/ims/rcs/uce/util/NetworkSipCode.java
new file mode 100644
index 00000000..a1e35d76
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/util/NetworkSipCode.java
@@ -0,0 +1,113 @@
+/*
+ * 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.ims.rcs.uce.util;
+
+import android.telephony.ims.RcsUceAdapter;
+
+import com.android.ims.rcs.uce.UceController;
+import com.android.ims.rcs.uce.UceController.RequestType;
+
+/**
+ * Define the network sip code and the reason.
+ */
+public class NetworkSipCode {
+ public static final int SIP_CODE_OK = 200;
+ public static final int SIP_CODE_ACCEPTED = 202;
+ public static final int SIP_CODE_BAD_REQUEST = 400;
+ public static final int SIP_CODE_FORBIDDEN = 403;
+ public static final int SIP_CODE_NOT_FOUND = 404;
+ public static final int SIP_CODE_METHOD_NOT_ALLOWED = 405;
+ public static final int SIP_CODE_REQUEST_TIMEOUT = 408;
+ public static final int SIP_CODE_INTERVAL_TOO_BRIEF = 423;
+ public static final int SIP_CODE_TEMPORARILY_UNAVAILABLE = 480;
+ public static final int SIP_CODE_BAD_EVENT = 489;
+ public static final int SIP_CODE_BUSY = 486;
+ public static final int SIP_CODE_SERVER_INTERNAL_ERROR = 500;
+ public static final int SIP_CODE_SERVICE_UNAVAILABLE = 503;
+ public static final int SIP_CODE_SERVER_TIMEOUT = 504;
+ public static final int SIP_CODE_BUSY_EVERYWHERE = 600;
+ public static final int SIP_CODE_DECLINE = 603;
+ public static final int SIP_CODE_DOES_NOT_EXIST_ANYWHERE = 604;
+
+ public static final String SIP_OK = "OK";
+ public static final String SIP_ACCEPTED = "Accepted";
+ public static final String SIP_BAD_REQUEST = "Bad Request";
+ public static final String SIP_SERVICE_UNAVAILABLE = "Service Unavailable";
+ public static final String SIP_INTERNAL_SERVER_ERROR = "Internal Server Error";
+ public static final String SIP_NOT_REGISTERED = "User not registered";
+ public static final String SIP_NOT_AUTHORIZED_FOR_PRESENCE = "not authorized for presence";
+
+ /**
+ * Convert the given SIP CODE to the Contact uce capabilities error.
+ * @param sipCode The SIP code of the request response.
+ * @param reason The reason of the request response.
+ * @param requestType The type of this request.
+ * @return The RCS contact UCE capabilities error which is defined in RcsUceAdapter.
+ */
+ public static int getCapabilityErrorFromSipCode(int sipCode, String reason,
+ @RequestType int requestType) {
+ int uceError;
+ switch (sipCode) {
+ case NetworkSipCode.SIP_CODE_FORBIDDEN: // 403
+ if(requestType == UceController.REQUEST_TYPE_PUBLISH) {
+ // Not provisioned for PUBLISH request.
+ uceError = RcsUceAdapter.ERROR_NOT_AUTHORIZED;
+ } else {
+ // Check the reason for CAPABILITY request
+ if (NetworkSipCode.SIP_NOT_REGISTERED.equalsIgnoreCase(reason)) {
+ // Not registered with IMS. Device shall register to IMS.
+ uceError = RcsUceAdapter.ERROR_NOT_REGISTERED;
+ } else if (NetworkSipCode.SIP_NOT_AUTHORIZED_FOR_PRESENCE.equalsIgnoreCase(
+ reason)) {
+ // Not provisioned for EAB. Device shall not retry.
+ uceError = RcsUceAdapter.ERROR_NOT_AUTHORIZED;
+ } else {
+ // The network has responded SIP 403 error with no reason.
+ uceError = RcsUceAdapter.ERROR_FORBIDDEN;
+ }
+ }
+ break;
+ case NetworkSipCode.SIP_CODE_NOT_FOUND: // 404
+ if(requestType == UceController.REQUEST_TYPE_PUBLISH) {
+ // Not provisioned for PUBLISH request.
+ uceError = RcsUceAdapter.ERROR_NOT_AUTHORIZED;
+ } else {
+ uceError = RcsUceAdapter.ERROR_NOT_FOUND;
+ }
+ break;
+ case NetworkSipCode.SIP_CODE_REQUEST_TIMEOUT: // 408
+ uceError = RcsUceAdapter.ERROR_REQUEST_TIMEOUT;
+ break;
+ case NetworkSipCode.SIP_CODE_INTERVAL_TOO_BRIEF: // 423
+ // Rejected by the network because the requested expiry interval is too short.
+ uceError = RcsUceAdapter.ERROR_GENERIC_FAILURE;
+ break;
+ case NetworkSipCode.SIP_CODE_BAD_EVENT:
+ uceError = RcsUceAdapter.ERROR_FORBIDDEN; // 489
+ break;
+ case NetworkSipCode.SIP_CODE_SERVER_INTERNAL_ERROR: // 500
+ case NetworkSipCode.SIP_CODE_SERVICE_UNAVAILABLE: // 503
+ // The network is temporarily unavailable or busy.
+ uceError = RcsUceAdapter.ERROR_SERVER_UNAVAILABLE;
+ break;
+ default:
+ uceError = RcsUceAdapter.ERROR_GENERIC_FAILURE;
+ break;
+ }
+ return uceError;
+ }
+}
diff --git a/src/java/com/android/ims/rcs/uce/util/UceUtils.java b/src/java/com/android/ims/rcs/uce/util/UceUtils.java
new file mode 100644
index 00000000..e5ba6a96
--- /dev/null
+++ b/src/java/com/android/ims/rcs/uce/util/UceUtils.java
@@ -0,0 +1,402 @@
+/*
+ * 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.ims.rcs.uce.util;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.net.Uri;
+import android.os.PersistableBundle;
+import android.preference.PreferenceManager;
+import android.provider.BlockedNumberContract;
+import android.telephony.CarrierConfigManager;
+import android.telephony.SubscriptionManager;
+import android.telephony.ims.ProvisioningManager;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.ims.rcs.uce.UceDeviceState.DeviceStateResult;
+
+import java.time.Instant;
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+
+public class UceUtils {
+
+ public static final int LOG_SIZE = 20;
+ private static final String LOG_PREFIX = "RcsUce.";
+ private static final String LOG_TAG = LOG_PREFIX + "UceUtils";
+
+ private static final String SHARED_PREF_DEVICE_STATE_KEY = "UceDeviceState";
+
+ private static final int DEFAULT_RCL_MAX_NUM_ENTRIES = 100;
+ private static final long DEFAULT_RCS_PUBLISH_SOURCE_THROTTLE_MS = 60000L;
+ private static final long DEFAULT_NON_RCS_CAPABILITIES_CACHE_EXPIRATION_SEC =
+ TimeUnit.DAYS.toSeconds(30);
+ private static final long DEFAULT_REQUEST_RETRY_INTERVAL_MS = TimeUnit.MINUTES.toMillis(20);
+ private static final long DEFAULT_MINIMUM_REQUEST_RETRY_AFTER_MS = TimeUnit.SECONDS.toMillis(3);
+
+ private static final long DEFAULT_CAP_REQUEST_TIMEOUT_AFTER_MS = TimeUnit.MINUTES.toMillis(1);
+ private static Optional<Long> OVERRIDE_CAP_REQUEST_TIMEOUT_AFTER_MS = Optional.empty();
+
+ // The task ID of the UCE request
+ private static long TASK_ID = 0L;
+
+ // The request coordinator ID
+ private static long REQUEST_COORDINATOR_ID = 0;
+
+ /**
+ * Get the log prefix of RCS UCE
+ */
+ public static String getLogPrefix() {
+ return LOG_PREFIX;
+ }
+
+ /**
+ * Generate the unique UCE request task id.
+ */
+ public static synchronized long generateTaskId() {
+ return ++TASK_ID;
+ }
+
+ /**
+ * Generate the unique request coordinator id.
+ */
+ public static synchronized long generateRequestCoordinatorId() {
+ return ++REQUEST_COORDINATOR_ID;
+ }
+
+ public static boolean isEabProvisioned(Context context, int subId) {
+ boolean isProvisioned = false;
+ if (!SubscriptionManager.isValidSubscriptionId(subId)) {
+ Log.w(LOG_TAG, "isEabProvisioned: invalid subscriptionId " + subId);
+ return false;
+ }
+ CarrierConfigManager configManager = (CarrierConfigManager)
+ context.getSystemService(Context.CARRIER_CONFIG_SERVICE);
+ if (configManager != null) {
+ PersistableBundle config = configManager.getConfigForSubId(subId);
+ if (config != null && !config.getBoolean(
+ CarrierConfigManager.KEY_CARRIER_VOLTE_PROVISIONED_BOOL)) {
+ return true;
+ }
+ }
+ try {
+ ProvisioningManager manager = ProvisioningManager.createForSubscriptionId(subId);
+ isProvisioned = manager.getProvisioningIntValue(
+ ProvisioningManager.KEY_EAB_PROVISIONING_STATUS)
+ == ProvisioningManager.PROVISIONING_VALUE_ENABLED;
+ } catch (Exception e) {
+ Log.w(LOG_TAG, "isEabProvisioned: exception=" + e.getMessage());
+ }
+ return isProvisioned;
+ }
+
+ /**
+ * Check whether or not this carrier supports the exchange of phone numbers with the carrier's
+ * presence server.
+ */
+ public static boolean isPresenceCapExchangeEnabled(Context context, int subId) {
+ CarrierConfigManager configManager = context.getSystemService(CarrierConfigManager.class);
+ if (configManager == null) {
+ return false;
+ }
+ PersistableBundle config = configManager.getConfigForSubId(subId);
+ if (config == null) {
+ return false;
+ }
+ return config.getBoolean(
+ CarrierConfigManager.Ims.KEY_ENABLE_PRESENCE_CAPABILITY_EXCHANGE_BOOL);
+ }
+
+ /**
+ * Check if Presence is supported by the carrier.
+ */
+ public static boolean isPresenceSupported(Context context, int subId) {
+ CarrierConfigManager configManager = context.getSystemService(CarrierConfigManager.class);
+ if (configManager == null) {
+ return false;
+ }
+ PersistableBundle config = configManager.getConfigForSubId(subId);
+ if (config == null) {
+ return false;
+ }
+ return config.getBoolean(CarrierConfigManager.Ims.KEY_ENABLE_PRESENCE_PUBLISH_BOOL);
+ }
+
+ /**
+ * Check if SIP OPTIONS is supported by the carrier.
+ */
+ public static boolean isSipOptionsSupported(Context context, int subId) {
+ CarrierConfigManager configManager = context.getSystemService(CarrierConfigManager.class);
+ if (configManager == null) {
+ return false;
+ }
+ PersistableBundle config = configManager.getConfigForSubId(subId);
+ if (config == null) {
+ return false;
+ }
+ return config.getBoolean(CarrierConfigManager.KEY_USE_RCS_SIP_OPTIONS_BOOL);
+ }
+
+ /**
+ * Check whether the PRESENCE group subscribe is enabled or not.
+ *
+ * @return true when the Presence group subscribe is enabled, false otherwise.
+ */
+ public static boolean isPresenceGroupSubscribeEnabled(Context context, int subId) {
+ CarrierConfigManager configManager = context.getSystemService(CarrierConfigManager.class);
+ if (configManager == null) {
+ return false;
+ }
+ PersistableBundle config = configManager.getConfigForSubId(subId);
+ if (config == null) {
+ return false;
+ }
+ return config.getBoolean(CarrierConfigManager.Ims.KEY_ENABLE_PRESENCE_GROUP_SUBSCRIBE_BOOL);
+ }
+
+ /**
+ * Returns {@code true} if {@code phoneNumber} is blocked.
+ *
+ * @param context the context of the caller.
+ * @param phoneNumber the number to check.
+ * @return true if the number is blocked, false otherwise.
+ */
+ public static boolean isNumberBlocked(Context context, String phoneNumber) {
+ int blockStatus;
+ try {
+ blockStatus = BlockedNumberContract.SystemContract.shouldSystemBlockNumber(
+ context, phoneNumber, null /*extras*/);
+ } catch (Exception e) {
+ return false;
+ }
+ return blockStatus != BlockedNumberContract.STATUS_NOT_BLOCKED;
+ }
+
+ /**
+ * Get the minimum time that allow two PUBLISH requests can be executed continuously.
+ *
+ * @param subId The subscribe ID
+ * @return The milliseconds that allowed two consecutive publish request.
+ */
+ public static long getRcsPublishThrottle(int subId) {
+ long throttle = DEFAULT_RCS_PUBLISH_SOURCE_THROTTLE_MS;
+ try {
+ ProvisioningManager manager = ProvisioningManager.createForSubscriptionId(subId);
+ long provisioningValue = manager.getProvisioningIntValue(
+ ProvisioningManager.KEY_RCS_PUBLISH_SOURCE_THROTTLE_MS);
+ if (provisioningValue > 0) {
+ throttle = provisioningValue;
+ }
+ } catch (Exception e) {
+ Log.w(LOG_TAG, "getRcsPublishThrottle: exception=" + e.getMessage());
+ }
+ return throttle;
+ }
+
+ /**
+ * Retrieve the maximum number of contacts that is in one Request Contained List(RCL)
+ *
+ * @param subId The subscribe ID
+ * @return The maximum number of contacts.
+ */
+ public static int getRclMaxNumberEntries(int subId) {
+ int maxNumEntries = DEFAULT_RCL_MAX_NUM_ENTRIES;
+ try {
+ ProvisioningManager manager = ProvisioningManager.createForSubscriptionId(subId);
+ int provisioningValue = manager.getProvisioningIntValue(
+ ProvisioningManager.KEY_RCS_MAX_NUM_ENTRIES_IN_RCL);
+ if (provisioningValue > 0) {
+ maxNumEntries = provisioningValue;
+ }
+ } catch (Exception e) {
+ Log.w(LOG_TAG, "getRclMaxNumberEntries: exception=" + e.getMessage());
+ }
+ return maxNumEntries;
+ }
+
+ public static long getNonRcsCapabilitiesCacheExpiration(Context context, int subId) {
+ CarrierConfigManager configManager = context.getSystemService(CarrierConfigManager.class);
+ if (configManager == null) {
+ return DEFAULT_NON_RCS_CAPABILITIES_CACHE_EXPIRATION_SEC;
+ }
+ PersistableBundle config = configManager.getConfigForSubId(subId);
+ if (config == null) {
+ return DEFAULT_NON_RCS_CAPABILITIES_CACHE_EXPIRATION_SEC;
+ }
+ return config.getInt(
+ CarrierConfigManager.Ims.KEY_NON_RCS_CAPABILITIES_CACHE_EXPIRATION_SEC_INT);
+ }
+
+ public static boolean isRequestForbiddenBySip489(Context context, int subId) {
+ CarrierConfigManager configManager = context.getSystemService(CarrierConfigManager.class);
+ if (configManager == null) {
+ return false;
+ }
+ PersistableBundle config = configManager.getConfigForSubId(subId);
+ if (config == null) {
+ return false;
+ }
+ return config.getBoolean(
+ CarrierConfigManager.Ims.KEY_RCS_REQUEST_FORBIDDEN_BY_SIP_489_BOOL);
+ }
+
+ public static long getRequestRetryInterval(Context context, int subId) {
+ CarrierConfigManager configManager = context.getSystemService(CarrierConfigManager.class);
+ if (configManager == null) {
+ return DEFAULT_REQUEST_RETRY_INTERVAL_MS;
+ }
+ PersistableBundle config = configManager.getConfigForSubId(subId);
+ if (config == null) {
+ return DEFAULT_REQUEST_RETRY_INTERVAL_MS;
+ }
+ return config.getLong(
+ CarrierConfigManager.Ims.KEY_RCS_REQUEST_RETRY_INTERVAL_MILLIS_LONG);
+ }
+
+ public static boolean saveDeviceStateToPreference(Context context, int subId,
+ DeviceStateResult deviceState) {
+ SharedPreferences sharedPreferences =
+ PreferenceManager.getDefaultSharedPreferences(context);
+ SharedPreferences.Editor editor = sharedPreferences.edit();
+ editor.putString(getDeviceStateSharedPrefKey(subId),
+ getDeviceStateSharedPrefValue(deviceState));
+ return editor.commit();
+ }
+
+ public static Optional<DeviceStateResult> restoreDeviceState(Context context, int subId) {
+ SharedPreferences sharedPreferences =
+ PreferenceManager.getDefaultSharedPreferences(context);
+ final String sharedPrefKey = getDeviceStateSharedPrefKey(subId);
+ String sharedPrefValue = sharedPreferences.getString(sharedPrefKey, "");
+ if (TextUtils.isEmpty(sharedPrefValue)) {
+ return Optional.empty();
+ }
+ String[] valueAry = sharedPrefValue.split(",");
+ if (valueAry == null || valueAry.length != 4) {
+ return Optional.empty();
+ }
+ try {
+ int deviceState = Integer.valueOf(valueAry[0]);
+ Optional<Integer> errorCode = (Integer.valueOf(valueAry[1]) == -1L) ?
+ Optional.empty() : Optional.of(Integer.valueOf(valueAry[1]));
+
+ long retryTimeMillis = Long.valueOf(valueAry[2]);
+ Optional<Instant> retryTime = (retryTimeMillis == -1L) ?
+ Optional.empty() : Optional.of(Instant.ofEpochMilli(retryTimeMillis));
+
+ long exitStateTimeMillis = Long.valueOf(valueAry[3]);
+ Optional<Instant> exitStateTime = (exitStateTimeMillis == -1L) ?
+ Optional.empty() : Optional.of(Instant.ofEpochMilli(exitStateTimeMillis));
+
+ return Optional.of(new DeviceStateResult(deviceState, errorCode, retryTime,
+ exitStateTime));
+ } catch (Exception e) {
+ Log.d(LOG_TAG, "restoreDeviceState: exception " + e);
+ return Optional.empty();
+ }
+ }
+
+ public static boolean removeDeviceStateFromPreference(Context context, int subId) {
+ SharedPreferences sharedPreferences =
+ PreferenceManager.getDefaultSharedPreferences(context);
+ SharedPreferences.Editor editor = sharedPreferences.edit();
+ editor.remove(getDeviceStateSharedPrefKey(subId));
+ return editor.commit();
+ }
+
+ private static String getDeviceStateSharedPrefKey(int subId) {
+ return SHARED_PREF_DEVICE_STATE_KEY + subId;
+ }
+
+ /**
+ * Build the device state preference value.
+ */
+ private static String getDeviceStateSharedPrefValue(DeviceStateResult deviceState) {
+ StringBuilder builder = new StringBuilder();
+ builder.append(deviceState.getDeviceState()) // device state
+ .append(",").append(deviceState.getErrorCode().orElse(-1)); // error code
+
+ long retryTimeMillis = -1L;
+ Optional<Instant> retryTime = deviceState.getRequestRetryTime();
+ if (retryTime.isPresent()) {
+ retryTimeMillis = retryTime.get().toEpochMilli();
+ }
+ builder.append(",").append(retryTimeMillis); // retryTime
+
+ long exitStateTimeMillis = -1L;
+ Optional<Instant> exitStateTime = deviceState.getExitStateTime();
+ if (exitStateTime.isPresent()) {
+ exitStateTimeMillis = exitStateTime.get().toEpochMilli();
+ }
+ builder.append(",").append(exitStateTimeMillis); // exit state time
+ return builder.toString();
+ }
+
+ /**
+ * Get the minimum value of the capabilities request retry after.
+ */
+ public static long getMinimumRequestRetryAfterMillis() {
+ return DEFAULT_MINIMUM_REQUEST_RETRY_AFTER_MS;
+ }
+
+ /**
+ * Override the capability request timeout to the millisecond value specified. Sending a
+ * value <= 0 will reset the capabilities.
+ */
+ public static synchronized void setCapRequestTimeoutAfterMillis(long timeoutAfterMs) {
+ if (timeoutAfterMs <= 0L) {
+ OVERRIDE_CAP_REQUEST_TIMEOUT_AFTER_MS = Optional.empty();
+ } else {
+ OVERRIDE_CAP_REQUEST_TIMEOUT_AFTER_MS = Optional.of(timeoutAfterMs);
+ }
+ }
+
+ /**
+ * Get the milliseconds of the capabilities request timed out.
+ * @return the time in milliseconds before a pending capabilities request will time out.
+ */
+ public static synchronized long getCapRequestTimeoutAfterMillis() {
+ if(OVERRIDE_CAP_REQUEST_TIMEOUT_AFTER_MS.isPresent()) {
+ return OVERRIDE_CAP_REQUEST_TIMEOUT_AFTER_MS.get();
+ } else {
+ return DEFAULT_CAP_REQUEST_TIMEOUT_AFTER_MS;
+ }
+ }
+
+ /**
+ * Get the contact number from the given URI.
+ * @param contactUri The contact uri of the capabilities to request for.
+ * @return The number of the contact uri. NULL if the number cannot be retrieved.
+ */
+ public static String getContactNumber(Uri contactUri) {
+ if (contactUri == null) {
+ return null;
+ }
+ String number = contactUri.getSchemeSpecificPart();
+ if (TextUtils.isEmpty(number)) {
+ return null;
+ }
+
+ String numberParts[] = number.split("[@;:]");
+ if (numberParts.length == 0) {
+ Log.d(LOG_TAG, "getContactNumber: the length of numberPars is 0");
+ return contactUri.toString();
+ }
+ return numberParts[0];
+ }
+}
diff --git a/tests/Android.bp b/tests/Android.bp
index 0d0440b6..82c303d5 100644
--- a/tests/Android.bp
+++ b/tests/Android.bp
@@ -14,6 +14,10 @@
// limitations under the License.
//
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
android_test {
name: "ImsCommonTests",
@@ -25,11 +29,17 @@ android_test {
libs: [
"ims-common",
"android.test.runner",
+ "android.test.mock",
"android.test.base",
],
static_libs: [
+ "androidx.test.ext.junit",
"androidx.test.rules",
"mockito-target-minus-junit4",
],
+
+ test_suites: [
+ "device-tests"
+ ]
}
diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml
index 706035a4..88831aa4 100644
--- a/tests/AndroidManifest.xml
+++ b/tests/AndroidManifest.xml
@@ -18,6 +18,11 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.ims.tests">
+ <!-- For EabBulkCapabilityUpdaterTest, EabBulkCapabilityUpdater will register content
+ observer to contact provider but currently there is no better way to mock contact provider
+ (registerContentObserver() is final), so require the read_contacts permission to test APK.-->
+ <uses-permission android:name="android.permission.READ_CONTACTS"/>
+
<application android:label="@string/app_name">
<uses-library android:name="android.test.runner" />
</application>
diff --git a/tests/AndroidTest.xml b/tests/AndroidTest.xml
new file mode 100644
index 00000000..4610122e
--- /dev/null
+++ b/tests/AndroidTest.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+<configuration description="Runs Frameworks IMS Tests.">
+ <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
+ <option name="test-file-name" value="ImsCommonTests.apk" />
+ </target_preparer>
+
+ <option name="test-suite-tag" value="apct" />
+ <option name="test-tag" value="ImsCommonTests" />
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+ <option name="package" value="com.android.ims.tests" />
+ <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+ <option name="hidden-api-checks" value="false"/>
+ </test>
+</configuration>
diff --git a/tests/src/com/android/ims/ContextFixture.java b/tests/src/com/android/ims/ContextFixture.java
new file mode 100644
index 00000000..e987b387
--- /dev/null
+++ b/tests/src/com/android/ims/ContextFixture.java
@@ -0,0 +1,168 @@
+/*
+ * 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.ims;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.IContentProvider;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.net.ConnectivityManager;
+import android.os.PersistableBundle;
+import android.telecom.TelecomManager;
+import android.telephony.CarrierConfigManager;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+import android.telephony.ims.ImsManager;
+import android.test.mock.MockContentResolver;
+import android.test.mock.MockContext;
+
+import org.mockito.stubbing.Answer;
+
+import java.util.HashSet;
+import java.util.concurrent.Executor;
+
+public class ContextFixture {
+
+ private final Context mContext = spy(new FakeContext());
+
+ private final TelephonyManager mTelephonyManager = mock(TelephonyManager.class);
+ private final ConnectivityManager mConnectivityManager = mock(ConnectivityManager.class);
+ private final CarrierConfigManager mCarrierConfigManager = mock(CarrierConfigManager.class);
+ private final PackageManager mPackageManager = mock(PackageManager.class);
+ private final SubscriptionManager mSubscriptionManager = mock(SubscriptionManager.class);
+ private final ImsManager mImsManager = mock(ImsManager.class);
+ private final Resources mResources = mock(Resources.class);
+
+ private final PersistableBundle mBundle = new PersistableBundle();
+ private final HashSet<String> mSystemFeatures = new HashSet<>();
+ private final MockContentResolver mMockContentResolver = new MockContentResolver();
+
+ public ContextFixture() throws Exception {
+ doReturn(mBundle).when(mCarrierConfigManager).getConfigForSubId(anyInt());
+ doReturn(mBundle).when(mCarrierConfigManager).getConfig();
+
+ doAnswer((Answer<Boolean>)
+ invocation -> mSystemFeatures.contains((String) invocation.getArgument(0)))
+ .when(mPackageManager).hasSystemFeature(any());
+
+ doReturn(mResources).when(mPackageManager).getResourcesForApplication(anyString());
+ doReturn(mTelephonyManager).when(mTelephonyManager).createForSubscriptionId(anyInt());
+ }
+
+ public void destroy() {
+ }
+
+ public class FakeContext extends MockContext {
+ @Override
+ public Resources getResources() {
+ return mResources;
+ }
+
+ @Override
+ public PackageManager getPackageManager() {
+ return mPackageManager;
+ }
+
+ @Override
+ public Object getSystemService(String name) {
+ switch (name) {
+ case Context.TELEPHONY_SERVICE:
+ return mTelephonyManager;
+ case Context.CARRIER_CONFIG_SERVICE:
+ return mCarrierConfigManager;
+ case Context.CONNECTIVITY_SERVICE:
+ return mConnectivityManager;
+ case Context.TELEPHONY_SUBSCRIPTION_SERVICE:
+ return mSubscriptionManager;
+ case Context.TELEPHONY_IMS_SERVICE:
+ return mImsManager;
+ default:
+ return null;
+ }
+ }
+
+ @Override
+ public String getSystemServiceName(Class<?> serviceClass) {
+ if (serviceClass == SubscriptionManager.class) {
+ return Context.TELEPHONY_SUBSCRIPTION_SERVICE;
+ } else if (serviceClass == TelecomManager.class) {
+ return Context.TELECOM_SERVICE;
+ } else if (serviceClass == ConnectivityManager.class) {
+ return Context.CONNECTIVITY_SERVICE;
+ } else if (serviceClass == TelephonyManager.class) {
+ return Context.TELEPHONY_SERVICE;
+ } else if (serviceClass == ImsManager.class) {
+ return Context.TELEPHONY_IMS_SERVICE;
+ } else if (serviceClass == CarrierConfigManager.class) {
+ return Context.CARRIER_CONFIG_SERVICE;
+ }
+ return super.getSystemServiceName(serviceClass);
+ }
+
+ @Override
+ public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
+ return null;
+ }
+
+ @Override
+ public void unregisterReceiver(BroadcastReceiver receiver) {
+ }
+
+ @Override
+ public ContentResolver getContentResolver() {
+ return mMockContentResolver;
+ }
+
+ @Override
+ public Executor getMainExecutor() {
+ return Runnable::run;
+ }
+
+ @Override
+ public Context getApplicationContext() {
+ return mContext;
+ }
+ }
+
+ public Context getContext() {
+ return mContext;
+ }
+
+ public PersistableBundle getTestCarrierConfigBundle() {
+ return mBundle;
+ }
+
+ public void addSystemFeature(String feature) {
+ mSystemFeatures.add(feature);
+ }
+
+ public void removeSystemFeature(String feature) {
+ mSystemFeatures.remove(feature);
+ }
+}
diff --git a/tests/src/com/android/ims/FeatureConnectionTest.java b/tests/src/com/android/ims/FeatureConnectionTest.java
new file mode 100644
index 00000000..d7a9134c
--- /dev/null
+++ b/tests/src/com/android/ims/FeatureConnectionTest.java
@@ -0,0 +1,177 @@
+/*
+ * 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.ims;
+
+import junit.framework.AssertionFailedError;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.telephony.ims.aidl.IImsConfig;
+import android.telephony.ims.aidl.IImsRegistration;
+import android.telephony.ims.aidl.ISipTransport;
+import android.telephony.ims.feature.ImsFeature;
+import android.telephony.ims.stub.ImsRegistrationImplBase;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+
+@RunWith(AndroidJUnit4.class)
+public class FeatureConnectionTest extends ImsTestBase {
+
+ private class TestFeatureConnection extends FeatureConnection {
+ private Integer mFeatureState = ImsFeature.STATE_READY;
+
+ public boolean isFeatureCreatedCalled = false;
+ public boolean isFeatureRemovedCalled = false;
+ public int mNewStatus = ImsFeature.STATE_UNAVAILABLE;
+ public long mCapabilities;
+
+ TestFeatureConnection(Context context, int slotId) {
+ super(context, slotId, mConfigBinder, mRegistrationBinder, mSipTransportBinder);
+ if (!ImsManager.isImsSupportedOnDevice(context)) {
+ sImsSupportedOnDevice = false;
+ }
+ }
+
+ @Override
+ public void checkServiceIsReady() throws RemoteException {
+ super.checkServiceIsReady();
+ }
+
+ @Override
+ protected Integer retrieveFeatureState() {
+ return mFeatureState;
+ }
+
+ @Override
+ protected void onFeatureCapabilitiesUpdated(long capabilities) {
+ mCapabilities = capabilities;
+ }
+
+ public void setFeatureState(int state) {
+ mFeatureState = state;
+ }
+ };
+
+ private TestFeatureConnection mTestFeatureConnection;
+ @Mock IBinder mBinder;
+ @Mock IImsRegistration mRegistrationBinder;
+ @Mock IImsConfig mConfigBinder;
+ @Mock ISipTransport mSipTransportBinder;
+
+ public static final int PHONE_ID = 1;
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+
+ doReturn(null).when(mContext).getMainLooper();
+ mContextFixture.addSystemFeature(PackageManager.FEATURE_TELEPHONY_IMS);
+
+ mTestFeatureConnection = new TestFeatureConnection(mContext, PHONE_ID);
+ mTestFeatureConnection.setBinder(mBinder);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ /**
+ * Test service is ready when binder is alive and IMS status is ready.
+ */
+ @Test
+ @SmallTest
+ public void testServiceIsReady() {
+ when(mBinder.isBinderAlive()).thenReturn(true);
+ mTestFeatureConnection.setFeatureState(ImsFeature.STATE_READY);
+
+ try {
+ mTestFeatureConnection.checkServiceIsReady();
+ } catch (RemoteException e) {
+ throw new AssertionFailedError("Exception in testServiceIsReady: " + e);
+ }
+ }
+
+ /**
+ * Test service is not ready when binder is not alive or status is not ready.
+ */
+ @Test
+ @SmallTest
+ public void testServiceIsNotReady() {
+ // Binder is not alive
+ when(mBinder.isBinderAlive()).thenReturn(false);
+
+ try {
+ mTestFeatureConnection.checkServiceIsReady();
+ throw new AssertionFailedError("testServiceIsNotReady: binder isn't alive");
+ } catch (RemoteException e) {
+ // expected result
+ }
+
+ // IMS feature status is unavailable
+ when(mBinder.isBinderAlive()).thenReturn(true);
+ mTestFeatureConnection.setFeatureState(ImsFeature.STATE_UNAVAILABLE);
+
+ try {
+ mTestFeatureConnection.checkServiceIsReady();
+ throw new AssertionFailedError("testServiceIsNotReady: status unavailable");
+ } catch (RemoteException e) {
+ // expected result
+ }
+ }
+
+ /**
+ * Test registration tech callbacks.
+ */
+ @Test
+ @SmallTest
+ public void testRegistrationTech() throws Exception {
+ when(mRegistrationBinder.getRegistrationTechnology()).thenReturn(
+ ImsRegistrationImplBase.REGISTRATION_TECH_IWLAN);
+
+ assertEquals(ImsRegistrationImplBase.REGISTRATION_TECH_IWLAN,
+ mTestFeatureConnection.getRegistrationTech());
+
+ }
+
+ /**
+ * Test registration tech callbacks.
+ */
+ @Test
+ @SmallTest
+ public void testUpdateCapabilities() throws Exception {
+ long testCaps = 1;
+ assertEquals(0 /*base state*/, mTestFeatureConnection.mCapabilities);
+ mTestFeatureConnection.updateFeatureCapabilities(testCaps);
+ assertEquals(testCaps, mTestFeatureConnection.mCapabilities);
+
+ }
+}
diff --git a/tests/src/com/android/ims/FeatureConnectorTest.java b/tests/src/com/android/ims/FeatureConnectorTest.java
new file mode 100644
index 00000000..e560ae66
--- /dev/null
+++ b/tests/src/com/android/ims/FeatureConnectorTest.java
@@ -0,0 +1,422 @@
+/*
+ * 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.ims;
+
+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.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.content.Context;
+import android.content.pm.PackageManager;
+import android.os.IBinder;
+import android.telephony.ims.ImsService;
+import android.telephony.ims.aidl.IImsConfig;
+import android.telephony.ims.aidl.IImsRegistration;
+import android.telephony.ims.aidl.ISipTransport;
+import android.telephony.ims.feature.ImsFeature;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.ims.internal.IImsServiceFeatureCallback;
+
+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.List;
+
+@RunWith(AndroidJUnit4.class)
+public class FeatureConnectorTest extends ImsTestBase {
+
+ private static class TestFeatureConnection extends FeatureConnection {
+
+ public TestFeatureConnection(Context context, int slotId, IImsConfig c,
+ IImsRegistration r, ISipTransport s) {
+ super(context, slotId, c, r, s);
+ }
+
+ @Override
+ protected Integer retrieveFeatureState() {
+ return null;
+ }
+
+ @Override
+ protected void onFeatureCapabilitiesUpdated(long capabilities) {
+ }
+ }
+
+ private static class TestManager implements FeatureUpdates {
+
+ public IImsServiceFeatureCallback callback;
+ public TestFeatureConnection connection;
+ private Context mContext;
+ private int mPhoneId;
+
+
+ public TestManager(Context context, int phoneId) {
+ mContext = context;
+ mPhoneId = phoneId;
+ }
+
+ @Override
+ public void registerFeatureCallback(int slotId, IImsServiceFeatureCallback cb) {
+ callback = cb;
+ }
+
+ @Override
+ public void unregisterFeatureCallback(IImsServiceFeatureCallback cb) {
+ callback = null;
+ }
+
+ @Override
+ public void associate(ImsFeatureContainer c) {
+ connection = new TestFeatureConnection(mContext, mPhoneId, c.imsConfig,
+ c.imsRegistration, c.sipTransport);
+ connection.setBinder(c.imsFeature);
+ }
+
+ @Override
+ public void invalidate() {
+ connection = null;
+ }
+
+ @Override
+ public void updateFeatureState(int state) {
+ assertNotNull(connection);
+ connection.updateFeatureState(state);
+ }
+
+ @Override
+ public void updateFeatureCapabilities(long capabilities) {
+ connection.updateFeatureCapabilities(capabilities);
+ }
+ }
+
+ private FeatureConnector<TestManager> mFeatureConnector;
+ private TestManager mTestManager;
+ @Mock private FeatureConnector.Listener<TestManager> mListener;
+ @Mock private IBinder feature;
+ @Mock private IImsRegistration reg;
+ @Mock private IImsConfig config;
+ @Mock private ISipTransport transport;
+
+ private static final int PHONE_ID = 1;
+ private static final long TEST_CAPS = ImsService.CAPABILITY_EMERGENCY_OVER_MMTEL;
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ setImsSupportedFeature(true);
+ mTestManager = new TestManager(mContext, PHONE_ID);
+ when(feature.isBinderAlive()).thenReturn(true);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ @Test
+ @SmallTest
+ public void testConnect() throws Exception {
+ createFeatureConnector();
+ mFeatureConnector.connect();
+ assertNotNull("connect should trigger the callback registration", mTestManager.callback);
+
+ // simulate callback from ImsResolver
+ mTestManager.callback.imsFeatureCreated(createContainer());
+ assertNotNull(mTestManager.connection);
+ assertEquals(TEST_CAPS, mTestManager.connection.getFeatureCapabilties());
+ verify(mListener, never()).connectionReady(any());
+ verify(mListener, never()).connectionUnavailable(anyInt());
+
+ // simulate callback from ImsResolver
+ mTestManager.callback.imsStatusChanged(ImsFeature.STATE_READY);
+ verify(mListener).connectionReady(mTestManager);
+ verify(mListener, never()).connectionUnavailable(anyInt());
+ }
+
+ @Test
+ @SmallTest
+ public void testConnectNotSupported() {
+ createFeatureConnector();
+ // set not supported
+ setImsSupportedFeature(false);
+
+ mFeatureConnector.connect();
+ assertNull("connect should not the callback registration if not supported",
+ mTestManager.callback);
+ verify(mListener).connectionUnavailable(
+ FeatureConnector.UNAVAILABLE_REASON_IMS_UNSUPPORTED);
+ }
+
+ @Test
+ @SmallTest
+ public void testConnectReadyNotReady() throws Exception {
+ createFeatureConnector();
+ mFeatureConnector.connect();
+ assertNotNull("connect should trigger the callback registration", mTestManager.callback);
+ // simulate callback from ImsResolver
+ mTestManager.callback.imsFeatureCreated(createContainer());
+ mTestManager.callback.imsStatusChanged(ImsFeature.STATE_READY);
+ verify(mListener).connectionReady(mTestManager);
+ verify(mListener, never()).connectionUnavailable(anyInt());
+
+ mTestManager.callback.imsStatusChanged(ImsFeature.STATE_UNAVAILABLE);
+ assertNotNull("When not ready, the callback should still be registered",
+ mTestManager.callback);
+ assertNotNull("Do not invalidate the connection if not ready", mTestManager.connection);
+ verify(mListener).connectionReady(mTestManager);
+ verify(mListener).connectionUnavailable(FeatureConnector.UNAVAILABLE_REASON_NOT_READY);
+ }
+
+ @Test
+ @SmallTest
+ public void testConnectReadyAndInitializing() throws Exception {
+ ArrayList<Integer> filterList = new ArrayList<>();
+ filterList.add(ImsFeature.STATE_READY);
+ filterList.add(ImsFeature.STATE_INITIALIZING);
+ createFeatureConnector(filterList);
+ mFeatureConnector.connect();
+ assertNotNull("connect should trigger the callback registration", mTestManager.callback);
+ // simulate callback from ImsResolver
+ mTestManager.callback.imsFeatureCreated(createContainer());
+ verify(mListener, never()).connectionReady(mTestManager);
+ verify(mListener, never()).connectionUnavailable(anyInt());
+
+ mTestManager.callback.imsStatusChanged(ImsFeature.STATE_INITIALIZING);
+ assertNotNull("When not ready, the callback should still be registered",
+ mTestManager.callback);
+ assertNotNull("Do not invalidate the connection if not ready", mTestManager.connection);
+ verify(mListener).connectionReady(mTestManager);
+ verify(mListener, never()).connectionUnavailable(anyInt());
+
+ mTestManager.callback.imsStatusChanged(ImsFeature.STATE_READY);
+ assertNotNull("When not ready, the callback should still be registered",
+ mTestManager.callback);
+ assertNotNull("Do not invalidate the connection if not ready", mTestManager.connection);
+ // Should not notify ready multiple times
+ verify(mListener).connectionReady(mTestManager);
+ verify(mListener, never()).connectionUnavailable(anyInt());
+ }
+
+ @Test
+ @SmallTest
+ public void testConnectReadyAndUnavailable() throws Exception {
+ ArrayList<Integer> filterList = new ArrayList<>();
+ filterList.add(ImsFeature.STATE_READY);
+ filterList.add(ImsFeature.STATE_INITIALIZING);
+ filterList.add(ImsFeature.STATE_UNAVAILABLE);
+ createFeatureConnector(filterList);
+ mFeatureConnector.connect();
+ assertNotNull("connect should trigger the callback registration", mTestManager.callback);
+ // simulate callback from ImsResolver
+ mTestManager.callback.imsFeatureCreated(createContainer());
+ verify(mListener).connectionReady(mTestManager);
+ verify(mListener, never()).connectionUnavailable(anyInt());
+
+ mTestManager.callback.imsStatusChanged(ImsFeature.STATE_UNAVAILABLE);
+ assertNotNull("When not ready, the callback should still be registered",
+ mTestManager.callback);
+ assertNotNull("Do not invalidate the connection if not ready", mTestManager.connection);
+ verify(mListener).connectionReady(mTestManager);
+ verify(mListener, never()).connectionUnavailable(anyInt());
+
+ mTestManager.callback.imsStatusChanged(ImsFeature.STATE_INITIALIZING);
+ assertNotNull("When not ready, the callback should still be registered",
+ mTestManager.callback);
+ assertNotNull("Do not invalidate the connection if not ready", mTestManager.connection);
+ // Should not notify ready multiple times
+ verify(mListener).connectionReady(mTestManager);
+ verify(mListener, never()).connectionUnavailable(anyInt());
+
+ mTestManager.callback.imsStatusChanged(ImsFeature.STATE_READY);
+ // Should not notify ready multiple times
+ verify(mListener).connectionReady(mTestManager);
+ verify(mListener, never()).connectionUnavailable(anyInt());
+ }
+
+ @Test
+ @SmallTest
+ public void testCantConnectToServer() throws Exception {
+ ArrayList<Integer> filterList = new ArrayList<>();
+ filterList.add(ImsFeature.STATE_READY);
+ filterList.add(ImsFeature.STATE_INITIALIZING);
+ filterList.add(ImsFeature.STATE_UNAVAILABLE);
+ createFeatureConnector(filterList);
+
+ mFeatureConnector.connect();
+ mTestManager.callback.imsFeatureRemoved(
+ FeatureConnector.UNAVAILABLE_REASON_SERVER_UNAVAILABLE);
+ verify(mListener).connectionUnavailable(
+ FeatureConnector.UNAVAILABLE_REASON_SERVER_UNAVAILABLE);
+
+ // Clear callback and ensure that the second connect tries to register a callback.
+ mTestManager.registerFeatureCallback(PHONE_ID, null);
+ mFeatureConnector.connect();
+ assertNotNull("The register request should happen the second time as well.",
+ mTestManager.callback);
+ mTestManager.callback.imsFeatureRemoved(
+ FeatureConnector.UNAVAILABLE_REASON_SERVER_UNAVAILABLE);
+ // In the special case that UNAVAILABLE_REASON_SERVER_UNAVAILABLE is returned, we should get
+ // an unavailable callback every time because it will require connect to be called again.
+ verify(mListener,times(2)).connectionUnavailable(
+ FeatureConnector.UNAVAILABLE_REASON_SERVER_UNAVAILABLE);
+ }
+
+ @Test
+ @SmallTest
+ public void testConnectReadyRemovedReady() throws Exception {
+ createFeatureConnector();
+ mFeatureConnector.connect();
+ assertNotNull("connect should trigger the callback registration", mTestManager.callback);
+ // simulate callback from ImsResolver
+ mTestManager.callback.imsFeatureCreated(createContainer());
+ mTestManager.callback.imsStatusChanged(ImsFeature.STATE_READY);
+ verify(mListener).connectionReady(mTestManager);
+ verify(mListener, never()).connectionUnavailable(anyInt());
+
+ mTestManager.callback.imsFeatureRemoved(FeatureConnector.UNAVAILABLE_REASON_DISCONNECTED);
+ assertNotNull("When not ready, the callback should still be registered",
+ mTestManager.callback);
+ verify(mListener).connectionReady(mTestManager);
+ verify(mListener).connectionUnavailable(FeatureConnector.UNAVAILABLE_REASON_DISCONNECTED);
+
+ mTestManager.callback.imsFeatureCreated(createContainer());
+ mTestManager.callback.imsStatusChanged(ImsFeature.STATE_READY);
+ verify(mListener, times(2)).connectionReady(mTestManager);
+ verify(mListener).connectionUnavailable(FeatureConnector.UNAVAILABLE_REASON_DISCONNECTED);
+ }
+
+ @Test
+ @SmallTest
+ public void testConnectDisconnect() throws Exception {
+ createFeatureConnector();
+ mFeatureConnector.connect();
+ assertNotNull("connect should trigger the callback registration", mTestManager.callback);
+ // simulate callback from ImsResolver
+ mTestManager.callback.imsFeatureCreated(createContainer());
+ mTestManager.callback.imsStatusChanged(ImsFeature.STATE_READY);
+ IImsServiceFeatureCallback oldCb = mTestManager.callback;
+ TestFeatureConnection testFc = mTestManager.connection;
+
+ mFeatureConnector.disconnect();
+ assertNull(mTestManager.callback);
+ assertNull(mTestManager.connection);
+ verify(mListener).connectionReady(mTestManager);
+ verify(mListener).connectionUnavailable(FeatureConnector.UNAVAILABLE_REASON_DISCONNECTED);
+
+ // make sure status/caps updates do not trigger more events after disconnect
+ oldCb.imsStatusChanged(ImsFeature.STATE_READY);
+ oldCb.imsStatusChanged(ImsFeature.STATE_UNAVAILABLE);
+ oldCb.updateCapabilities(0);
+ assertEquals(TEST_CAPS, testFc.getFeatureCapabilties());
+ verify(mListener).connectionReady(mTestManager);
+ verify(mListener).connectionUnavailable(FeatureConnector.UNAVAILABLE_REASON_DISCONNECTED);
+ }
+
+ @Test
+ @SmallTest
+ public void testConnectDisconnectConnect() throws Exception {
+ createFeatureConnector();
+ mFeatureConnector.connect();
+ assertNotNull("connect should trigger the callback registration", mTestManager.callback);
+ // simulate callback from ImsResolver
+ mTestManager.callback.imsFeatureCreated(createContainer());
+ mTestManager.callback.imsStatusChanged(ImsFeature.STATE_READY);
+
+ mFeatureConnector.disconnect();
+ assertNull(mTestManager.callback);
+ assertNull(mTestManager.connection);
+ verify(mListener).connectionReady(mTestManager);
+ verify(mListener).connectionUnavailable(FeatureConnector.UNAVAILABLE_REASON_DISCONNECTED);
+
+ mFeatureConnector.connect();
+ assertNotNull(mTestManager.callback);
+ // simulate callback from ImsResolver
+ mTestManager.callback.imsFeatureCreated(createContainer());
+ mTestManager.callback.imsStatusChanged(ImsFeature.STATE_READY);
+ assertNotNull(mTestManager.connection);
+ verify(mListener, times(2)).connectionReady(mTestManager);
+ verify(mListener).connectionUnavailable(FeatureConnector.UNAVAILABLE_REASON_DISCONNECTED);
+ }
+
+ @Test
+ @SmallTest
+ public void testUpdateCapabilities() throws Exception {
+ createFeatureConnector();
+ mFeatureConnector.connect();
+ assertNotNull("connect should trigger the callback registration", mTestManager.callback);
+ // simulate callback from ImsResolver
+ mTestManager.callback.imsFeatureCreated(createContainer());
+ mTestManager.callback.imsStatusChanged(ImsFeature.STATE_READY);
+ assertEquals(TEST_CAPS, mTestManager.connection.getFeatureCapabilties());
+ mTestManager.callback.updateCapabilities(0);
+ assertEquals(0, mTestManager.connection.getFeatureCapabilties());
+ }
+
+ @Test
+ @SmallTest
+ public void testUpdateStatus() throws Exception {
+ createFeatureConnector();
+ mFeatureConnector.connect();
+ assertNotNull("connect should trigger the callback registration", mTestManager.callback);
+ // simulate callback from ImsResolver
+ mTestManager.callback.imsFeatureCreated(createContainer());
+ mTestManager.callback.imsStatusChanged(ImsFeature.STATE_READY);
+ assertEquals(ImsFeature.STATE_READY, mTestManager.connection.getFeatureState());
+ }
+
+ private void setImsSupportedFeature(boolean isSupported) {
+ if(isSupported) {
+ mContextFixture.addSystemFeature(PackageManager.FEATURE_TELEPHONY_IMS);
+ } else {
+ mContextFixture.removeSystemFeature(PackageManager.FEATURE_TELEPHONY_IMS);
+ }
+ }
+
+ private ImsFeatureContainer createContainer() {
+ ImsFeatureContainer c = new ImsFeatureContainer(feature, config, reg, transport,
+ TEST_CAPS);
+ c.setState(ImsFeature.STATE_UNAVAILABLE);
+ return c;
+ }
+
+ private void createFeatureConnector() {
+ ArrayList<Integer> filter = new ArrayList<>();
+ filter.add(ImsFeature.STATE_READY);
+ createFeatureConnector(filter);
+ }
+
+ private void createFeatureConnector(List<Integer> featureReadyFilter) {
+ mFeatureConnector = new FeatureConnector<>(mContext, PHONE_ID,
+ (c, p) -> mTestManager, "Test", featureReadyFilter, mListener, Runnable::run);
+ }
+}
diff --git a/tests/src/com/android/ims/ImsConfigTest.java b/tests/src/com/android/ims/ImsConfigTest.java
index 63d14330..7ce26ddd 100644
--- a/tests/src/com/android/ims/ImsConfigTest.java
+++ b/tests/src/com/android/ims/ImsConfigTest.java
@@ -20,9 +20,9 @@ import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.verify;
import android.telephony.ims.aidl.IImsConfig;
-import android.test.suitebuilder.annotation.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
import org.junit.After;
import org.junit.Before;
diff --git a/tests/src/com/android/ims/ImsFeatureBinderRepositoryTest.java b/tests/src/com/android/ims/ImsFeatureBinderRepositoryTest.java
new file mode 100644
index 00000000..273d1dc8
--- /dev/null
+++ b/tests/src/com/android/ims/ImsFeatureBinderRepositoryTest.java
@@ -0,0 +1,363 @@
+/*
+ * 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.ims;
+
+import static org.junit.Assert.assertEquals;
+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.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.os.IBinder;
+import android.telephony.ims.ImsService;
+import android.telephony.ims.aidl.IImsConfig;
+import android.telephony.ims.aidl.IImsRegistration;
+import android.telephony.ims.aidl.ISipTransport;
+import android.telephony.ims.feature.ImsFeature;
+
+import androidx.test.filters.SmallTest;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.ims.internal.IImsServiceFeatureCallback;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+@RunWith(AndroidJUnit4.class)
+public class ImsFeatureBinderRepositoryTest extends ImsTestBase {
+
+ private static final int TEST_PHONE_ID_1 = 1;
+ private static final int TEST_PHONE_ID_2 = 2;
+ private static final long TEST_SERVICE_CAPS = ImsService.CAPABILITY_EMERGENCY_OVER_MMTEL;
+
+ @Mock IBinder mMockMmTelFeatureA;
+ @Mock IBinder mMockMmTelFeatureB;
+ @Mock IBinder mMockRcsFeatureA;
+ @Mock IImsConfig mMockImsConfig;
+ @Mock IImsRegistration mMockImsRegistration;
+ @Mock ISipTransport mMockSipTransport;
+
+ @Mock IImsServiceFeatureCallback mConnectionCallback;
+ @Mock IBinder mConnectionCallbackBinder;
+ @Mock IImsServiceFeatureCallback mConnectionCallback2;
+ @Mock IBinder mConnectionCallback2Binder;
+
+ private ImsFeatureBinderRepository mRepository;
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ mRepository = new ImsFeatureBinderRepository();
+ when(mConnectionCallbackBinder.isBinderAlive()).thenReturn(true);
+ when(mConnectionCallback2Binder.isBinderAlive()).thenReturn(true);
+ when(mConnectionCallback.asBinder()).thenReturn(mConnectionCallbackBinder);
+ when(mConnectionCallback2.asBinder()).thenReturn(mConnectionCallback2Binder);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ @Test
+ @SmallTest
+ public void testGetInterfaceExists() throws Exception {
+ ImsFeatureContainer fc =
+ getFeatureContainer(mMockMmTelFeatureA, TEST_SERVICE_CAPS);
+ mRepository.addConnection(TEST_PHONE_ID_1, ImsFeature.FEATURE_MMTEL, fc);
+ ImsFeatureContainer resultFc =
+ mRepository.getIfExists(TEST_PHONE_ID_1, ImsFeature.FEATURE_MMTEL).orElse(null);
+ assertNotNull("returned connection should not be null!", resultFc);
+ assertEquals("returned connection does not match the set connection",
+ fc, resultFc);
+ }
+
+ @Test
+ @SmallTest
+ public void testGetInterfaceDoesntExist() throws Exception {
+ ImsFeatureContainer fc =
+ mRepository.getIfExists(TEST_PHONE_ID_1,
+ ImsFeature.FEATURE_MMTEL).orElse(null);
+ assertNull("returned connection should be null!", fc);
+ }
+
+ @Test
+ @SmallTest
+ public void testGetInterfaceRemoveDoesntExist() throws Exception {
+ ImsFeatureContainer fc =
+ getFeatureContainer(mMockMmTelFeatureA, TEST_SERVICE_CAPS);
+ mRepository.addConnection(TEST_PHONE_ID_1, ImsFeature.FEATURE_MMTEL, fc);
+ mRepository.addConnection(TEST_PHONE_ID_1, ImsFeature.FEATURE_MMTEL, null);
+ ImsFeatureContainer resultFc =
+ mRepository.getIfExists(TEST_PHONE_ID_1,
+ ImsFeature.FEATURE_MMTEL).orElse(null);
+ assertNull("returned connection should be null!", resultFc);
+ }
+
+ @Test
+ @SmallTest
+ public void testGetInterfaceUpdateExists() throws Exception {
+ ImsFeatureContainer fcA =
+ getFeatureContainer(mMockMmTelFeatureA, TEST_SERVICE_CAPS);
+ ImsFeatureContainer fcB =
+ getFeatureContainer(mMockMmTelFeatureA, TEST_SERVICE_CAPS);
+ mRepository.addConnection(TEST_PHONE_ID_1, ImsFeature.FEATURE_MMTEL, fcA);
+ mRepository.addConnection(TEST_PHONE_ID_1, ImsFeature.FEATURE_MMTEL, fcB);
+ ImsFeatureContainer resultFc =
+ mRepository.getIfExists(TEST_PHONE_ID_1,
+ ImsFeature.FEATURE_MMTEL).orElse(null);
+ assertNotNull("returned connection should not be null!", resultFc);
+ assertEquals("returned connection does not match the set connection",
+ fcB, resultFc);
+ }
+
+ @Test
+ @SmallTest
+ public void testGetMultipleInterfacesExists() throws Exception {
+ ImsFeatureContainer fcA =
+ getFeatureContainer(mMockMmTelFeatureA, TEST_SERVICE_CAPS);
+ ImsFeatureContainer fcB =
+ getFeatureContainer(mMockRcsFeatureA, TEST_SERVICE_CAPS);
+ mRepository.addConnection(TEST_PHONE_ID_1, ImsFeature.FEATURE_MMTEL, fcA);
+ mRepository.addConnection(TEST_PHONE_ID_1, ImsFeature.FEATURE_RCS, fcB);
+ ImsFeatureContainer resultFcA =
+ mRepository.getIfExists(TEST_PHONE_ID_1,
+ ImsFeature.FEATURE_MMTEL).orElse(null);
+ ImsFeatureContainer resultFcB =
+ mRepository.getIfExists(TEST_PHONE_ID_1,
+ ImsFeature.FEATURE_RCS).orElse(null);
+ assertNotNull("returned connection should not be null!", resultFcA);
+ assertNotNull("returned connection should not be null!", resultFcB);
+ assertEquals("returned connection does not match the set connection",
+ fcA, resultFcA);
+ assertEquals("returned connection does not match the set connection",
+ fcB, resultFcB);
+ }
+
+ @Test
+ @SmallTest
+ public void testListenForUpdate() throws Exception {
+ ImsFeatureContainer fcA =
+ getFeatureContainer(mMockMmTelFeatureA, TEST_SERVICE_CAPS);
+ mRepository.registerForConnectionUpdates(TEST_PHONE_ID_1, ImsFeature.FEATURE_MMTEL,
+ mConnectionCallback, Runnable::run);
+ verify(mConnectionCallback, never()).imsFeatureRemoved(anyInt());
+ mRepository.addConnection(TEST_PHONE_ID_1, ImsFeature.FEATURE_MMTEL, fcA);
+ verifyFeatureCreatedCalled(1 /*times*/, mConnectionCallback, fcA);
+ verify(mConnectionCallback, never()).imsFeatureRemoved(anyInt());
+ }
+
+ @Test
+ @SmallTest
+ public void testListenNoUpdateForStaleListener() throws Exception {
+ ImsFeatureContainer fcA =
+ getFeatureContainer(mMockMmTelFeatureA, TEST_SERVICE_CAPS);
+ mRepository.registerForConnectionUpdates(TEST_PHONE_ID_1, ImsFeature.FEATURE_MMTEL,
+ mConnectionCallback, Runnable::run);
+ mRepository.addConnection(TEST_PHONE_ID_1, ImsFeature.FEATURE_MMTEL, fcA);
+ verifyFeatureCreatedCalled(1 /*times*/, mConnectionCallback, fcA);
+ verify(mConnectionCallback, never()).imsFeatureRemoved(anyInt());
+
+ when(mConnectionCallbackBinder.isBinderAlive()).thenReturn(false);
+ // Listener is "dead", so we should not get this update
+ mRepository.notifyFeatureStateChanged(TEST_PHONE_ID_1, ImsFeature.FEATURE_MMTEL,
+ ImsFeature.STATE_READY);
+ verify(mConnectionCallback, never()).imsStatusChanged(ImsFeature.STATE_READY);
+ }
+
+ @Test
+ @SmallTest
+ public void testListenForUpdateStateChanged() throws Exception {
+ ImsFeatureContainer fcA =
+ getFeatureContainer(mMockMmTelFeatureA, TEST_SERVICE_CAPS);
+ mRepository.registerForConnectionUpdates(TEST_PHONE_ID_1, ImsFeature.FEATURE_MMTEL,
+ mConnectionCallback, Runnable::run);
+ mRepository.addConnection(TEST_PHONE_ID_1, ImsFeature.FEATURE_MMTEL, fcA);
+ ImsFeatureContainer resultFc =
+ mRepository.getIfExists(TEST_PHONE_ID_1, ImsFeature.FEATURE_MMTEL).orElse(null);
+ assertNotNull(resultFc);
+ assertEquals(ImsFeature.STATE_UNAVAILABLE, resultFc.getState());
+ verifyFeatureCreatedCalled(1 /*times*/, mConnectionCallback, fcA);
+ verify(mConnectionCallback, never()).imsFeatureRemoved(anyInt());
+ verify(mConnectionCallback, never()).imsStatusChanged(anyInt());
+
+ mRepository.notifyFeatureStateChanged(TEST_PHONE_ID_1, ImsFeature.FEATURE_MMTEL,
+ ImsFeature.STATE_READY);
+ verify(mConnectionCallback).imsStatusChanged(ImsFeature.STATE_READY);
+ assertEquals(ImsFeature.STATE_READY, resultFc.getState());
+ }
+
+ @Test
+ @SmallTest
+ public void testListenForUpdateCapsChanged() throws Exception {
+ ImsFeatureContainer fcA =
+ getFeatureContainer(mMockMmTelFeatureA, TEST_SERVICE_CAPS);
+ mRepository.registerForConnectionUpdates(TEST_PHONE_ID_1, ImsFeature.FEATURE_MMTEL,
+ mConnectionCallback, Runnable::run);
+
+ mRepository.addConnection(TEST_PHONE_ID_1, ImsFeature.FEATURE_MMTEL, fcA);
+ ImsFeatureContainer resultFc =
+ mRepository.getIfExists(TEST_PHONE_ID_1, ImsFeature.FEATURE_MMTEL).orElse(null);
+ assertNotNull(resultFc);
+ assertEquals(TEST_SERVICE_CAPS, resultFc.getCapabilities());
+ verify(mConnectionCallback, never()).imsFeatureRemoved(anyInt());
+
+ mRepository.notifyFeatureCapabilitiesChanged(TEST_PHONE_ID_1, ImsFeature.FEATURE_MMTEL, 0);
+ verify(mConnectionCallback).updateCapabilities(0);
+ assertEquals(0, resultFc.getCapabilities());
+ }
+
+
+ @Test
+ @SmallTest
+ public void testRemoveCallback() throws Exception {
+ ImsFeatureContainer fcA =
+ getFeatureContainer(mMockMmTelFeatureA, TEST_SERVICE_CAPS);
+ ImsFeatureContainer fcB =
+ getFeatureContainer(mMockMmTelFeatureB, TEST_SERVICE_CAPS);
+ mRepository.registerForConnectionUpdates(TEST_PHONE_ID_1, ImsFeature.FEATURE_MMTEL,
+ mConnectionCallback, Runnable::run);
+ mRepository.addConnection(TEST_PHONE_ID_1, ImsFeature.FEATURE_MMTEL, fcA);
+ mRepository.unregisterForConnectionUpdates(mConnectionCallback);
+
+ mRepository.addConnection(TEST_PHONE_ID_1, ImsFeature.FEATURE_MMTEL, fcB);
+ verifyFeatureCreatedCalled(1 /*times*/, mConnectionCallback, fcA);
+ verifyFeatureCreatedCalled(0 /*times*/, mConnectionCallback, fcB);
+ verify(mConnectionCallback, never()).imsFeatureRemoved(anyInt());
+ }
+
+ @Test
+ @SmallTest
+ public void testAddSameCallback() throws Exception {
+ ImsFeatureContainer fcA =
+ getFeatureContainer(mMockMmTelFeatureA, TEST_SERVICE_CAPS);
+ mRepository.registerForConnectionUpdates(TEST_PHONE_ID_1, ImsFeature.FEATURE_MMTEL,
+ mConnectionCallback, Runnable::run);
+ mRepository.registerForConnectionUpdates(TEST_PHONE_ID_1, ImsFeature.FEATURE_MMTEL,
+ mConnectionCallback, Runnable::run);
+ mRepository.addConnection(TEST_PHONE_ID_1, ImsFeature.FEATURE_MMTEL, fcA);
+ verifyFeatureCreatedCalled(1 /*times*/, mConnectionCallback, fcA);
+ verify(mConnectionCallback, never()).imsFeatureRemoved(anyInt());
+ }
+
+ @Test
+ @SmallTest
+ public void testListenAfterUpdate() throws Exception {
+ ImsFeatureContainer fcA =
+ getFeatureContainer(mMockMmTelFeatureA, TEST_SERVICE_CAPS);
+ mRepository.addConnection(TEST_PHONE_ID_1, ImsFeature.FEATURE_MMTEL, fcA);
+ mRepository.registerForConnectionUpdates(TEST_PHONE_ID_1, ImsFeature.FEATURE_MMTEL,
+ mConnectionCallback, Runnable::run);
+ verifyFeatureCreatedCalled(1 /*times*/, mConnectionCallback, fcA);
+ verify(mConnectionCallback, never()).imsFeatureRemoved(anyInt());
+ }
+
+ @Test
+ @SmallTest
+ public void testListenNoUpdate() throws Exception {
+ mRepository.registerForConnectionUpdates(TEST_PHONE_ID_1, ImsFeature.FEATURE_MMTEL,
+ mConnectionCallback, Runnable::run);
+ verify(mConnectionCallback, never()).imsFeatureCreated(any());
+ verify(mConnectionCallback, never()).imsFeatureRemoved(anyInt());
+ }
+
+ @Test
+ @SmallTest
+ public void testListenNull() throws Exception {
+ ImsFeatureContainer fcA =
+ getFeatureContainer(mMockMmTelFeatureA, TEST_SERVICE_CAPS);
+ mRepository.addConnection(TEST_PHONE_ID_1, ImsFeature.FEATURE_MMTEL, fcA);
+ mRepository.registerForConnectionUpdates(TEST_PHONE_ID_1, ImsFeature.FEATURE_MMTEL,
+ mConnectionCallback, Runnable::run);
+ mRepository.removeConnection(TEST_PHONE_ID_1, ImsFeature.FEATURE_MMTEL);
+ verifyFeatureCreatedCalled(1 /*times*/, mConnectionCallback, fcA);
+ verify(mConnectionCallback).imsFeatureRemoved(
+ FeatureConnector.UNAVAILABLE_REASON_DISCONNECTED);
+ }
+
+ @Test
+ @SmallTest
+ public void testMultipleListeners() throws Exception {
+ ImsFeatureContainer fcA =
+ getFeatureContainer(mMockMmTelFeatureA, TEST_SERVICE_CAPS);
+ ImsFeatureContainer fcB =
+ getFeatureContainer(mMockRcsFeatureA, TEST_SERVICE_CAPS);
+ mRepository.addConnection(TEST_PHONE_ID_1, ImsFeature.FEATURE_MMTEL, fcA);
+ mRepository.registerForConnectionUpdates(TEST_PHONE_ID_1, ImsFeature.FEATURE_MMTEL,
+ mConnectionCallback, Runnable::run);
+ verifyFeatureCreatedCalled(1 /*times*/, mConnectionCallback, fcA);
+ verify(mConnectionCallback, never()).imsFeatureRemoved(anyInt());
+
+ mRepository.registerForConnectionUpdates(TEST_PHONE_ID_1, ImsFeature.FEATURE_RCS,
+ mConnectionCallback2, Runnable::run);
+ verifyFeatureCreatedCalled(1 /*times*/, mConnectionCallback, fcA);
+ verify(mConnectionCallback, never()).imsFeatureRemoved(anyInt());
+ verify(mConnectionCallback2, never()).imsFeatureCreated(any());
+ verify(mConnectionCallback2, never()).imsFeatureRemoved(anyInt());
+
+ mRepository.addConnection(TEST_PHONE_ID_1, ImsFeature.FEATURE_RCS, fcB);
+ verifyFeatureCreatedCalled(1 /*times*/, mConnectionCallback, fcA);
+ verify(mConnectionCallback, never()).imsFeatureRemoved(anyInt());
+ verifyFeatureCreatedCalled(1 /*times*/, mConnectionCallback2, fcB);
+ verify(mConnectionCallback2, never()).imsFeatureRemoved(anyInt());
+ }
+
+ @Test
+ @SmallTest
+ public void testMultiplePhones() throws Exception {
+ ImsFeatureContainer fcA =
+ getFeatureContainer(mMockMmTelFeatureA, TEST_SERVICE_CAPS);
+ ImsFeatureContainer fcB =
+ getFeatureContainer(mMockRcsFeatureA, TEST_SERVICE_CAPS);
+ mRepository.addConnection(TEST_PHONE_ID_1, ImsFeature.FEATURE_MMTEL, fcA);
+ mRepository.registerForConnectionUpdates(TEST_PHONE_ID_1, ImsFeature.FEATURE_MMTEL,
+ mConnectionCallback, Runnable::run);
+ verifyFeatureCreatedCalled(1 /*times*/, mConnectionCallback, fcA);
+ verify(mConnectionCallback, never()).imsFeatureRemoved(anyInt());
+
+ mRepository.registerForConnectionUpdates(TEST_PHONE_ID_2, ImsFeature.FEATURE_RCS,
+ mConnectionCallback2, Runnable::run);
+ verifyFeatureCreatedCalled(1 /*times*/, mConnectionCallback, fcA);
+ verify(mConnectionCallback, never()).imsFeatureRemoved(anyInt());
+ verify(mConnectionCallback2, never()).imsFeatureCreated(any());
+ verify(mConnectionCallback2, never()).imsFeatureRemoved(anyInt());
+
+ mRepository.addConnection(TEST_PHONE_ID_2, ImsFeature.FEATURE_RCS, fcB);
+ verifyFeatureCreatedCalled(1 /*times*/, mConnectionCallback, fcA);
+ verify(mConnectionCallback, never()).imsFeatureRemoved(anyInt());
+ verifyFeatureCreatedCalled(1 /*times*/, mConnectionCallback2, fcB);
+ verify(mConnectionCallback2, never()).imsFeatureRemoved(anyInt());
+ }
+
+ private void verifyFeatureCreatedCalled(int timesCalled, IImsServiceFeatureCallback cb,
+ ImsFeatureContainer fc) throws Exception {
+ verify(cb, times(timesCalled)).imsFeatureCreated(fc);
+ }
+
+ private ImsFeatureContainer getFeatureContainer(IBinder feature, long caps) {
+ return new ImsFeatureContainer(feature, mMockImsConfig,
+ mMockImsRegistration, mMockSipTransport, caps);
+ }
+}
diff --git a/tests/src/com/android/ims/ImsFeatureContainerTest.java b/tests/src/com/android/ims/ImsFeatureContainerTest.java
new file mode 100644
index 00000000..e6a59975
--- /dev/null
+++ b/tests/src/com/android/ims/ImsFeatureContainerTest.java
@@ -0,0 +1,87 @@
+/*
+ * 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.ims;
+
+import static org.junit.Assert.assertEquals;
+
+import android.os.Parcel;
+import android.os.RemoteException;
+import android.telephony.ims.DelegateRequest;
+import android.telephony.ims.ImsService;
+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.feature.ImsFeature;
+import android.telephony.ims.feature.MmTelFeature;
+import android.telephony.ims.stub.ImsConfigImplBase;
+import android.telephony.ims.stub.ImsRegistrationImplBase;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class ImsFeatureContainerTest {
+
+ // Use real objects here as I'm not sure how mock IBinders/IInterfaces would parcel.
+ private MmTelFeature mMmTelFeature = new MmTelFeature();
+ private ImsConfigImplBase mImsConfig = new ImsConfigImplBase();
+ private ImsRegistrationImplBase mImsReg = new ImsRegistrationImplBase();
+ private ISipTransport mSipTransport = new ISipTransport.Stub() {
+ @Override
+ public void createSipDelegate(int subId, DelegateRequest request,
+ ISipDelegateStateCallback dc, ISipDelegateMessageCallback mc) {
+ }
+
+ @Override
+ public void destroySipDelegate(ISipDelegate delegate, int reason) {
+ }
+ };
+
+ @Test
+ @SmallTest
+ public void testParcelUnparcel() throws Exception {
+ final int state = ImsFeature.STATE_READY;
+ final long caps = ImsService.CAPABILITY_EMERGENCY_OVER_MMTEL;
+ ImsFeatureContainer c = new ImsFeatureContainer(mMmTelFeature.getBinder().asBinder(),
+ mImsConfig.getIImsConfig(), mImsReg.getBinder(), mSipTransport, caps);
+ c.setState(state);
+
+ ImsFeatureContainer result = parcelUnparcel(c);
+
+ assertEquals(mMmTelFeature.getBinder().asBinder(), result.imsFeature);
+ assertEquals(mImsConfig.getIImsConfig(), result.imsConfig);
+ assertEquals(mImsReg.getBinder(), result.imsRegistration);
+ assertEquals(state, result.getState());
+ assertEquals(caps, result.getCapabilities());
+
+ assertEquals(c, result);
+ }
+
+ public ImsFeatureContainer parcelUnparcel(ImsFeatureContainer data) {
+ Parcel parcel = Parcel.obtain();
+ data.writeToParcel(parcel, 0);
+ parcel.setDataPosition(0);
+ ImsFeatureContainer unparceledData =
+ ImsFeatureContainer.CREATOR.createFromParcel(parcel);
+ parcel.recycle();
+ return unparceledData;
+ }
+}
diff --git a/tests/src/com/android/ims/ImsManagerTest.java b/tests/src/com/android/ims/ImsManagerTest.java
new file mode 100644
index 00000000..3db80259
--- /dev/null
+++ b/tests/src/com/android/ims/ImsManagerTest.java
@@ -0,0 +1,907 @@
+/*
+ * 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.ims;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.anyInt;
+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.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.os.IBinder;
+import android.os.PersistableBundle;
+import android.telephony.CarrierConfigManager;
+import android.telephony.SubscriptionManager;
+import android.telephony.ims.ImsMmTelManager;
+import android.telephony.ims.ProvisioningManager;
+import android.telephony.ims.aidl.IImsConfig;
+import android.telephony.ims.aidl.IImsRegistration;
+import android.telephony.ims.aidl.ISipTransport;
+import android.telephony.ims.feature.MmTelFeature;
+import android.telephony.ims.stub.ImsConfigImplBase;
+import android.telephony.ims.stub.ImsRegistrationImplBase;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.internal.os.SomeArgs;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+import java.util.Hashtable;
+
+@RunWith(AndroidJUnit4.class)
+public class ImsManagerTest extends ImsTestBase {
+ private static final boolean ENHANCED_4G_MODE_DEFAULT_VAL = true;
+ private static final boolean ENHANCED_4G_MODE_EDITABLE = true;
+ private static final boolean WFC_IMS_ENABLE_DEFAULT_VAL = false;
+ private static final boolean WFC_IMS_ROAMING_ENABLE_DEFAULT_VAL = true;
+ private static final boolean VT_IMS_ENABLE_DEFAULT_VAL = true;
+ private static final boolean WFC_IMS_EDITABLE_VAL = true;
+ private static final boolean WFC_IMS_NOT_EDITABLE_VAL = false;
+ private static final boolean WFC_IMS_ROAMING_EDITABLE_VAL = true;
+ private static final boolean WFC_IMS_ROAMING_NOT_EDITABLE_VAL = false;
+ private static final int WFC_IMS_MODE_DEFAULT_VAL =
+ ImsConfig.WfcModeFeatureValueConstants.CELLULAR_PREFERRED;
+ private static final int WFC_IMS_ROAMING_MODE_DEFAULT_VAL =
+ ImsConfig.WfcModeFeatureValueConstants.WIFI_PREFERRED;
+ private static final boolean WFC_USE_HOME_MODE_FOR_ROAMING_VAL = true;
+ private static final boolean WFC_NOT_USE_HOME_MODE_FOR_ROAMING_VAL = false;
+
+ PersistableBundle mBundle;
+ @Mock ImsConfigImplBase mImsConfigImplBaseMock;
+ Hashtable<Integer, Integer> mProvisionedIntVals = new Hashtable<>();
+ ImsConfigImplBase.ImsConfigStub mImsConfigStub;
+ @Mock MmTelFeatureConnection mMmTelFeatureConnection;
+ @Mock IBinder mMmTelFeature;
+ @Mock IImsConfig mImsConfig;
+ @Mock IImsRegistration mImsReg;
+ @Mock ISipTransport mSipTransport;
+ @Mock ImsManager.SubscriptionManagerProxy mSubscriptionManagerProxy;
+ @Mock ImsManager.SettingsProxy mSettingsProxy;
+
+ private final int[] mSubId = {0};
+ private final int mPhoneId = 1;
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ mBundle = mContextFixture.getTestCarrierConfigBundle();
+ // Force MmTelFeatureConnection to create an executor using Looper.myLooper().
+ doReturn(null).when(mContext).getMainLooper();
+
+ doReturn(true).when(mMmTelFeatureConnection).isBinderAlive();
+ mContextFixture.addSystemFeature(PackageManager.FEATURE_TELEPHONY_IMS);
+
+ doReturn(true).when(mSubscriptionManagerProxy).isValidSubscriptionId(anyInt());
+ doReturn(mSubId).when(mSubscriptionManagerProxy).getSubscriptionIds(eq(mPhoneId));
+ doReturn(mSubId).when(mSubscriptionManagerProxy).getActiveSubscriptionIdList();
+ doReturn(mPhoneId).when(mSubscriptionManagerProxy).getDefaultVoicePhoneId();
+ doReturn(-1).when(mSubscriptionManagerProxy).getIntegerSubscriptionProperty(anyInt(),
+ anyString(), anyInt());
+
+
+ setDefaultValues();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ private void setDefaultValues() {
+ mBundle.putBoolean(CarrierConfigManager.KEY_EDITABLE_ENHANCED_4G_LTE_BOOL,
+ ENHANCED_4G_MODE_EDITABLE);
+ mBundle.putBoolean(CarrierConfigManager.KEY_EDITABLE_WFC_MODE_BOOL,
+ WFC_IMS_EDITABLE_VAL);
+ mBundle.putBoolean(CarrierConfigManager.KEY_EDITABLE_WFC_ROAMING_MODE_BOOL,
+ WFC_IMS_ROAMING_EDITABLE_VAL);
+ mBundle.putBoolean(CarrierConfigManager.KEY_CARRIER_DEFAULT_WFC_IMS_ENABLED_BOOL,
+ WFC_IMS_ENABLE_DEFAULT_VAL);
+ mBundle.putBoolean(CarrierConfigManager.KEY_CARRIER_DEFAULT_WFC_IMS_ROAMING_ENABLED_BOOL,
+ WFC_IMS_ROAMING_ENABLE_DEFAULT_VAL);
+ mBundle.putInt(CarrierConfigManager.KEY_CARRIER_DEFAULT_WFC_IMS_MODE_INT,
+ WFC_IMS_MODE_DEFAULT_VAL);
+ mBundle.putInt(CarrierConfigManager.KEY_CARRIER_DEFAULT_WFC_IMS_ROAMING_MODE_INT,
+ WFC_IMS_ROAMING_MODE_DEFAULT_VAL);
+ mBundle.putBoolean(CarrierConfigManager.KEY_ENHANCED_4G_LTE_ON_BY_DEFAULT_BOOL,
+ ENHANCED_4G_MODE_DEFAULT_VAL);
+ mBundle.putBoolean(CarrierConfigManager.KEY_CARRIER_VOLTE_PROVISIONING_REQUIRED_BOOL, true);
+ mBundle.putBoolean(
+ CarrierConfigManager.KEY_USE_WFC_HOME_NETWORK_MODE_IN_ROAMING_NETWORK_BOOL,
+ WFC_NOT_USE_HOME_MODE_FOR_ROAMING_VAL);
+ mBundle.putBoolean(CarrierConfigManager.KEY_CARRIER_RCS_PROVISIONING_REQUIRED_BOOL, true);
+ mBundle.putBoolean(CarrierConfigManager.KEY_CARRIER_WFC_IMS_AVAILABLE_BOOL, true);
+ mBundle.putBoolean(CarrierConfigManager.KEY_CARRIER_IMS_GBA_REQUIRED_BOOL, false);
+
+ }
+
+ @Test @SmallTest
+ public void testGetDefaultValues() {
+ ImsManager imsManager = getImsManagerAndInitProvisionedValues();
+
+ assertEquals(WFC_IMS_ENABLE_DEFAULT_VAL, imsManager.isWfcEnabledByUser());
+ verify(mSubscriptionManagerProxy, times(1)).getIntegerSubscriptionProperty(
+ anyInt(),
+ eq(SubscriptionManager.WFC_IMS_ENABLED),
+ anyInt());
+
+ assertEquals(WFC_IMS_ROAMING_ENABLE_DEFAULT_VAL, imsManager.isWfcRoamingEnabledByUser());
+ verify(mSubscriptionManagerProxy, times(1)).getIntegerSubscriptionProperty(
+ anyInt(),
+ eq(SubscriptionManager.WFC_IMS_ROAMING_ENABLED),
+ anyInt());
+
+ assertEquals(ENHANCED_4G_MODE_DEFAULT_VAL,
+ imsManager.isEnhanced4gLteModeSettingEnabledByUser());
+ verify(mSubscriptionManagerProxy, times(1)).getIntegerSubscriptionProperty(
+ anyInt(),
+ eq(SubscriptionManager.ENHANCED_4G_MODE_ENABLED),
+ anyInt());
+
+ assertEquals(WFC_IMS_MODE_DEFAULT_VAL, imsManager.getWfcMode(false));
+ verify(mSubscriptionManagerProxy, times(1)).getIntegerSubscriptionProperty(
+ anyInt(),
+ eq(SubscriptionManager.WFC_IMS_MODE),
+ anyInt());
+
+ assertEquals(WFC_IMS_ROAMING_MODE_DEFAULT_VAL, imsManager.getWfcMode(true));
+ verify(mSubscriptionManagerProxy, times(1)).getIntegerSubscriptionProperty(
+ anyInt(),
+ eq(SubscriptionManager.WFC_IMS_ROAMING_MODE),
+ anyInt());
+
+ assertEquals(VT_IMS_ENABLE_DEFAULT_VAL, imsManager.isVtEnabledByUser());
+ verify(mSubscriptionManagerProxy, times(1)).getIntegerSubscriptionProperty(
+ anyInt(),
+ eq(SubscriptionManager.VT_IMS_ENABLED),
+ anyInt());
+ }
+
+ @SmallTest
+ @Test
+ public void testImsStats() {
+ setWfcEnabledByUser(true);
+ SomeArgs args = SomeArgs.obtain();
+ ImsManager.setImsStatsCallback(mPhoneId, new ImsManager.ImsStatsCallback() {
+ @Override
+ public void onEnabledMmTelCapabilitiesChanged(int capability, int regTech,
+ boolean isEnabled) {
+ args.arg1 = capability;
+ args.arg2 = regTech;
+ args.arg3 = isEnabled;
+ }
+ });
+ mBundle.putBoolean(CarrierConfigManager.KEY_CARRIER_VOLTE_PROVISIONING_REQUIRED_BOOL,
+ false);
+ ImsManager imsManager = getImsManagerAndInitProvisionedValues();
+ // Assert that the IMS stats callback is called properly when a setting changes.
+ imsManager.setWfcSetting(true);
+ assertEquals(args.arg1, MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VOICE);
+ assertEquals(args.arg2, ImsRegistrationImplBase.REGISTRATION_TECH_IWLAN);
+ assertEquals(args.arg3, true);
+ args.recycle();
+ }
+
+ @Test @SmallTest
+ public void testSetValues() {
+ setWfcEnabledByUser(true);
+ ImsManager imsManager = getImsManagerAndInitProvisionedValues();
+
+ imsManager.setWfcMode(ImsConfig.WfcModeFeatureValueConstants.CELLULAR_PREFERRED);
+ verify(mSubscriptionManagerProxy, times(1)).setSubscriptionProperty(
+ eq(mSubId[0]),
+ eq(SubscriptionManager.WFC_IMS_MODE),
+ eq("1"));
+
+ imsManager.setWfcMode(ImsConfig.WfcModeFeatureValueConstants.CELLULAR_PREFERRED, true);
+ verify(mSubscriptionManagerProxy, times(1)).setSubscriptionProperty(
+ eq(mSubId[0]),
+ eq(SubscriptionManager.WFC_IMS_ROAMING_MODE),
+ eq("1"));
+
+ imsManager.setVtSetting(false);
+ verify(mSubscriptionManagerProxy, times(1)).setSubscriptionProperty(
+ eq(mSubId[0]),
+ eq(SubscriptionManager.VT_IMS_ENABLED),
+ eq("0"));
+
+ // enhanced 4g mode must be editable to use setEnhanced4gLteModeSetting
+ mBundle.putBoolean(CarrierConfigManager.KEY_EDITABLE_ENHANCED_4G_LTE_BOOL,
+ ENHANCED_4G_MODE_EDITABLE);
+ imsManager.setEnhanced4gLteModeSetting(true);
+ verify(mSubscriptionManagerProxy, times(1)).setSubscriptionProperty(
+ eq(mSubId[0]),
+ eq(SubscriptionManager.ENHANCED_4G_MODE_ENABLED),
+ eq("1"));
+
+ imsManager.setWfcSetting(true);
+ verify(mSubscriptionManagerProxy, times(1)).setSubscriptionProperty(
+ eq(mSubId[0]),
+ eq(SubscriptionManager.WFC_IMS_ENABLED),
+ eq("1"));
+ }
+ @Test
+ public void testGetProvisionedValues() throws Exception {
+ ImsManager imsManager = getImsManagerAndInitProvisionedValues();
+
+ assertEquals(true, imsManager.isWfcProvisionedOnDevice());
+ verify(mImsConfigImplBaseMock, times(1)).getConfigInt(
+ eq(ImsConfig.ConfigConstants.VOICE_OVER_WIFI_SETTING_ENABLED));
+
+ assertEquals(true, imsManager.isVtProvisionedOnDevice());
+ verify(mImsConfigImplBaseMock, times(1)).getConfigInt(
+ eq(ImsConfig.ConfigConstants.LVC_SETTING_ENABLED));
+
+ assertEquals(true, imsManager.isVolteProvisionedOnDevice());
+ verify(mImsConfigImplBaseMock, times(1)).getConfigInt(
+ eq(ImsConfig.ConfigConstants.VLT_SETTING_ENABLED));
+
+ // If we call get again, times should still be one because the value should be fetched
+ // from cache.
+ assertEquals(true, imsManager.isWfcProvisionedOnDevice());
+ verify(mImsConfigImplBaseMock, times(1)).getConfigInt(
+ eq(ImsConfig.ConfigConstants.VOICE_OVER_WIFI_SETTING_ENABLED));
+
+ assertEquals(true, imsManager.isVtProvisionedOnDevice());
+ verify(mImsConfigImplBaseMock, times(1)).getConfigInt(
+ eq(ImsConfig.ConfigConstants.LVC_SETTING_ENABLED));
+
+ assertEquals(true, imsManager.isVolteProvisionedOnDevice());
+ verify(mImsConfigImplBaseMock, times(1)).getConfigInt(
+ eq(ImsConfig.ConfigConstants.VLT_SETTING_ENABLED));
+
+ assertEquals(true, imsManager.isEabProvisionedOnDevice());
+ verify(mImsConfigImplBaseMock, times(1)).getConfigInt(
+ eq(ImsConfig.ConfigConstants.EAB_SETTING_ENABLED));
+ }
+
+ @Test
+ public void testSetProvisionedValues() throws Exception {
+ ImsManager imsManager = getImsManagerAndInitProvisionedValues();
+
+ assertEquals(true, imsManager.isWfcProvisionedOnDevice());
+ verify(mImsConfigImplBaseMock, times(1)).getConfigInt(
+ eq(ImsConfig.ConfigConstants.VOICE_OVER_WIFI_SETTING_ENABLED));
+
+ imsManager.getConfigInterface().setProvisionedValue(
+ ImsConfig.ConfigConstants.VOICE_OVER_WIFI_SETTING_ENABLED,
+ ImsConfig.FeatureValueConstants.OFF);
+
+ assertEquals(0, (int) mProvisionedIntVals.get(
+ ImsConfig.ConfigConstants.VOICE_OVER_WIFI_SETTING_ENABLED));
+
+ assertEquals(false, imsManager.isWfcProvisionedOnDevice());
+
+ verify(mImsConfigImplBaseMock, times(1)).setConfig(
+ eq(ImsConfig.ConfigConstants.VOICE_OVER_WIFI_SETTING_ENABLED),
+ eq(0));
+ verify(mImsConfigImplBaseMock, times(1)).getConfigInt(
+ eq(ImsConfig.ConfigConstants.VOICE_OVER_WIFI_SETTING_ENABLED));
+ }
+
+ @Test
+ public void testEabSetProvisionedValues() throws Exception {
+ ImsManager imsManager = getImsManagerAndInitProvisionedValues();
+
+ assertEquals(true, imsManager.isEabProvisionedOnDevice());
+ verify(mImsConfigImplBaseMock, times(1)).getConfigInt(
+ eq(ImsConfig.ConfigConstants.EAB_SETTING_ENABLED));
+
+ imsManager.getConfigInterface().setProvisionedValue(
+ ImsConfig.ConfigConstants.EAB_SETTING_ENABLED,
+ ImsConfig.FeatureValueConstants.OFF);
+
+ assertEquals(0, (int) mProvisionedIntVals.get(
+ ImsConfig.ConfigConstants.EAB_SETTING_ENABLED));
+
+ assertEquals(false, imsManager.isEabProvisionedOnDevice());
+
+ verify(mImsConfigImplBaseMock, times(1)).setConfig(
+ eq(ImsConfig.ConfigConstants.EAB_SETTING_ENABLED),
+ eq(0));
+ verify(mImsConfigImplBaseMock, times(1)).getConfigInt(
+ eq(ImsConfig.ConfigConstants.EAB_SETTING_ENABLED));
+ }
+
+ /**
+ * Tests that when WFC is enabled/disabled for home/roaming, that setting is sent to the
+ * ImsService correctly.
+ *
+ * Preconditions:
+ * - CarrierConfigManager.KEY_EDITABLE_WFC_MODE_BOOL = true
+ * - CarrierConfigManager.KEY_CARRIER_DEFAULT_WFC_IMS_ROAMING_ENABLED_BOOL = true
+ */
+ @Test @SmallTest
+ public void testSetWfcSetting_true_shouldSetWfcModeWrtRoamingState() throws Exception {
+ setWfcEnabledByUser(true);
+ // First, Set WFC home/roaming mode that is not the Carrier Config default.
+ doReturn(ImsConfig.WfcModeFeatureValueConstants.WIFI_PREFERRED)
+ .when(mSubscriptionManagerProxy).getIntegerSubscriptionProperty(
+ anyInt(),
+ eq(SubscriptionManager.WFC_IMS_MODE),
+ anyInt());
+ doReturn(ImsConfig.WfcModeFeatureValueConstants.CELLULAR_PREFERRED)
+ .when(mSubscriptionManagerProxy).getIntegerSubscriptionProperty(
+ anyInt(),
+ eq(SubscriptionManager.WFC_IMS_ROAMING_MODE),
+ anyInt());
+ ImsManager imsManager = getImsManagerAndInitProvisionedValues();
+
+ // Roaming
+ doReturn(true).when(mTelephonyManager).isNetworkRoaming();
+ // Turn on WFC
+ imsManager.setWfcSetting(true);
+ // Roaming mode (CELLULAR_PREFERRED) should be set.
+ verify(mImsConfigImplBaseMock).setConfig(
+ eq(ImsConfig.ConfigConstants.VOICE_OVER_WIFI_MODE),
+ eq(ImsConfig.WfcModeFeatureValueConstants.CELLULAR_PREFERRED));
+ // WFC is enabled, so we should set user roaming setting
+ verify(mImsConfigImplBaseMock).setConfig(
+ eq(ProvisioningManager.KEY_VOICE_OVER_WIFI_ROAMING_ENABLED_OVERRIDE),
+ eq(ProvisioningManager.PROVISIONING_VALUE_ENABLED));
+
+ // Not roaming
+ doReturn(false).when(mTelephonyManager).isNetworkRoaming();
+ // Turn on WFC
+ imsManager.setWfcSetting(true);
+ // Home mode (WIFI_PREFERRED) should be set.
+ verify(mImsConfigImplBaseMock).setConfig(
+ eq(ImsConfig.ConfigConstants.VOICE_OVER_WIFI_MODE),
+ eq(ImsConfig.WfcModeFeatureValueConstants.WIFI_PREFERRED));
+ // WFC is enabled, so we should set user roaming setting
+ verify(mImsConfigImplBaseMock, times(2)).setConfig(
+ eq(ProvisioningManager.KEY_VOICE_OVER_WIFI_ROAMING_ENABLED_OVERRIDE),
+ eq(ProvisioningManager.PROVISIONING_VALUE_ENABLED));
+
+
+ // Turn off WFC and ensure that roaming setting is disabled.
+ doReturn(false).when(mTelephonyManager).isNetworkRoaming();
+ // mock Subscription DB change due to WFC setting being set to false
+ setWfcEnabledByUser(false);
+ imsManager.setWfcSetting(false);
+ verify(mSubscriptionManagerProxy, times(1)).setSubscriptionProperty(
+ anyInt(),
+ eq(SubscriptionManager.WFC_IMS_ENABLED),
+ eq("0" /*false*/));
+ verify(mImsConfigImplBaseMock).setConfig(
+ eq(ProvisioningManager.KEY_VOICE_OVER_WIFI_ROAMING_ENABLED_OVERRIDE),
+ eq(ProvisioningManager.PROVISIONING_VALUE_DISABLED));
+ }
+
+
+ /**
+ * Tests that when user changed WFC setting while NOT roaming, the home WFC mode is sent to the
+ * modem and the roaming enabled configuration is pushed.
+ *
+ * Preconditions:
+ * - CarrierConfigManager.KEY_EDITABLE_WFC_MODE_BOOL = true
+ * - CarrierConfigManager.KEY_CARRIER_DEFAULT_WFC_IMS_ROAMING_ENABLED_BOOL = true
+ */
+ @Test @SmallTest
+ public void testSetWfcSetting_shouldSetWfcModeRoamingDisabledUserEnabled() throws Exception {
+ setWfcEnabledByUser(true);
+ // The user has previously enabled "WFC while roaming" setting in UI and then turned WFC
+ // off.
+ doReturn(1 /*true*/).when(mSubscriptionManagerProxy).getIntegerSubscriptionProperty(
+ anyInt(),
+ eq(SubscriptionManager.WFC_IMS_ROAMING_ENABLED),
+ anyInt());
+
+ ImsManager imsManager = getImsManagerAndInitProvisionedValues();
+
+ // We are currently on the home network, not roaming.
+ doReturn(false).when(mTelephonyManager).isNetworkRoaming();
+
+ // User enables WFC from UI
+ imsManager.setWfcSetting(true /*enabled*/);
+ verify(mImsConfigImplBaseMock).setConfig(
+ eq(ProvisioningManager.KEY_VOICE_OVER_WIFI_MODE_OVERRIDE),
+ eq(ImsMmTelManager.WIFI_MODE_CELLULAR_PREFERRED));
+ verify(mImsConfigImplBaseMock).setConfig(
+ eq(ProvisioningManager.KEY_VOICE_OVER_WIFI_ROAMING_ENABLED_OVERRIDE),
+ // Should be enabled because the user enabled the "WFC while roaming" setting
+ // independent of whether or not we are roaming.
+ eq(ProvisioningManager.PROVISIONING_VALUE_ENABLED));
+ }
+
+ /**
+ * Tests that when user changed WFC setting while roaming, that the correct user setting
+ * is sent to the ImsService when changing the roaming mode.
+ *
+ * Preconditions:
+ * - CarrierConfigManager.KEY_EDITABLE_WFC_MODE_BOOL = true
+ * - CarrierConfigManager.KEY_CARRIER_DEFAULT_WFC_IMS_ROAMING_ENABLED_BOOL = true
+ */
+ @Test @SmallTest
+ public void testSetWfcSetting_shouldSetWfcModeRoamingEnabledUserEnabled() throws Exception {
+ setWfcEnabledByUser(true);
+ // The user has previously enabled "WFC while roaming" setting in UI and then turned WFC
+ // off.
+ doReturn(1 /*true*/).when(mSubscriptionManagerProxy).getIntegerSubscriptionProperty(
+ anyInt(),
+ eq(SubscriptionManager.WFC_IMS_ROAMING_ENABLED),
+ anyInt());
+
+ ImsManager imsManager = getImsManagerAndInitProvisionedValues();
+
+ // The device is currently roaming
+ doReturn(true).when(mTelephonyManager).isNetworkRoaming();
+
+ // The user has enabled WFC in the UI while the device is roaming.
+ imsManager.setWfcSetting(true /*enabled*/);
+
+ verify(mImsConfigImplBaseMock).setConfig(
+ eq(ProvisioningManager.KEY_VOICE_OVER_WIFI_MODE_OVERRIDE),
+ // Default for roaming is WFC_IMS_ROAMING_MODE_DEFAULT_VAL
+ eq(ImsMmTelManager.WIFI_MODE_WIFI_PREFERRED));
+ verify(mImsConfigImplBaseMock).setConfig(
+ eq(ProvisioningManager.KEY_VOICE_OVER_WIFI_ROAMING_ENABLED_OVERRIDE),
+ // Should be enabled because user enabled the setting in the UI previously.
+ eq(ProvisioningManager.PROVISIONING_VALUE_ENABLED));
+ }
+
+ /**
+ * Tests that when a WFC mode is updated for home, that setting is sent to the
+ * ImsService correctly or ignored if the roaming mode is changed.
+ *
+ * Preconditions:
+ * - CarrierConfigManager.KEY_EDITABLE_WFC_MODE_BOOL = true
+ * - CarrierConfigManager.KEY_CARRIER_DEFAULT_WFC_IMS_ROAMING_ENABLED_BOOL = true
+ */
+ @Test @SmallTest
+ public void testSetWfcMode_shouldSetWfcModeRoamingDisabled() throws Exception {
+ ImsManager imsManager = getImsManagerAndInitProvisionedValues();
+
+ // the device is not currently roaming
+ doReturn(false).when(mTelephonyManager).isNetworkRoaming();
+
+ // set the WFC roaming mode while the device is not roaming, so any changes to roaming mode
+ // should be ignored
+ imsManager.setWfcMode(ImsMmTelManager.WIFI_MODE_CELLULAR_PREFERRED, true /*IsRoaming*/);
+ verify(mImsConfigImplBaseMock, never()).setConfig(
+ eq(ProvisioningManager.KEY_VOICE_OVER_WIFI_MODE_OVERRIDE),
+ anyInt());
+ verify(mImsConfigImplBaseMock, never()).setConfig(
+ eq(ProvisioningManager.KEY_VOICE_OVER_WIFI_ROAMING_ENABLED_OVERRIDE),
+ anyInt());
+
+ // set home WFC mode setting while not roaming, the configuration should be set correctly.
+ imsManager.setWfcMode(ImsMmTelManager.WIFI_MODE_CELLULAR_PREFERRED, false /*IsRoaming*/);
+ verify(mImsConfigImplBaseMock).setConfig(
+ eq(ProvisioningManager.KEY_VOICE_OVER_WIFI_MODE_OVERRIDE),
+ eq(ImsMmTelManager.WIFI_MODE_CELLULAR_PREFERRED));
+ // WiFi Roaming enabled setting is not related to WFC mode
+ verify(mImsConfigImplBaseMock, never()).setConfig(
+ eq(ProvisioningManager.KEY_VOICE_OVER_WIFI_ROAMING_ENABLED_OVERRIDE),
+ anyInt());
+ }
+
+ /**
+ * Tests that when a WFC mode is updated for roaming while WFC is enabled, that setting is sent
+ * to the ImsService correctly when changing the roaming mode or ignored if the home setting is
+ * changed.
+ *
+ * Preconditions:
+ * - CarrierConfigManager.KEY_EDITABLE_WFC_MODE_BOOL = true
+ * - CarrierConfigManager.KEY_CARRIER_DEFAULT_WFC_IMS_ROAMING_ENABLED_BOOL = true
+ */
+ @Test @SmallTest
+ public void testSetWfcMode_wfcEnabledShouldSetWfcModeRoamingEnabled() throws Exception {
+ ImsManager imsManager = getImsManagerAndInitProvisionedValues();
+
+ // The user has previously enabled WFC in the settings UI.
+ doReturn(1 /*true*/).when(mSubscriptionManagerProxy).getIntegerSubscriptionProperty(
+ anyInt(),
+ eq(SubscriptionManager.WFC_IMS_ENABLED),
+ anyInt());
+
+ // The device is roaming
+ doReturn(true).when(mTelephonyManager).isNetworkRoaming();
+
+ // The carrier app has changed the WFC mode for roaming while the device is home. The
+ // result of this operation is that the neither the WFC mode or the roaming enabled
+ // configuration should change.
+ imsManager.setWfcMode(ImsMmTelManager.WIFI_MODE_CELLULAR_PREFERRED, false /*IsRoaming*/);
+ verify(mImsConfigImplBaseMock, never()).setConfig(
+ eq(ProvisioningManager.KEY_VOICE_OVER_WIFI_MODE_OVERRIDE),
+ anyInt());
+ verify(mImsConfigImplBaseMock, never()).setConfig(
+ eq(ProvisioningManager.KEY_VOICE_OVER_WIFI_ROAMING_ENABLED_OVERRIDE),
+ anyInt());
+
+ // The carrier app has set the WFC mode for roaming while the device is roaming. The
+ // WFC mode should be updated to reflect the roaming setting and the roaming enabled
+ // configuration should be changed to enabled.
+ imsManager.setWfcMode(ImsMmTelManager.WIFI_MODE_CELLULAR_PREFERRED, true /*IsRoaming*/);
+ verify(mImsConfigImplBaseMock).setConfig(
+ eq(ProvisioningManager.KEY_VOICE_OVER_WIFI_MODE_OVERRIDE),
+ eq(ImsMmTelManager.WIFI_MODE_CELLULAR_PREFERRED));
+ // WiFi Roaming enabled setting is not related to WFC mode
+ verify(mImsConfigImplBaseMock, never()).setConfig(
+ eq(ProvisioningManager.KEY_VOICE_OVER_WIFI_ROAMING_ENABLED_OVERRIDE),
+ anyInt());
+ }
+
+ /**
+ * Tests that when a WFC mode is updated for roaming while WFC is disabled, the WFC roaming
+ * setting is always set to disabled.
+ *
+ * Preconditions:
+ * - CarrierConfigManager.KEY_EDITABLE_WFC_MODE_BOOL = true
+ * - CarrierConfigManager.KEY_CARRIER_DEFAULT_WFC_IMS_ROAMING_ENABLED_BOOL = true
+ */
+ @Test @SmallTest
+ public void testSetWfcMode_WfcDisabledShouldNotSetWfcModeRoamingEnabled() throws Exception {
+ ImsManager imsManager = getImsManagerAndInitProvisionedValues();
+
+ // The user has previously disabled WFC in the settings UI.
+ doReturn(0 /*false*/).when(mSubscriptionManagerProxy).getIntegerSubscriptionProperty(
+ anyInt(),
+ eq(SubscriptionManager.WFC_IMS_ENABLED),
+ anyInt());
+
+ // The device is roaming
+ doReturn(true).when(mTelephonyManager).isNetworkRoaming();
+
+ // WFC is disabled and the carrier app has set the WFC mode for roaming while the device is
+ // roaming. The WFC mode should be updated to reflect the roaming setting and the roaming
+ // enabled configuration should be disabled because WFC is disabled.
+ imsManager.setWfcMode(ImsMmTelManager.WIFI_MODE_CELLULAR_PREFERRED, true /*IsRoaming*/);
+ verify(mImsConfigImplBaseMock).setConfig(
+ eq(ProvisioningManager.KEY_VOICE_OVER_WIFI_MODE_OVERRIDE),
+ eq(ImsMmTelManager.WIFI_MODE_CELLULAR_PREFERRED));
+ // WiFi Roaming enabled setting is not related to WFC mode
+ verify(mImsConfigImplBaseMock, never()).setConfig(
+ eq(ProvisioningManager.KEY_VOICE_OVER_WIFI_ROAMING_ENABLED_OVERRIDE),
+ anyInt());
+ }
+
+ /**
+ * Tests that when user changed WFC mode while not roaming, the new mode is sent to the modem
+ * and roaming enabled indication is sent to the ImsService correctly when changing the roaming
+ * mode.
+ *
+ * Preconditions:
+ * - CarrierConfigManager.KEY_EDITABLE_WFC_MODE_BOOL = true
+ * - CarrierConfigManager.KEY_CARRIER_DEFAULT_WFC_IMS_ROAMING_ENABLED_BOOL = true
+ */
+ @Test @SmallTest
+ public void testSetWfcMode_shouldSetWfcModeRoamingDisabledUserEnabled() throws Exception {
+ // The user has enabled the WFC setting in the UI.
+ doReturn(1 /*true*/).when(mSubscriptionManagerProxy).getIntegerSubscriptionProperty(
+ anyInt(),
+ eq(SubscriptionManager.WFC_IMS_ENABLED),
+ anyInt());
+ // The user has enabled the "WFC while roaming" setting in the UI while WFC was enabled
+ doReturn(1 /*true*/).when(mSubscriptionManagerProxy).getIntegerSubscriptionProperty(
+ anyInt(),
+ eq(SubscriptionManager.WFC_IMS_ROAMING_ENABLED),
+ anyInt());
+
+ ImsManager imsManager = getImsManagerAndInitProvisionedValues();
+
+ // The device is currently on the home network
+ doReturn(false).when(mTelephonyManager).isNetworkRoaming();
+
+ // The user has changed the WFC mode in the UI for the non-roaming configuration
+ imsManager.setWfcMode(ImsMmTelManager.WIFI_MODE_CELLULAR_PREFERRED, false /*IsRoaming*/);
+ verify(mImsConfigImplBaseMock).setConfig(
+ eq(ProvisioningManager.KEY_VOICE_OVER_WIFI_MODE_OVERRIDE),
+ // ensure that the correct cellular preferred config change is sent
+ eq(ImsMmTelManager.WIFI_MODE_CELLULAR_PREFERRED));
+ // WiFi Roaming enabled setting is not related to WFC mode
+ verify(mImsConfigImplBaseMock, never()).setConfig(
+ eq(ProvisioningManager.KEY_VOICE_OVER_WIFI_ROAMING_ENABLED_OVERRIDE),
+ anyInt());
+ }
+
+ /**
+ * Tests that when user changed WFC mode while roaming, that setting is sent to the
+ * ImsService correctly when changing the roaming mode.
+ *
+ * Preconditions:
+ * - CarrierConfigManager.KEY_EDITABLE_WFC_MODE_BOOL = true
+ * - CarrierConfigManager.KEY_CARRIER_DEFAULT_WFC_IMS_ROAMING_ENABLED_BOOL = true
+ */
+ @Test @SmallTest
+ public void testSetWfcMode_shouldSetWfcModeRoamingEnabledUserDisabled() throws Exception {
+ // The user disabled "WFC while roaming" setting in the UI
+ doReturn(0 /*false*/).when(mSubscriptionManagerProxy).getIntegerSubscriptionProperty(
+ anyInt(),
+ eq(SubscriptionManager.WFC_IMS_ROAMING_ENABLED),
+ anyInt());
+
+ ImsManager imsManager = getImsManagerAndInitProvisionedValues();
+
+ // the device is currently roaming
+ doReturn(true).when(mTelephonyManager).isNetworkRoaming();
+
+ // The carrier app has changed the WFC mode while roaming, so we must set the WFC mode
+ // to the new configuration.
+ imsManager.setWfcMode(ImsMmTelManager.WIFI_MODE_CELLULAR_PREFERRED, true /*IsRoaming*/);
+ verify(mImsConfigImplBaseMock).setConfig(
+ eq(ProvisioningManager.KEY_VOICE_OVER_WIFI_MODE_OVERRIDE),
+ eq(ImsMmTelManager.WIFI_MODE_CELLULAR_PREFERRED));
+ // WiFi Roaming enabled setting is not related to WFC mode
+ verify(mImsConfigImplBaseMock, never()).setConfig(
+ eq(ProvisioningManager.KEY_VOICE_OVER_WIFI_ROAMING_ENABLED_OVERRIDE),
+ anyInt());
+ }
+
+ /**
+ * Tests that the settings for WFC mode are ignored if the Carrier sets the settings to not
+ * editable.
+ *
+ * Preconditions:
+ * - CarrierConfigManager.KEY_EDITABLE_WFC_MODE_BOOL = false
+ */
+ @Test @SmallTest
+ public void testSetWfcSetting_wfcNotEditable() throws Exception {
+ setWfcEnabledByUser(true);
+ mBundle.putBoolean(CarrierConfigManager.KEY_EDITABLE_WFC_MODE_BOOL,
+ WFC_IMS_NOT_EDITABLE_VAL);
+ mBundle.putBoolean(CarrierConfigManager.KEY_EDITABLE_WFC_ROAMING_MODE_BOOL,
+ WFC_IMS_ROAMING_NOT_EDITABLE_VAL);
+ // Set some values that are different than the defaults for WFC mode.
+ doReturn(ImsConfig.WfcModeFeatureValueConstants.WIFI_ONLY)
+ .when(mSubscriptionManagerProxy).getIntegerSubscriptionProperty(
+ anyInt(),
+ eq(SubscriptionManager.WFC_IMS_MODE),
+ anyInt());
+ doReturn(ImsConfig.WfcModeFeatureValueConstants.WIFI_ONLY)
+ .when(mSubscriptionManagerProxy).getIntegerSubscriptionProperty(
+ anyInt(),
+ eq(SubscriptionManager.WFC_IMS_ROAMING_MODE),
+ anyInt());
+ ImsManager imsManager = getImsManagerAndInitProvisionedValues();
+
+ // Roaming
+ doReturn(true).when(mTelephonyManager).isNetworkRoaming();
+ // Turn on WFC
+ imsManager.setWfcSetting(true);
+ verify(mImsConfigImplBaseMock).setConfig(
+ eq(ImsConfig.ConfigConstants.VOICE_OVER_WIFI_MODE),
+ eq(WFC_IMS_ROAMING_MODE_DEFAULT_VAL));
+
+ // Not roaming
+ doReturn(false).when(mTelephonyManager).isNetworkRoaming();
+ // Turn on WFC
+ imsManager.setWfcSetting(true);
+ // Default Home mode (CELLULAR_PREFERRED) should be set.
+ verify(mImsConfigImplBaseMock).setConfig(
+ eq(ImsConfig.ConfigConstants.VOICE_OVER_WIFI_MODE),
+ eq(WFC_IMS_MODE_DEFAULT_VAL));
+ }
+
+ /**
+ * Tests that the CarrierConfig defaults will be used if no setting is set in the Subscription
+ * Manager.
+ *
+ * Preconditions:
+ * - CarrierConfigManager.KEY_EDITABLE_WFC_MODE_BOOL = true
+ * - CarrierConfigManager.KEY_CARRIER_DEFAULT_WFC_IMS_MODE_INT = Carrier preferred
+ * - CarrierConfigManager.KEY_CARRIER_DEFAULT_WFC_IMS_ROAMING_MODE_INT = WiFi preferred
+ */
+ @Test @SmallTest
+ public void testSetWfcSetting_noUserSettingSet() throws Exception {
+ setWfcEnabledByUser(true);
+ ImsManager imsManager = getImsManagerAndInitProvisionedValues();
+
+ // Roaming
+ doReturn(true).when(mTelephonyManager).isNetworkRoaming();
+ // Turn on WFC
+ imsManager.setWfcSetting(true);
+
+ // Default Roaming mode (WIFI_PREFERRED) for carrier should be set. With 1000 ms timeout.
+ verify(mImsConfigImplBaseMock).setConfig(
+ eq(ImsConfig.ConfigConstants.VOICE_OVER_WIFI_MODE),
+ eq(WFC_IMS_ROAMING_MODE_DEFAULT_VAL));
+
+ // Not roaming
+ doReturn(false).when(mTelephonyManager).isNetworkRoaming();
+ // Turn on WFC
+ imsManager.setWfcSetting(true);
+
+ // Default Home mode (CELLULAR_PREFERRED) for carrier should be set. With 1000 ms timeout.
+ verify(mImsConfigImplBaseMock).setConfig(
+ eq(ImsConfig.ConfigConstants.VOICE_OVER_WIFI_MODE),
+ eq(WFC_IMS_MODE_DEFAULT_VAL));
+ }
+
+ /**
+ * Tests the operation of getWfcMode when the configuration to use the home network mode when
+ * roaming for WFC is false. First, it checks that the user setting for WFC_IMS_ROAMING_MODE is
+ * returned when WFC roaming is set to editable. Then, it switches the WFC roaming mode to not
+ * editable and ensures that the default WFC roaming mode is returned.
+ *
+ * Preconditions:
+ * - CarrierConfigManager.KEY_USE_WFC_HOME_NETWORK_MODE_IN_ROAMING_NETWORK_BOOL = false
+ */
+ @Test @SmallTest
+ public void getWfcMode_useWfcHomeModeConfigFalse_shouldUseWfcRoamingMode() {
+ // Set some values that are different than the defaults for WFC mode.
+ doReturn(ImsConfig.WfcModeFeatureValueConstants.WIFI_ONLY)
+ .when(mSubscriptionManagerProxy).getIntegerSubscriptionProperty(
+ anyInt(),
+ eq(SubscriptionManager.WFC_IMS_MODE),
+ anyInt());
+ doReturn(ImsConfig.WfcModeFeatureValueConstants.CELLULAR_PREFERRED)
+ .when(mSubscriptionManagerProxy).getIntegerSubscriptionProperty(
+ anyInt(),
+ eq(SubscriptionManager.WFC_IMS_ROAMING_MODE),
+ anyInt());
+
+ ImsManager imsManager = getImsManagerAndInitProvisionedValues();
+
+ // Check that use the WFC roaming network mode.
+ assertEquals(ImsConfig.WfcModeFeatureValueConstants.CELLULAR_PREFERRED,
+ imsManager.getWfcMode(true));
+ verify(mSubscriptionManagerProxy, times(1)).getIntegerSubscriptionProperty(
+ anyInt(),
+ eq(SubscriptionManager.WFC_IMS_ROAMING_MODE),
+ anyInt());
+
+ // Set WFC roaming network mode to not editable.
+ mBundle.putBoolean(CarrierConfigManager.KEY_EDITABLE_WFC_ROAMING_MODE_BOOL,
+ WFC_IMS_ROAMING_NOT_EDITABLE_VAL);
+
+ // Check that use the default WFC roaming network mode.
+ assertEquals(WFC_IMS_ROAMING_MODE_DEFAULT_VAL, imsManager.getWfcMode(true));
+ verify(mSubscriptionManagerProxy, times(1)).getIntegerSubscriptionProperty(
+ anyInt(),
+ eq(SubscriptionManager.WFC_IMS_ROAMING_MODE),
+ anyInt());
+ }
+
+ /**
+ * Tests the operation of getWfcMode when the configuration to use the home network mode when
+ * roaming for WFC is true independent of whether or not the WFC roaming mode is editable.
+ *
+ * Preconditions:
+ * - CarrierConfigManager.KEY_USE_WFC_HOME_NETWORK_MODE_IN_ROAMING_NETWORK_BOOL = true
+ */
+ @Test @SmallTest
+ public void getWfcMode_useWfcHomeModeConfigTrue_shouldUseWfcHomeMode() {
+ // Set some values that are different than the defaults for WFC mode.
+ doReturn(ImsConfig.WfcModeFeatureValueConstants.WIFI_ONLY)
+ .when(mSubscriptionManagerProxy).getIntegerSubscriptionProperty(
+ anyInt(),
+ eq(SubscriptionManager.WFC_IMS_MODE),
+ anyInt());
+ doReturn(ImsConfig.WfcModeFeatureValueConstants.CELLULAR_PREFERRED)
+ .when(mSubscriptionManagerProxy).getIntegerSubscriptionProperty(
+ anyInt(),
+ eq(SubscriptionManager.WFC_IMS_ROAMING_MODE),
+ anyInt());
+
+ // Set to use WFC home network mode in roaming network.
+ mBundle.putBoolean(
+ CarrierConfigManager.KEY_USE_WFC_HOME_NETWORK_MODE_IN_ROAMING_NETWORK_BOOL,
+ WFC_USE_HOME_MODE_FOR_ROAMING_VAL);
+
+ ImsManager imsManager = getImsManagerAndInitProvisionedValues();
+
+ // Check that use the WFC home network mode.
+ assertEquals(ImsConfig.WfcModeFeatureValueConstants.WIFI_ONLY, imsManager.getWfcMode(true));
+ verify(mSubscriptionManagerProxy, times(1)).getIntegerSubscriptionProperty(
+ anyInt(),
+ eq(SubscriptionManager.WFC_IMS_MODE),
+ anyInt());
+
+ // Set WFC home network mode to not editable.
+ mBundle.putBoolean(CarrierConfigManager.KEY_EDITABLE_WFC_MODE_BOOL,
+ WFC_IMS_NOT_EDITABLE_VAL);
+
+ // Check that use the default WFC home network mode.
+ assertEquals(WFC_IMS_MODE_DEFAULT_VAL, imsManager.getWfcMode(true));
+ verify(mSubscriptionManagerProxy, times(1)).getIntegerSubscriptionProperty(
+ anyInt(),
+ eq(SubscriptionManager.WFC_IMS_MODE),
+ anyInt());
+ }
+
+ /**
+ * Tests the operation of setWfcRoamingSetting and ensures that the user setting for WFC roaming
+ * and the ImsConfig setting are both called properly.
+ */
+ @Test @SmallTest
+ public void setWfcRoamingSettingTest() {
+ ImsManager imsManager = getImsManagerAndInitProvisionedValues();
+
+ imsManager.setWfcRoamingSetting(true);
+ verify(mSubscriptionManagerProxy, times(1)).setSubscriptionProperty(
+ anyInt(),
+ eq(SubscriptionManager.WFC_IMS_ROAMING_ENABLED),
+ eq("1"));
+ verify(mImsConfigImplBaseMock).setConfig(
+ eq(ProvisioningManager.KEY_VOICE_OVER_WIFI_ROAMING_ENABLED_OVERRIDE),
+ eq(ProvisioningManager.PROVISIONING_VALUE_ENABLED));
+
+ imsManager.setWfcRoamingSetting(false);
+ verify(mSubscriptionManagerProxy, times(1)).setSubscriptionProperty(
+ anyInt(),
+ eq(SubscriptionManager.WFC_IMS_ROAMING_ENABLED),
+ eq("0"));
+ verify(mImsConfigImplBaseMock).setConfig(
+ eq(ProvisioningManager.KEY_VOICE_OVER_WIFI_ROAMING_ENABLED_OVERRIDE),
+ eq(ProvisioningManager.PROVISIONING_VALUE_DISABLED));
+
+ }
+
+ private ImsManager getImsManagerAndInitProvisionedValues() {
+ when(mImsConfigImplBaseMock.getConfigInt(anyInt()))
+ .thenAnswer(invocation -> {
+ return getProvisionedInt((Integer) (invocation.getArguments()[0]));
+ });
+
+ when(mImsConfigImplBaseMock.setConfig(anyInt(), anyInt()))
+ .thenAnswer(invocation -> {
+ mProvisionedIntVals.put((Integer) (invocation.getArguments()[0]),
+ (Integer) (invocation.getArguments()[1]));
+ return ImsConfig.OperationStatusConstants.SUCCESS;
+ });
+
+
+ // Configure ImsConfigStub
+ mImsConfigStub = new ImsConfigImplBase.ImsConfigStub(mImsConfigImplBaseMock);
+ doReturn(mImsConfigStub).when(mMmTelFeatureConnection).getConfig();
+
+ ImsManager mgr = new ImsManager(mContext, mPhoneId,
+ (context, phoneId, feature, c, r, s) -> mMmTelFeatureConnection,
+ mSubscriptionManagerProxy, mSettingsProxy);
+ ImsFeatureContainer c = new ImsFeatureContainer(mMmTelFeature, mImsConfig, mImsReg,
+ mSipTransport, 0 /*caps*/);
+ mgr.associate(c);
+ // Enabled WFC by default
+ setWfcEnabledByPlatform(true);
+ return mgr;
+ }
+
+ private void setWfcEnabledByPlatform(boolean isEnabled) {
+ Resources res = mContext.getResources();
+ doReturn(isEnabled).when(res).getBoolean(
+ com.android.internal.R.bool.config_device_wfc_ims_available);
+ }
+
+ private void setWfcEnabledByUser(boolean isEnabled) {
+ // The user has previously enabled WFC in the settings UI.
+ doReturn(isEnabled ? 1 /*true*/ : 0).when(mSubscriptionManagerProxy)
+ .getIntegerSubscriptionProperty(anyInt(), eq(SubscriptionManager.WFC_IMS_ENABLED),
+ anyInt());
+ }
+
+ // If the value is ever set, return the set value. If not, return a constant value 1000.
+ private int getProvisionedInt(int item) {
+ if (mProvisionedIntVals.containsKey(item)) {
+ return mProvisionedIntVals.get(item);
+ } else {
+ return ImsConfig.FeatureValueConstants.ON;
+ }
+ }
+}
diff --git a/tests/src/com/android/ims/ImsTestBase.java b/tests/src/com/android/ims/ImsTestBase.java
index 32e57a3f..8e9064ee 100644
--- a/tests/src/com/android/ims/ImsTestBase.java
+++ b/tests/src/com/android/ims/ImsTestBase.java
@@ -19,8 +19,8 @@ package com.android.ims;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
-
-import androidx.test.InstrumentationRegistry;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
import org.mockito.MockitoAnnotations;
@@ -32,11 +32,21 @@ import java.util.concurrent.TimeUnit;
*/
public class ImsTestBase {
+ protected ContextFixture mContextFixture;
protected Context mContext;
+ protected TelephonyManager mTelephonyManager;
+ protected SubscriptionManager mSubscriptionManager;
+
public void setUp() throws Exception {
- mContext = InstrumentationRegistry.getTargetContext();
MockitoAnnotations.initMocks(this);
+ mContextFixture = new ContextFixture();
+ mContext = mContextFixture.getContext();
+
+ mTelephonyManager = (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
+ mSubscriptionManager = (SubscriptionManager) mContext.getSystemService(
+ Context.TELEPHONY_SUBSCRIPTION_SERVICE);
+
// Set up the looper if it does not exist on the test thread.
if (Looper.myLooper() == null) {
Looper.prepare();
diff --git a/tests/src/com/android/ims/ImsUtTest.java b/tests/src/com/android/ims/ImsUtTest.java
new file mode 100644
index 00000000..634b4d91
--- /dev/null
+++ b/tests/src/com/android/ims/ImsUtTest.java
@@ -0,0 +1,149 @@
+/*
+ * 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.ims;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertNotNull;
+import static junit.framework.TestCase.fail;
+
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.verify;
+
+import android.os.AsyncResult;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.telephony.ims.ImsSsInfo;
+import android.telephony.ims.ImsUtListener;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.ims.internal.IImsUt;
+
+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.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+@RunWith(AndroidJUnit4.class)
+public class ImsUtTest extends ImsTestBase {
+
+ private static final int MSG_QUERY = 1;
+ private static final int TEST_TIMEOUT_MS = 5000;
+
+ private static class TestHandler extends Handler {
+
+ TestHandler(Looper looper) {
+ super(looper);
+ }
+
+ private final LinkedBlockingQueue<ImsSsInfo> mPendingSsInfos = new LinkedBlockingQueue<>(1);
+ @Override
+ public void handleMessage(Message msg) {
+ if (msg.what == MSG_QUERY) {
+ AsyncResult ar = (AsyncResult) msg.obj;
+ mPendingSsInfos.offer((ImsSsInfo) ar.result);
+ }
+ }
+ public ImsSsInfo getPendingImsSsInfo() {
+ try {
+ return mPendingSsInfos.poll(TEST_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+ } catch (InterruptedException e) {
+ fail("test interrupted!");
+ }
+ return null;
+ }
+ }
+
+ @Mock IImsUt mImsUtBinder;
+
+ private TestHandler mHandler;
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ mHandler = new TestHandler(Looper.getMainLooper());
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ waitForHandlerAction(mHandler, 1000/*ms*/);
+ super.tearDown();
+ }
+
+ @Test
+ @SmallTest
+ public void testClirConversionCompat() throws Exception {
+ ArgumentCaptor<ImsUt.IImsUtListenerProxy> captor =
+ ArgumentCaptor.forClass(ImsUt.IImsUtListenerProxy.class);
+ ImsUt mImsUt = new ImsUt(mImsUtBinder);
+ verify(mImsUtBinder).setListener(captor.capture());
+ ImsUt.IImsUtListenerProxy proxy = captor.getValue();
+ assertNotNull(proxy);
+
+ doReturn(2).when(mImsUtBinder).queryCLIR();
+ mImsUt.queryCLIR(Message.obtain(mHandler, MSG_QUERY));
+
+ Bundle result = new Bundle();
+ result.putIntArray(ImsUtListener.BUNDLE_KEY_CLIR, new int[] {
+ ImsSsInfo.CLIR_OUTGOING_INVOCATION, ImsSsInfo.CLIR_STATUS_PROVISIONED_PERMANENT});
+ // This is deprecated, will be converted from Bundle -> ImsSsInfo
+ proxy.utConfigurationQueried(null, 2 /*id*/, result);
+ waitForHandlerAction(mHandler, 1000/*ms*/);
+
+
+ ImsSsInfo info = mHandler.getPendingImsSsInfo();
+ assertNotNull(info);
+ assertEquals(ImsSsInfo.CLIR_OUTGOING_INVOCATION, info.getClirOutgoingState());
+ assertEquals(ImsSsInfo.CLIR_STATUS_PROVISIONED_PERMANENT,
+ info.getClirInterrogationStatus());
+ }
+
+ @Test
+ @SmallTest
+ public void testClipConversionCompat() throws Exception {
+ ArgumentCaptor<ImsUt.IImsUtListenerProxy> captor =
+ ArgumentCaptor.forClass(ImsUt.IImsUtListenerProxy.class);
+ ImsUt mImsUt = new ImsUt(mImsUtBinder);
+ verify(mImsUtBinder).setListener(captor.capture());
+ ImsUt.IImsUtListenerProxy proxy = captor.getValue();
+ assertNotNull(proxy);
+
+ doReturn(2).when(mImsUtBinder).queryCLIP();
+ mImsUt.queryCLIP(Message.obtain(mHandler, MSG_QUERY));
+
+ ImsSsInfo info = new ImsSsInfo.Builder(ImsSsInfo.ENABLED).setProvisionStatus(
+ ImsSsInfo.CLIR_STATUS_PROVISIONED_PERMANENT).build();
+ Bundle result = new Bundle();
+ result.putParcelable(ImsUtListener.BUNDLE_KEY_SSINFO, info);
+ // This is deprecated, will be converted from Bundle -> ImsSsInfo
+ proxy.utConfigurationQueried(null, 2 /*id*/, result);
+ waitForHandlerAction(mHandler, 1000/*ms*/);
+
+ ImsSsInfo resultInfo = mHandler.getPendingImsSsInfo();
+ assertNotNull(resultInfo);
+ assertEquals(info.getStatus(), resultInfo.getStatus());
+ assertEquals(info.getProvisionStatus(), resultInfo.getProvisionStatus());
+ }
+}
diff --git a/tests/src/com/android/ims/MmTelFeatureConnectionTest.java b/tests/src/com/android/ims/MmTelFeatureConnectionTest.java
new file mode 100644
index 00000000..620fa23b
--- /dev/null
+++ b/tests/src/com/android/ims/MmTelFeatureConnectionTest.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.ims;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+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 android.content.Context;
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.IInterface;
+import android.telephony.SubscriptionInfo;
+import android.telephony.SubscriptionManager;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+public class MmTelFeatureConnectionTest extends ImsTestBase {
+
+ private class TestCallback extends Binder implements IInterface {
+
+ @Override
+ public IBinder asBinder() {
+ return this;
+ }
+ }
+
+ private class CallbackManagerTest extends
+ ImsCallbackAdapterManager<TestCallback> {
+
+ List<TestCallback> mCallbacks = new ArrayList<>();
+
+ CallbackManagerTest(Context context, Object lock) {
+ super(context, lock, 0 /*slotId*/);
+ }
+
+ // A callback has been registered. Register that callback with the MmTelFeature.
+ @Override
+ public void registerCallback(TestCallback localCallback) {
+ mCallbacks.add(localCallback);
+ }
+
+ // A callback has been removed, unregister that callback with the MmTelFeature.
+ @Override
+ public void unregisterCallback(TestCallback localCallback) {
+ mCallbacks.remove(localCallback);
+ }
+
+ public boolean doesCallbackExist(TestCallback callback) {
+ return mCallbacks.contains(callback);
+ }
+ }
+ private CallbackManagerTest mCallbackManagerUT;
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ mCallbackManagerUT = new CallbackManagerTest(mContext, this);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ mCallbackManagerUT = null;
+ super.tearDown();
+ }
+
+ /**
+ * Basic test of deprecated functionality, ensure that adding the callback directly triggers the
+ * appropriate registerCallback and unregisterCallback calls.
+ */
+ @Test
+ @SmallTest
+ public void testCallbackAdapter_addAndRemoveCallback() throws Exception {
+ TestCallback testCallback = new TestCallback();
+ mCallbackManagerUT.addCallback(testCallback);
+ assertTrue(mCallbackManagerUT.doesCallbackExist(testCallback));
+ // The subscriptions changed listener should only be added for callbacks that are being
+ // linked to a subscription.
+ verify(mSubscriptionManager, never()).addOnSubscriptionsChangedListener(
+ any(SubscriptionManager.OnSubscriptionsChangedListener.class));
+
+ mCallbackManagerUT.removeCallback(testCallback);
+ assertFalse(mCallbackManagerUT.doesCallbackExist(testCallback));
+ // The subscriptions changed listener should only be removed for callbacks that are
+ // linked to a subscription.
+ verify(mSubscriptionManager, never()).removeOnSubscriptionsChangedListener(
+ any(SubscriptionManager.OnSubscriptionsChangedListener.class));
+ }
+
+ /**
+ * Ensure that adding the callback and linking subId triggers the appropriate registerCallback
+ * and unregisterCallback calls as well as the subscriptionChanged listener.
+ */
+ @Test
+ @SmallTest
+ public void testCallbackAdapter_addAndRemoveCallbackForSub() throws Exception {
+ TestCallback testCallback = new TestCallback();
+ int testSub = 1;
+ mCallbackManagerUT.addCallbackForSubscription(testCallback, testSub);
+ assertTrue(mCallbackManagerUT.doesCallbackExist(testCallback));
+ verify(mSubscriptionManager, times(1)).addOnSubscriptionsChangedListener(
+ any(SubscriptionManager.OnSubscriptionsChangedListener.class));
+
+ mCallbackManagerUT.removeCallbackForSubscription(testCallback, testSub);
+ assertFalse(mCallbackManagerUT.doesCallbackExist(testCallback));
+ verify(mSubscriptionManager, times(1)).removeOnSubscriptionsChangedListener(
+ any(SubscriptionManager.OnSubscriptionsChangedListener.class));
+ }
+
+ /**
+ * Ensure that adding the callback and linking multiple subIds trigger the appropriate
+ * registerCallback and unregisterCallback calls as well as the subscriptionChanged listener.
+ * When removing the callbacks, the subscriptionChanged listener shoud only be removed when all
+ * callbacks have been removed.
+ */
+ @Test
+ @SmallTest
+ public void testCallbackAdapter_addAndRemoveCallbackForMultipleSubs() throws Exception {
+ TestCallback testCallback1 = new TestCallback();
+ TestCallback testCallback2 = new TestCallback();
+ int testSub1 = 1;
+ int testSub2 = 2;
+ mCallbackManagerUT.addCallbackForSubscription(testCallback1, testSub1);
+ assertTrue(mCallbackManagerUT.doesCallbackExist(testCallback1));
+ mCallbackManagerUT.addCallbackForSubscription(testCallback2, testSub2);
+ assertTrue(mCallbackManagerUT.doesCallbackExist(testCallback2));
+ // This should only happen once.
+ verify(mSubscriptionManager, times(1)).addOnSubscriptionsChangedListener(
+ any(SubscriptionManager.OnSubscriptionsChangedListener.class));
+
+ mCallbackManagerUT.removeCallbackForSubscription(testCallback1, testSub1);
+ assertFalse(mCallbackManagerUT.doesCallbackExist(testCallback1));
+ // removing the listener should not happen until the second callback is removed.
+ verify(mSubscriptionManager, never()).removeOnSubscriptionsChangedListener(
+ any(SubscriptionManager.OnSubscriptionsChangedListener.class));
+
+ mCallbackManagerUT.removeCallbackForSubscription(testCallback2, testSub2);
+ assertFalse(mCallbackManagerUT.doesCallbackExist(testCallback2));
+ verify(mSubscriptionManager, times(1)).removeOnSubscriptionsChangedListener(
+ any(SubscriptionManager.OnSubscriptionsChangedListener.class));
+ }
+
+ /**
+ * The subscriptions have changed, ensure that the callbacks registered to the original
+ * subscription testSub1 are removed, while keeping the callbacks for testSub2, since it was not
+ * removed.
+ */
+ @Test
+ @SmallTest
+ public void testCallbackAdapter_onSubscriptionsChangedMultipleSubs() throws Exception {
+ TestCallback testCallback1 = new TestCallback();
+ TestCallback testCallback2 = new TestCallback();
+ int testSub1 = 1;
+ int testSub2 = 2;
+ int testSub3 = 3;
+ mCallbackManagerUT.addCallbackForSubscription(testCallback1, testSub1);
+ assertTrue(mCallbackManagerUT.doesCallbackExist(testCallback1));
+ mCallbackManagerUT.addCallbackForSubscription(testCallback2, testSub2);
+ assertTrue(mCallbackManagerUT.doesCallbackExist(testCallback2));
+ verify(mSubscriptionManager, times(1)).addOnSubscriptionsChangedListener(
+ any(SubscriptionManager.OnSubscriptionsChangedListener.class));
+
+ // Simulate subscriptions changed, where testSub1 is no longer active
+ doReturn(createSubscriptionInfoList(new int[] {testSub2, testSub3}))
+ .when(mSubscriptionManager).getActiveSubscriptionInfoList(anyBoolean());
+ mCallbackManagerUT.mSubChangedListener.onSubscriptionsChanged();
+ assertFalse(mCallbackManagerUT.doesCallbackExist(testCallback1));
+ // verify that the subscription changed listener is not removed, since we still have a
+ // callback on testSub2
+ verify(mSubscriptionManager, never()).removeOnSubscriptionsChangedListener(
+ any(SubscriptionManager.OnSubscriptionsChangedListener.class));
+ }
+
+ /**
+ * The active subscription has changed, ensure that the callback registered to the original
+ * subscription testSub1 are removed as well as the subscription changed listener, since
+ * there are mo more active callbacks.
+ */
+ @Test
+ @SmallTest
+ public void testCallbackAdapter_onSubscriptionsChangedOneSub() throws Exception {
+ TestCallback testCallback1 = new TestCallback();
+ int testSub1 = 1;
+ int testSub2 = 2;
+ mCallbackManagerUT.addCallbackForSubscription(testCallback1, testSub1);
+ assertTrue(mCallbackManagerUT.doesCallbackExist(testCallback1));
+ verify(mSubscriptionManager, times(1)).addOnSubscriptionsChangedListener(
+ any(SubscriptionManager.OnSubscriptionsChangedListener.class));
+
+ // Simulate subscriptions changed, where testSub1 is no longer active
+ doReturn(createSubscriptionInfoList(new int[] {testSub2}))
+ .when(mSubscriptionManager).getActiveSubscriptionInfoList(anyBoolean());
+ mCallbackManagerUT.mSubChangedListener.onSubscriptionsChanged();
+ assertFalse(mCallbackManagerUT.doesCallbackExist(testCallback1));
+ // verify that the subscription listener is removed, since the only active callback has been
+ // removed.
+ verify(mSubscriptionManager, times(1)).removeOnSubscriptionsChangedListener(
+ any(SubscriptionManager.OnSubscriptionsChangedListener.class));
+ }
+
+ /**
+ * The close() method has been called, so al callbacks should be cleaned up and notified
+ * that they have been removed. The subscriptions changed listener should also be removed.
+ */
+ @Test
+ @SmallTest
+ public void testCallbackAdapter_closeMultipleSubs() throws Exception {
+ TestCallback testCallback1 = new TestCallback();
+ TestCallback testCallback2 = new TestCallback();
+ int testSub1 = 1;
+ int testSub2 = 2;
+ mCallbackManagerUT.addCallbackForSubscription(testCallback1, testSub1);
+ assertTrue(mCallbackManagerUT.doesCallbackExist(testCallback1));
+ mCallbackManagerUT.addCallbackForSubscription(testCallback2, testSub2);
+ assertTrue(mCallbackManagerUT.doesCallbackExist(testCallback2));
+ verify(mSubscriptionManager, times(1)).addOnSubscriptionsChangedListener(
+ any(SubscriptionManager.OnSubscriptionsChangedListener.class));
+
+ // Close the manager, ensure all subscription callbacks are removed
+ mCallbackManagerUT.close();
+ assertFalse(mCallbackManagerUT.doesCallbackExist(testCallback1));
+ assertFalse(mCallbackManagerUT.doesCallbackExist(testCallback2));
+ // verify that the subscription changed listener is removed.
+ verify(mSubscriptionManager, times(1)).removeOnSubscriptionsChangedListener(
+ any(SubscriptionManager.OnSubscriptionsChangedListener.class));
+ }
+
+ /**
+ * The close() method has been called, so all callbacks should be cleaned up. Since they are
+ * not associated with any subscriptions, no subscription based logic should be called.
+ */
+ @Test
+ @SmallTest
+ public void testCallbackAdapter_closeSlotBasedCallbacks() throws Exception {
+ TestCallback testCallback1 = new TestCallback();
+ TestCallback testCallback2 = new TestCallback();
+ mCallbackManagerUT.addCallback(testCallback1);
+ assertTrue(mCallbackManagerUT.doesCallbackExist(testCallback1));
+ mCallbackManagerUT.addCallback(testCallback2);
+ assertTrue(mCallbackManagerUT.doesCallbackExist(testCallback2));
+ // verify that the subscription changed listener is never called for these callbacks
+ // because they are not associated with any subscriptions.
+ verify(mSubscriptionManager, never()).addOnSubscriptionsChangedListener(
+ any(SubscriptionManager.OnSubscriptionsChangedListener.class));
+
+ // Close the manager, ensure all subscription callbacks are removed
+ mCallbackManagerUT.close();
+ assertFalse(mCallbackManagerUT.doesCallbackExist(testCallback1));
+ assertFalse(mCallbackManagerUT.doesCallbackExist(testCallback2));
+ // verify that the subscription changed removed method is never called
+ verify(mSubscriptionManager, never()).removeOnSubscriptionsChangedListener(
+ any(SubscriptionManager.OnSubscriptionsChangedListener.class));
+ }
+
+ private List<SubscriptionInfo> createSubscriptionInfoList(int[] subIds) {
+ List<SubscriptionInfo> infos = new ArrayList<>();
+ for (int i = 0; i < subIds.length; i++) {
+ SubscriptionInfo info = new SubscriptionInfo(subIds[i], null, -1, null, null, -1, -1,
+ null, -1, null, null, null, null, false, null, null);
+ infos.add(info);
+ }
+ return infos;
+ }
+}
diff --git a/tests/src/com/android/ims/rcs/uce/OWNERS b/tests/src/com/android/ims/rcs/uce/OWNERS
new file mode 100644
index 00000000..dff71c49
--- /dev/null
+++ b/tests/src/com/android/ims/rcs/uce/OWNERS
@@ -0,0 +1,3 @@
+jamescflin@google.com
+calvinpan@google.com
+allenwtsu@google.com \ No newline at end of file
diff --git a/tests/src/com/android/ims/rcs/uce/UceControllerTest.java b/tests/src/com/android/ims/rcs/uce/UceControllerTest.java
new file mode 100644
index 00000000..69d52811
--- /dev/null
+++ b/tests/src/com/android/ims/rcs/uce/UceControllerTest.java
@@ -0,0 +1,283 @@
+/*
+ * 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.ims.rcs.uce;
+
+import static org.junit.Assert.assertEquals;
+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.verify;
+
+import android.net.Uri;
+import android.telephony.ims.RcsUceAdapter;
+import android.telephony.ims.aidl.IOptionsRequestCallback;
+import android.telephony.ims.aidl.IRcsUceControllerCallback;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.ims.ImsTestBase;
+import com.android.ims.RcsFeatureManager;
+import com.android.ims.rcs.uce.eab.EabController;
+import com.android.ims.rcs.uce.options.OptionsController;
+import com.android.ims.rcs.uce.presence.publish.PublishController;
+import com.android.ims.rcs.uce.presence.subscribe.SubscribeController;
+import com.android.ims.rcs.uce.request.UceRequestManager;
+import com.android.ims.rcs.uce.UceDeviceState.DeviceStateResult;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+@RunWith(AndroidJUnit4.class)
+public class UceControllerTest extends ImsTestBase {
+
+ @Mock EabController mEabController;
+ @Mock PublishController mPublishController;
+ @Mock SubscribeController mSubscribeController;
+ @Mock OptionsController mOptionsController;
+ @Mock UceController.ControllerFactory mControllerFactory;
+
+ @Mock UceRequestManager mTaskManager;
+ @Mock UceController.RequestManagerFactory mTaskManagerFactory;
+
+ @Mock UceDeviceState mDeviceState;
+ @Mock DeviceStateResult mDeviceStateResult;
+ @Mock RcsFeatureManager mFeatureManager;
+ @Mock UceController.UceControllerCallback mCallback;
+ @Mock IRcsUceControllerCallback mCapabilitiesCallback;
+ @Mock IOptionsRequestCallback mOptionsRequestCallback;
+
+ private int mSubId = 1;
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ doReturn(mEabController).when(mControllerFactory).createEabController(any(), eq(mSubId),
+ any(), any());
+ doReturn(mPublishController).when(mControllerFactory).createPublishController(any(),
+ eq(mSubId), any(), any());
+ doReturn(mSubscribeController).when(mControllerFactory).createSubscribeController(any(),
+ eq(mSubId));
+ doReturn(mOptionsController).when(mControllerFactory).createOptionsController(any(),
+ eq(mSubId));
+ doReturn(mTaskManager).when(mTaskManagerFactory).createRequestManager(any(), eq(mSubId),
+ any(), any());
+ doReturn(mDeviceStateResult).when(mDeviceState).getCurrentState();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ @Test
+ @SmallTest
+ public void testRcsConnected() throws Exception {
+ UceController uceController = createUceController();
+
+ uceController.onRcsConnected(mFeatureManager);
+
+ verify(mEabController).onRcsConnected(mFeatureManager);
+ verify(mPublishController).onRcsConnected(mFeatureManager);
+ verify(mSubscribeController).onRcsConnected(mFeatureManager);
+ verify(mOptionsController).onRcsConnected(mFeatureManager);
+ verify(mFeatureManager).addCapabilityEventCallback(any());
+ }
+
+ @Test
+ @SmallTest
+ public void testRcsDisconnected() throws Exception {
+ UceController uceController = createUceController();
+ uceController.onRcsConnected(mFeatureManager);
+
+ uceController.onRcsDisconnected();
+
+ verify(mFeatureManager).removeCapabilityEventCallback(any());
+ verify(mEabController).onRcsDisconnected();
+ verify(mPublishController).onRcsDisconnected();
+ verify(mSubscribeController).onRcsDisconnected();
+ verify(mOptionsController).onRcsDisconnected();
+ }
+
+ @Test
+ @SmallTest
+ public void testOnDestroyed() throws Exception {
+ UceController uceController = createUceController();
+
+ uceController.onDestroy();
+
+ verify(mTaskManager).onDestroy();
+ verify(mEabController).onDestroy();
+ verify(mPublishController).onDestroy();
+ verify(mSubscribeController).onDestroy();
+ verify(mOptionsController).onDestroy();
+ }
+
+ @Test
+ @SmallTest
+ public void testRequestCapabilitiesWithRcsDisconnected() throws Exception {
+ UceController uceController = createUceController();
+ uceController.onRcsDisconnected();
+
+ List<Uri> uriList = new ArrayList<>();
+ uceController.requestCapabilities(uriList, mCapabilitiesCallback);
+
+ verify(mCapabilitiesCallback).onError(RcsUceAdapter.ERROR_GENERIC_FAILURE, 0L);
+ verify(mTaskManager, never()).sendCapabilityRequest(any(), eq(false), any());
+ }
+
+ @Test
+ @SmallTest
+ public void testRequestCapabilitiesWithForbidden() throws Exception {
+ UceController uceController = createUceController();
+ uceController.onRcsConnected(mFeatureManager);
+ doReturn(true).when(mDeviceStateResult).isRequestForbidden();
+ doReturn(Optional.of(RcsUceAdapter.ERROR_FORBIDDEN)).when(mDeviceStateResult)
+ .getErrorCode();
+
+ List<Uri> uriList = new ArrayList<>();
+ uriList.add(Uri.fromParts("sip", "test", null));
+ uceController.requestCapabilities(uriList, mCapabilitiesCallback);
+
+ verify(mCapabilitiesCallback).onError(RcsUceAdapter.ERROR_FORBIDDEN, 0L);
+ verify(mTaskManager, never()).sendCapabilityRequest(any(), eq(false), any());
+ }
+
+ @Test
+ @SmallTest
+ public void testRequestCapabilitiesWithRcsConnected() throws Exception {
+ UceController uceController = createUceController();
+ uceController.onRcsConnected(mFeatureManager);
+ doReturn(false).when(mDeviceStateResult).isRequestForbidden();
+
+ List<Uri> uriList = new ArrayList<>();
+ uriList.add(Uri.fromParts("sip", "test", null));
+ uceController.requestCapabilities(uriList, mCapabilitiesCallback);
+
+ verify(mTaskManager).sendCapabilityRequest(uriList, false, mCapabilitiesCallback);
+ }
+
+ @Test
+ @SmallTest
+ public void testRequestAvailabilityWithRcsDisconnected() throws Exception {
+ UceController uceController = createUceController();
+ uceController.onRcsDisconnected();
+
+ Uri contact = Uri.fromParts("sip", "test", null);
+ uceController.requestAvailability(contact, mCapabilitiesCallback);
+
+ verify(mCapabilitiesCallback).onError(RcsUceAdapter.ERROR_GENERIC_FAILURE, 0L);
+ verify(mTaskManager, never()).sendAvailabilityRequest(any(), any());
+ }
+
+ @Test
+ @SmallTest
+ public void testRequestAvailabilityWithForbidden() throws Exception {
+ UceController uceController = createUceController();
+ uceController.onRcsConnected(mFeatureManager);
+ doReturn(true).when(mDeviceStateResult).isRequestForbidden();
+ doReturn(Optional.of(RcsUceAdapter.ERROR_FORBIDDEN)).when(mDeviceStateResult)
+ .getErrorCode();
+
+ Uri contact = Uri.fromParts("sip", "test", null);
+ uceController.requestAvailability(contact, mCapabilitiesCallback);
+
+ verify(mCapabilitiesCallback).onError(RcsUceAdapter.ERROR_FORBIDDEN, 0L);
+ verify(mTaskManager, never()).sendCapabilityRequest(any(), eq(false), any());
+ }
+
+ @Test
+ @SmallTest
+ public void testRequestAvailabilityWithRcsConnected() throws Exception {
+ UceController uceController = createUceController();
+ uceController.onRcsConnected(mFeatureManager);
+ doReturn(false).when(mDeviceStateResult).isRequestForbidden();
+
+ Uri contact = Uri.fromParts("sip", "test", null);
+ uceController.requestAvailability(contact, mCapabilitiesCallback);
+
+ verify(mTaskManager).sendAvailabilityRequest(contact, mCapabilitiesCallback);
+ }
+
+ @Test
+ @SmallTest
+ public void TestRequestPublishCapabilitiesFromService() throws Exception {
+ UceController uceController = createUceController();
+
+ int triggerType = RcsUceAdapter.CAPABILITY_UPDATE_TRIGGER_MOVE_TO_WLAN;
+ uceController.onRequestPublishCapabilitiesFromService(triggerType);
+
+ verify(mPublishController).requestPublishCapabilitiesFromService(triggerType);
+ }
+
+ @Test
+ @SmallTest
+ public void testUnpublish() throws Exception {
+ UceController uceController = createUceController();
+
+ uceController.onUnpublish();
+
+ verify(mPublishController).onUnpublish();
+ }
+
+ @Test
+ @SmallTest
+ public void testRegisterPublishStateCallback() {
+ UceController uceController = createUceController();
+
+ uceController.registerPublishStateCallback(any());
+
+ verify(mPublishController).registerPublishStateCallback(any());
+ }
+
+ @Test
+ @SmallTest
+ public void unregisterPublishStateCallback() {
+ UceController uceController = createUceController();
+
+ uceController.unregisterPublishStateCallback(any());
+
+ verify(mPublishController).unregisterPublishStateCallback(any());
+ }
+
+ @Test
+ @SmallTest
+ public void testGetUcePublishState() {
+ UceController uceController = createUceController();
+
+ uceController.getUcePublishState();
+
+ verify(mPublishController).getUcePublishState();
+ }
+
+ private UceController createUceController() {
+ UceController uceController = new UceController(mContext, mSubId, mDeviceState,
+ mControllerFactory, mTaskManagerFactory);
+ uceController.setUceControllerCallback(mCallback);
+ return uceController;
+ }
+}
diff --git a/tests/src/com/android/ims/rcs/uce/eab/EabBulkCapabilityUpdaterTest.java b/tests/src/com/android/ims/rcs/uce/eab/EabBulkCapabilityUpdaterTest.java
new file mode 100644
index 00000000..61b04313
--- /dev/null
+++ b/tests/src/com/android/ims/rcs/uce/eab/EabBulkCapabilityUpdaterTest.java
@@ -0,0 +1,200 @@
+/*
+ * 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.ims.rcs.uce.eab;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyList;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import android.content.SharedPreferences;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.PersistableBundle;
+import android.telephony.CarrierConfigManager;
+import android.telephony.ims.ImsException;
+import android.telephony.ims.ImsManager;
+import android.telephony.ims.ImsRcsManager;
+import android.telephony.ims.RcsUceAdapter;
+import android.telephony.ims.aidl.IRcsUceControllerCallback;
+
+import com.android.ims.ImsTestBase;
+import com.android.ims.rcs.uce.UceController;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class EabBulkCapabilityUpdaterTest extends ImsTestBase {
+
+ private final int mSubId = 1;
+
+ private Handler mHandler;
+ private HandlerThread mHandlerThread;
+
+ @Mock
+ private UceController.UceControllerCallback mMockUceControllerCallback;
+ @Mock
+ private EabControllerImpl mMockEabControllerImpl;
+ @Mock
+ private ImsRcsManager mImsRcsManager;
+ @Mock
+ private RcsUceAdapter mRcsUceAdapter;
+ @Mock
+ private SharedPreferences mSharedPreferences;
+ @Mock
+ private SharedPreferences.Editor mSharedPreferencesEditor;
+ @Mock
+ private EabContactSyncController mEabContactSyncController;
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+
+ mHandlerThread = new HandlerThread("TestThread");
+ mHandlerThread.start();
+ mHandler = mHandlerThread.getThreadHandler();
+
+ doReturn(mSharedPreferences).when(mContext).getSharedPreferences(anyString(), anyInt());
+ doReturn(0L).when(mSharedPreferences).getLong(anyString(), anyInt());
+ doReturn(mSharedPreferencesEditor).when(mSharedPreferences).edit();
+ doReturn(mSharedPreferencesEditor).when(mSharedPreferencesEditor).putLong(anyString(),
+ anyLong());
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ super.tearDown();
+ mHandlerThread.quit();
+ }
+
+ @Test
+ public void testRefreshCapabilities() throws Exception {
+ // mock user settings
+ mockUceUserSettings(true);
+ mockBulkCapabilityCarrierConfig(true);
+ // mock expired contact list
+ List<Uri> expiredContactList = new ArrayList<>();
+ expiredContactList.add(Uri.parse("test"));
+ doReturn(expiredContactList)
+ .when(mEabContactSyncController)
+ .syncContactToEabProvider(any());
+
+ new EabBulkCapabilityUpdater(
+ mContext,
+ mSubId,
+ mMockEabControllerImpl,
+ mEabContactSyncController,
+ mMockUceControllerCallback,
+ mHandler);
+
+ waitHandlerThreadFinish();
+
+ verify(mMockUceControllerCallback).refreshCapabilities(
+ anyList(),
+ any(IRcsUceControllerCallback.class));
+ }
+
+ @Test
+ public void testUceSettingsDisabled() throws Exception {
+ // mock user settings
+ mockUceUserSettings(false);
+ mockBulkCapabilityCarrierConfig(true);
+ // mock expired contact list
+ List<Uri> expiredContactList = new ArrayList<>();
+ expiredContactList.add(Uri.parse("test"));
+ doReturn(expiredContactList)
+ .when(mEabContactSyncController)
+ .syncContactToEabProvider(any());
+
+ new EabBulkCapabilityUpdater(
+ mContext,
+ mSubId,
+ mMockEabControllerImpl,
+ mEabContactSyncController,
+ mMockUceControllerCallback,
+ mHandler);
+
+ waitHandlerThreadFinish();
+
+ verify(mMockUceControllerCallback, never()).refreshCapabilities(
+ any(),
+ any(IRcsUceControllerCallback.class));
+ }
+
+ @Test
+ public void testCarrierConfigDisabled() throws Exception {
+ // mock user settings
+ mockUceUserSettings(true);
+ mockBulkCapabilityCarrierConfig(false);
+ // mock expired contact list
+ List<Uri> expiredContactList = new ArrayList<>();
+ expiredContactList.add(Uri.parse("test"));
+ doReturn(expiredContactList)
+ .when(mEabContactSyncController)
+ .syncContactToEabProvider(any());
+
+ new EabBulkCapabilityUpdater(
+ mContext,
+ mSubId,
+ mMockEabControllerImpl,
+ mEabContactSyncController,
+ mMockUceControllerCallback,
+ mHandler);
+
+ waitHandlerThreadFinish();
+
+ verify(mMockUceControllerCallback, never()).refreshCapabilities(
+ anyList(),
+ any(IRcsUceControllerCallback.class));
+ }
+
+ private void mockBulkCapabilityCarrierConfig(boolean isEnabled) {
+ PersistableBundle persistableBundle = new PersistableBundle();
+ persistableBundle.putBoolean(
+ CarrierConfigManager.Ims.KEY_RCS_BULK_CAPABILITY_EXCHANGE_BOOL, isEnabled);
+ CarrierConfigManager carrierConfigManager =
+ mContext.getSystemService(CarrierConfigManager.class);
+ doReturn(persistableBundle).when(carrierConfigManager).getConfigForSubId(anyInt());
+ }
+
+ private void mockUceUserSettings(boolean isEnabled) throws ImsException {
+ // mock uce user settings
+ ImsManager imsManager = mContext.getSystemService(ImsManager.class);
+ doReturn(mImsRcsManager).when(imsManager).getImsRcsManager(eq(mSubId));
+ doReturn(mRcsUceAdapter).when(mImsRcsManager).getUceAdapter();
+ doReturn(isEnabled).when(mRcsUceAdapter).isUceSettingEnabled();
+ }
+
+ private void waitHandlerThreadFinish() throws Exception {
+ int retryTimes = 0;
+ do {
+ Thread.sleep(1000);
+ retryTimes++;
+ } while(mHandler.hasMessagesOrCallbacks() && retryTimes < 2);
+ }
+}
diff --git a/tests/src/com/android/ims/rcs/uce/eab/EabContactSyncControllerTest.java b/tests/src/com/android/ims/rcs/uce/eab/EabContactSyncControllerTest.java
new file mode 100644
index 00000000..0b70a92e
--- /dev/null
+++ b/tests/src/com/android/ims/rcs/uce/eab/EabContactSyncControllerTest.java
@@ -0,0 +1,277 @@
+/*
+ * 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.ims.rcs.uce.eab;
+
+import static android.provider.ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doReturn;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.content.SharedPreferences;
+import android.content.pm.ProviderInfo;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.net.Uri;
+import android.provider.ContactsContract;
+import android.test.mock.MockContentResolver;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.test.rule.provider.ProviderTestRule;
+
+import com.android.ims.ImsTestBase;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.Mock;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+public class EabContactSyncControllerTest extends ImsTestBase {
+ private static final String TAG = "EabContactDataSyncServiceTest";
+
+ FakeContactProvider mFakeContactProvider = new FakeContactProvider();
+
+ @Rule
+ public ProviderTestRule mProviderTestRule = new ProviderTestRule.Builder(
+ EabProvider.class, EabProvider.AUTHORITY).build();
+
+ @Mock private SharedPreferences mSharedPreferences;
+ @Mock private SharedPreferences.Editor mSharedPreferencesEditor;
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ MockContentResolver mockContentResolver =
+ (MockContentResolver) mProviderTestRule.getResolver();
+ ProviderInfo providerInfo = new ProviderInfo();
+ providerInfo.authority = ContactsContract.AUTHORITY;
+ mFakeContactProvider.attachInfo(mContext, providerInfo);
+ mockContentResolver.addProvider(providerInfo.authority, mFakeContactProvider);
+ doReturn("com.android.phone.tests").when(mContext).getPackageName();
+
+ doReturn(mProviderTestRule.getResolver()).when(mContext).getContentResolver();
+
+ doReturn(mSharedPreferences).when(mContext).getSharedPreferences(anyString(), anyInt());
+ doReturn(0L).when(mSharedPreferences).getLong(anyString(), anyInt());
+ doReturn(mSharedPreferencesEditor).when(mSharedPreferences).edit();
+ doReturn(mSharedPreferencesEditor).when(mSharedPreferencesEditor).putLong(anyString(),
+ anyLong());
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ super.tearDown();
+ mFakeContactProvider.clearData();
+ mContext.getContentResolver().delete(EabProvider.CONTACT_URI, null, null);
+ }
+
+ @Test
+ public void testContactDeletedCase() {
+ insertContactToEabProvider(1, 2, 3, "123456");
+ insertDeletedContactToContactProvider(1, 1);
+
+ new EabContactSyncController().syncContactToEabProvider(mContext);
+
+ Cursor result = mProviderTestRule.getResolver().query(
+ EabProvider.CONTACT_URI,
+ null,
+ null,
+ null);
+ assertEquals(0, result.getCount());
+ }
+
+ @Test
+ public void testMultipleContactsDeletedCase() {
+ // Insert 3 contacts in EabProvider
+ insertContactToEabProvider(1, 1, 1, "123456");
+ insertContactToEabProvider(2, 2, 2, "1234567");
+ insertContactToEabProvider(3, 3, 3, "12345678");
+ // Insert 2 deleted contacts
+ insertDeletedContactToContactProvider(1, 1);
+ insertDeletedContactToContactProvider(2, 1);
+ // Keep id:3 in contact provider
+ insertContactToContactProvider(3, 3, 3, "12345678");
+
+ new EabContactSyncController().syncContactToEabProvider(mContext);
+
+ // Make sure only 1 contact in Eab DB
+ Cursor result = mProviderTestRule.getResolver().query(
+ EabProvider.CONTACT_URI,
+ null,
+ null,
+ null);
+ assertEquals(1, result.getCount());
+ }
+
+ @Test
+ public void testPhoneNumberDeletedCase() {
+ insertContactToEabProvider(1, 1, 2, "123456");
+ insertContactToEabProvider(1, 1, 3, "1234567");
+ insertContactToEabProvider(1, 1, 4, "12345678");
+ // Delete phone number 12345678
+ insertContactToContactProvider(1, 1, 2, "123456");
+ insertContactToContactProvider(1, 1, 3, "1234567");
+
+ new EabContactSyncController().syncContactToEabProvider(mContext);
+
+ Cursor result = mProviderTestRule.getResolver().query(
+ EabProvider.CONTACT_URI,
+ null,
+ null,
+ null);
+ assertEquals(2, result.getCount());
+ }
+
+ @Test
+ public void testPhoneNumberUpdatedCase() {
+ insertContactToEabProvider(1, 1, 2, "123456");
+ insertContactToEabProvider(1, 1, 3, "1234567");
+ insertContactToEabProvider(1, 1, 4, "12345678");
+ // Update phone number to 1,2,3
+ insertContactToContactProvider(1, 1, 2, "1");
+ insertContactToContactProvider(1, 1, 3, "2");
+ insertContactToContactProvider(1, 1, 4, "3");
+
+ new EabContactSyncController().syncContactToEabProvider(mContext);
+
+ Cursor result = mProviderTestRule.getResolver().query(
+ EabProvider.CONTACT_URI,
+ null,
+ null,
+ null,
+ EabProvider.ContactColumns.DATA_ID);
+ result.moveToFirst();
+ assertEquals(3, result.getCount());
+ assertEquals("1",
+ result.getString(result.getColumnIndex(EabProvider.ContactColumns.PHONE_NUMBER)));
+ result.moveToNext();
+ assertEquals("2",
+ result.getString(result.getColumnIndex(EabProvider.ContactColumns.PHONE_NUMBER)));
+ result.moveToNext();
+ assertEquals("3",
+ result.getString(result.getColumnIndex(EabProvider.ContactColumns.PHONE_NUMBER)));
+ }
+
+ private void insertDeletedContactToContactProvider(int contactId, int timestamp) {
+ ContentValues values = new ContentValues();
+ values.put(ContactsContract.DeletedContacts.CONTACT_ID, contactId);
+ values.put(ContactsContract.DeletedContacts.CONTACT_DELETED_TIMESTAMP, timestamp);
+ mContext.getContentResolver().insert(
+ ContactsContract.DeletedContacts.CONTENT_URI, values);
+ }
+
+ private void insertContactToContactProvider(
+ int contactId, int rawContactId, int dataId, String number) {
+ ContentValues values = new ContentValues();
+ values.put(ContactsContract.Data._ID, dataId);
+ values.put(EabProvider.ContactColumns.CONTACT_ID, contactId);
+ values.put(ContactsContract.CommonDataKinds.Phone.RAW_CONTACT_ID, rawContactId);
+ values.put(ContactsContract.Data.MIMETYPE, CONTENT_ITEM_TYPE);
+ values.put(ContactsContract.CommonDataKinds.Phone.NUMBER, number);
+ values.put(ContactsContract.CommonDataKinds.Phone.CONTACT_LAST_UPDATED_TIMESTAMP, 1);
+
+ mContext.getContentResolver().insert(ContactsContract.Data.CONTENT_URI, values);
+ }
+
+ private void insertContactToEabProvider(int contactId,
+ int rawContactId, int dataId, String phoneNumber) {
+ ContentValues values = new ContentValues();
+ values.put(EabProvider.ContactColumns.CONTACT_ID, contactId);
+ values.put(EabProvider.ContactColumns.RAW_CONTACT_ID, rawContactId);
+ values.put(EabProvider.ContactColumns.DATA_ID, dataId);
+ values.put(EabProvider.ContactColumns.PHONE_NUMBER, phoneNumber);
+ mContext.getContentResolver().insert(EabProvider.CONTACT_URI, values);
+ }
+
+ /**
+ * Create a fake contact provider that store ContentValues in hashmap when invoke insert()
+ * and convert to cursor when invoke query()
+ */
+ public static class FakeContactProvider extends ContentProvider {
+ private final HashMap<Uri, List<ContentValues>> mFakeProviderData = new HashMap<>();
+
+ public FakeContactProvider() {
+ }
+
+ public void clearData() {
+ mFakeProviderData.clear();
+ }
+
+ @Override
+ public boolean onCreate() {
+ return true;
+ }
+
+ @Override
+ public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+ String sortOrder) {
+ return convertContentValuesToCursor(mFakeProviderData.get(uri));
+ }
+
+ @Nullable
+ @Override
+ public String getType(@NonNull Uri uri) {
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
+ List<ContentValues> allDataList =
+ mFakeProviderData.computeIfAbsent(uri, k -> new ArrayList<>());
+ allDataList.add(new ContentValues(values));
+ return null;
+ }
+
+ @Override
+ public int delete(Uri uri, String selection, String[] selectionArgs) {
+ return 0;
+ }
+
+ @Override
+ public int update(@NonNull Uri uri, @Nullable ContentValues values,
+ @Nullable String selection, @Nullable String[] selectionArgs) {
+ return 0;
+ }
+
+ private Cursor convertContentValuesToCursor(List<ContentValues> valuesList) {
+ if (valuesList != null) {
+ MatrixCursor result =
+ new MatrixCursor(valuesList.get(0).keySet().toArray(new String[0]));
+ for (ContentValues contentValue : valuesList) {
+ MatrixCursor.RowBuilder builder = result.newRow();
+ for (String key : contentValue.keySet()) {
+ builder.add(key, contentValue.get(key));
+ }
+ }
+ return result;
+ } else {
+ return new MatrixCursor(new String[0]);
+ }
+ }
+ }
+}
diff --git a/tests/src/com/android/ims/rcs/uce/eab/EabControllerTest.java b/tests/src/com/android/ims/rcs/uce/eab/EabControllerTest.java
new file mode 100644
index 00000000..af52217d
--- /dev/null
+++ b/tests/src/com/android/ims/rcs/uce/eab/EabControllerTest.java
@@ -0,0 +1,346 @@
+/*
+ * 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.ims.rcs.uce.eab;
+
+import static android.telephony.CarrierConfigManager.Ims.KEY_NON_RCS_CAPABILITIES_CACHE_EXPIRATION_SEC_INT;
+import static android.telephony.ims.RcsContactUceCapability.CAPABILITY_MECHANISM_PRESENCE;
+import static android.telephony.ims.RcsContactUceCapability.REQUEST_RESULT_FOUND;
+import static android.telephony.ims.RcsContactUceCapability.REQUEST_RESULT_NOT_FOUND;
+import static android.telephony.ims.RcsContactUceCapability.SOURCE_TYPE_NETWORK;
+
+import static com.android.ims.rcs.uce.eab.EabProvider.COMMON_URI;
+import static com.android.ims.rcs.uce.eab.EabProvider.CONTACT_URI;
+import static com.android.ims.rcs.uce.eab.EabProvider.OPTIONS_URI;
+import static com.android.ims.rcs.uce.eab.EabProvider.PRESENCE_URI;
+
+import static org.junit.Assert.fail;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Looper;
+import android.os.PersistableBundle;
+import android.telephony.ims.RcsContactPresenceTuple;
+import android.telephony.ims.RcsContactUceCapability;
+import android.test.mock.MockContentResolver;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.ims.ImsTestBase;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.time.temporal.ChronoUnit;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+import java.util.List;
+import java.util.TimeZone;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+@RunWith(AndroidJUnit4.class)
+public class EabControllerTest extends ImsTestBase {
+ EabProviderTestable mEabProviderTestable = new EabProviderTestable();
+ EabControllerImpl mEabController;
+ PersistableBundle mBundle;
+ ExecutorService mExecutor = Executors.newSingleThreadExecutor();
+
+ private static final int TEST_SUB_ID = 1;
+ private static final String TEST_PHONE_NUMBER = "16661234567";
+ private static final String TEST_SERVICE_STATUS = "status";
+ private static final String TEST_SERVICE_SERVICE_ID = "serviceId";
+ private static final String TEST_SERVICE_VERSION = "version";
+ private static final String TEST_SERVICE_DESCRIPTION = "description";
+ private static final boolean TEST_AUDIO_CAPABLE = true;
+ private static final boolean TEST_VIDEO_CAPABLE = false;
+
+ private static final int TIME_OUT_IN_SEC = 5;
+ private static final Uri TEST_CONTACT_URI = Uri.parse(TEST_PHONE_NUMBER + "@android.test");
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ MockContentResolver mockContentResolver =
+ (MockContentResolver) mContext.getContentResolver();
+ mEabProviderTestable.initializeForTesting(mContext);
+ mockContentResolver.addProvider(EabProvider.AUTHORITY, mEabProviderTestable);
+
+ insertContactInfoToDB();
+ mEabController = new EabControllerImpl(
+ mContext, TEST_SUB_ID, null, Looper.getMainLooper());
+
+ mBundle = mContextFixture.getTestCarrierConfigBundle();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ @Test
+ @SmallTest
+ public void testGetAvailability() {
+ List<RcsContactUceCapability> contactList = new ArrayList<>();
+ contactList.add(createPresenceCapability(false));
+
+ mEabController.saveCapabilities(contactList);
+
+ EabCapabilityResult result = mEabController.getAvailability(TEST_CONTACT_URI);
+ Assert.assertEquals(EabCapabilityResult.EAB_QUERY_SUCCESSFUL, result.getStatus());
+ Assert.assertEquals(TEST_CONTACT_URI,
+ result.getContactCapabilities().getContactUri());
+ }
+
+ @Test
+ @SmallTest
+ public void testGetCapability() {
+ List<RcsContactUceCapability> contactList = new ArrayList<>();
+ contactList.add(createPresenceCapability(false));
+
+ mEabController.saveCapabilities(contactList);
+
+ List<Uri> contactUriList = new ArrayList<>();
+ contactUriList.add(TEST_CONTACT_URI);
+ Assert.assertEquals(1,
+ mEabController.getCapabilities(contactUriList).size());
+ Assert.assertEquals(EabCapabilityResult.EAB_QUERY_SUCCESSFUL,
+ mEabController.getCapabilities(contactUriList).get(0).getStatus());
+ }
+
+ @Test
+ @SmallTest
+ public void testGetExpiredCapability() {
+ List<RcsContactUceCapability> contactList = new ArrayList<>();
+ contactList.add(createPresenceCapability(true));
+
+ mEabController.saveCapabilities(contactList);
+
+ List<Uri> contactUriList = new ArrayList<>();
+ contactUriList.add(TEST_CONTACT_URI);
+ Assert.assertEquals(1,
+ mEabController.getCapabilities(contactUriList).size());
+ Assert.assertEquals(EabCapabilityResult.EAB_CONTACT_EXPIRED_FAILURE,
+ mEabController.getCapabilities(contactUriList).get(0).getStatus());
+ }
+
+ @Test
+ @SmallTest
+ public void testNonRcsCapability() {
+ // Set non-rcs capabilities expiration to 121 days
+ mBundle.putInt(KEY_NON_RCS_CAPABILITIES_CACHE_EXPIRATION_SEC_INT, 121 * 24 * 60 * 60);
+ // Set timestamp to 120 days age
+ GregorianCalendar date = new GregorianCalendar();
+ date.setTimeZone(TimeZone.getTimeZone("UTC"));
+ date.add(Calendar.DATE, -120);
+
+ List<RcsContactUceCapability> contactList = new ArrayList<>();
+ contactList.add(createPresenceNonRcsCapability(Instant.now()));
+
+ mEabController.saveCapabilities(contactList);
+
+ List<Uri> contactUriList = new ArrayList<>();
+ contactUriList.add(TEST_CONTACT_URI);
+
+ // Verify result is not expired
+ Assert.assertEquals(1,
+ mEabController.getCapabilities(contactUriList).size());
+ Assert.assertEquals(EabCapabilityResult.EAB_QUERY_SUCCESSFUL,
+ mEabController.getCapabilities(contactUriList).get(0).getStatus());
+ }
+
+ @Test
+ @SmallTest
+ public void testNonRcsCapabilityExpired() {
+ // Set non-rcs capabilities expiration to 119 days
+ mBundle.putInt(KEY_NON_RCS_CAPABILITIES_CACHE_EXPIRATION_SEC_INT, 119 * 24 * 60 * 60);
+ // Set timestamp to 120 days age
+ Instant timestamp = Instant.now().minus(120, ChronoUnit.DAYS);
+
+ List<RcsContactUceCapability> contactList = new ArrayList<>();
+ contactList.add(createPresenceNonRcsCapability(timestamp));
+ mEabController.saveCapabilities(contactList);
+
+ // Verify result is expired
+ List<Uri> contactUriList = new ArrayList<>();
+ contactUriList.add(TEST_CONTACT_URI);
+ Assert.assertEquals(1,
+ mEabController.getCapabilities(contactUriList).size());
+ Assert.assertEquals(EabCapabilityResult.EAB_CONTACT_EXPIRED_FAILURE,
+ mEabController.getCapabilities(contactUriList).get(0).getStatus());
+ }
+
+ @Test
+ @SmallTest
+ public void testCleanupInvalidDataInCommonTable() throws InterruptedException {
+ // Insert invalid data in common table
+ ContentValues data = new ContentValues();
+ data.put(EabProvider.EabCommonColumns.EAB_CONTACT_ID, -1);
+ data.put(EabProvider.EabCommonColumns.MECHANISM, CAPABILITY_MECHANISM_PRESENCE);
+ data.put(EabProvider.EabCommonColumns.REQUEST_RESULT, REQUEST_RESULT_FOUND);
+ data.put(EabProvider.EabCommonColumns.SUBSCRIPTION_ID, -1);
+ mContext.getContentResolver().insert(COMMON_URI, data);
+
+ mExecutor.execute(mEabController.mCapabilityCleanupRunnable);
+ mExecutor.awaitTermination(TIME_OUT_IN_SEC, TimeUnit.SECONDS);
+
+ // Verify the entry that cannot map to presence/option table has been removed
+ Cursor cursor = mContext.getContentResolver().query(COMMON_URI, null, null, null, null);
+ while(cursor.moveToNext()) {
+ int contactId = cursor.getInt(
+ cursor.getColumnIndex(EabProvider.EabCommonColumns.EAB_CONTACT_ID));
+ if (contactId == -1) {
+ fail("Invalid data didn't been cleared");
+ }
+ }
+ }
+
+ @Test
+ @SmallTest
+ public void testCleanupInvalidDataInPresenceTable() throws InterruptedException {
+ String expiredContact = "expiredContact";
+ GregorianCalendar expiredDate = new GregorianCalendar();
+ expiredDate.setTimeZone(TimeZone.getTimeZone("UTC"));
+ expiredDate.add(Calendar.DATE, -120);
+ // Insert invalid data in presence table
+ ContentValues data = new ContentValues();
+ data.put(EabProvider.EabCommonColumns.REQUEST_RESULT, REQUEST_RESULT_FOUND);
+ Uri commonUri = mContext.getContentResolver().insert(COMMON_URI, data);
+
+ data = new ContentValues();
+ data.put(EabProvider.PresenceTupleColumns.EAB_COMMON_ID, commonUri.getLastPathSegment());
+ data.put(EabProvider.PresenceTupleColumns.CONTACT_URI, expiredContact);
+ data.put(EabProvider.PresenceTupleColumns.REQUEST_TIMESTAMP,
+ expiredDate.getTime().getTime() / 1000);
+ mContext.getContentResolver().insert(PRESENCE_URI, data);
+
+ mExecutor.execute(mEabController.mCapabilityCleanupRunnable);
+ mExecutor.awaitTermination(TIME_OUT_IN_SEC, TimeUnit.SECONDS);
+
+ // Verify the invalid data has been removed after save capabilities
+ Cursor cursor = mContext.getContentResolver().query(PRESENCE_URI, null, null, null, null);
+ while(cursor.moveToNext()) {
+ String contactUri = cursor.getString(
+ cursor.getColumnIndex(EabProvider.PresenceTupleColumns.CONTACT_URI));
+ if (contactUri.equals(expiredContact)) {
+ fail("Invalid data didn't been cleared");
+ }
+ }
+ }
+
+ @Test
+ @SmallTest
+ public void testCleanupInvalidDataInOptionTable() throws InterruptedException {
+ String expiredFeatureTag = "expiredFeatureTag";
+ GregorianCalendar expiredDate = new GregorianCalendar();
+ expiredDate.setTimeZone(TimeZone.getTimeZone("UTC"));
+ expiredDate.add(Calendar.DATE, -120);
+ // Insert invalid data in presence table
+ ContentValues data = new ContentValues();
+ data.put(EabProvider.EabCommonColumns.REQUEST_RESULT, REQUEST_RESULT_NOT_FOUND);
+ Uri commonUri = mContext.getContentResolver().insert(COMMON_URI, data);
+
+ data = new ContentValues();
+ data.put(EabProvider.PresenceTupleColumns.EAB_COMMON_ID, commonUri.getLastPathSegment());
+ data.put(EabProvider.OptionsColumns.FEATURE_TAG, expiredFeatureTag);
+ data.put(EabProvider.OptionsColumns.REQUEST_TIMESTAMP,
+ expiredDate.getTime().getTime() / 1000);
+ mContext.getContentResolver().insert(OPTIONS_URI, data);
+
+ mExecutor.execute(mEabController.mCapabilityCleanupRunnable);
+ mExecutor.awaitTermination(TIME_OUT_IN_SEC, TimeUnit.SECONDS);
+
+ // Verify the invalid data has been removed after save capabilities
+ Cursor cursor = mContext.getContentResolver().query(OPTIONS_URI, null, null, null, null);
+ while(cursor.moveToNext()) {
+ String featureTag = cursor.getString(
+ cursor.getColumnIndex(EabProvider.OptionsColumns.FEATURE_TAG));
+ if (featureTag.equals(expiredFeatureTag)) {
+ fail("Invalid data didn't been cleared");
+ }
+ }
+ }
+
+ private RcsContactUceCapability createPresenceCapability(boolean isExpired) {
+ Instant timestamp;
+ if (isExpired) {
+ timestamp = Instant.now().minus(120, ChronoUnit.DAYS);
+ } else {
+ timestamp = Instant.now().plus(120, ChronoUnit.DAYS);
+ }
+
+ RcsContactPresenceTuple.ServiceCapabilities.Builder serviceCapabilitiesBuilder =
+ new RcsContactPresenceTuple.ServiceCapabilities.Builder(TEST_AUDIO_CAPABLE,
+ TEST_VIDEO_CAPABLE);
+ RcsContactPresenceTuple tupleWithServiceCapabilities =
+ new RcsContactPresenceTuple.Builder(TEST_SERVICE_STATUS, TEST_SERVICE_SERVICE_ID,
+ TEST_SERVICE_VERSION)
+ .setServiceDescription(TEST_SERVICE_DESCRIPTION)
+ .setContactUri(TEST_CONTACT_URI)
+ .setServiceCapabilities(serviceCapabilitiesBuilder.build())
+ .setTime(timestamp)
+ .build();
+
+ RcsContactPresenceTuple tupleWithEmptyServiceCapabilities =
+ new RcsContactPresenceTuple.Builder(TEST_SERVICE_STATUS, TEST_SERVICE_SERVICE_ID,
+ TEST_SERVICE_VERSION)
+ .setServiceDescription(TEST_SERVICE_DESCRIPTION)
+ .setContactUri(TEST_CONTACT_URI)
+ .setTime(timestamp)
+ .build();
+
+ RcsContactUceCapability.PresenceBuilder builder =
+ new RcsContactUceCapability.PresenceBuilder(
+ TEST_CONTACT_URI, SOURCE_TYPE_NETWORK, REQUEST_RESULT_FOUND);
+ builder.addCapabilityTuple(tupleWithServiceCapabilities);
+ builder.addCapabilityTuple(tupleWithEmptyServiceCapabilities);
+ return builder.build();
+ }
+
+ private RcsContactUceCapability createPresenceNonRcsCapability(Instant timestamp) {
+ RcsContactPresenceTuple.ServiceCapabilities.Builder serviceCapabilitiesBuilder =
+ new RcsContactPresenceTuple.ServiceCapabilities.Builder(false, false);
+ RcsContactPresenceTuple tupleWithServiceCapabilities =
+ new RcsContactPresenceTuple.Builder(TEST_SERVICE_STATUS, TEST_SERVICE_SERVICE_ID,
+ TEST_SERVICE_VERSION)
+ .setServiceDescription(TEST_SERVICE_DESCRIPTION)
+ .setContactUri(TEST_CONTACT_URI)
+ .setServiceCapabilities(serviceCapabilitiesBuilder.build())
+ .setTime(timestamp)
+ .build();
+
+ RcsContactUceCapability.PresenceBuilder builder =
+ new RcsContactUceCapability.PresenceBuilder(
+ TEST_CONTACT_URI, SOURCE_TYPE_NETWORK, REQUEST_RESULT_NOT_FOUND);
+ builder.addCapabilityTuple(tupleWithServiceCapabilities);
+ return builder.build();
+ }
+
+ private void insertContactInfoToDB() {
+ ContentValues data = new ContentValues();
+ data.put(EabProvider.ContactColumns.PHONE_NUMBER, TEST_PHONE_NUMBER);
+ data.put(EabProvider.ContactColumns.RAW_CONTACT_ID, 1);
+ mContext.getContentResolver().insert(CONTACT_URI, data);
+ }
+}
diff --git a/tests/src/com/android/ims/rcs/uce/eab/EabProviderTest.java b/tests/src/com/android/ims/rcs/uce/eab/EabProviderTest.java
new file mode 100644
index 00000000..3c22e0ed
--- /dev/null
+++ b/tests/src/com/android/ims/rcs/uce/eab/EabProviderTest.java
@@ -0,0 +1,338 @@
+/*
+ * 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.ims.rcs.uce.eab;
+
+import static android.telephony.ims.RcsContactUceCapability.CAPABILITY_MECHANISM_OPTIONS;
+import static android.telephony.ims.RcsContactUceCapability.CAPABILITY_MECHANISM_PRESENCE;
+import static android.telephony.ims.RcsContactUceCapability.REQUEST_RESULT_FOUND;
+
+import static com.android.ims.rcs.uce.eab.EabProvider.ALL_DATA_URI;
+import static com.android.ims.rcs.uce.eab.EabProvider.COMMON_URI;
+import static com.android.ims.rcs.uce.eab.EabProvider.CONTACT_URI;
+import static com.android.ims.rcs.uce.eab.EabProvider.OPTIONS_URI;
+import static com.android.ims.rcs.uce.eab.EabProvider.PRESENCE_URI;
+
+import static org.junit.Assert.assertEquals;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+import android.test.mock.MockContentResolver;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.ims.ImsTestBase;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class EabProviderTest extends ImsTestBase {
+ EabProviderTestable mEabProviderTestable = new EabProviderTestable();
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ MockContentResolver mockContentResolver =
+ (MockContentResolver) mContext.getContentResolver();
+ mEabProviderTestable.initializeForTesting(mContext);
+ mockContentResolver.addProvider(EabProvider.AUTHORITY, mEabProviderTestable);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ @Test
+ @SmallTest
+ public void testQueryContactInfo() {
+ ContentValues data = new ContentValues();
+ data.put(EabProvider.ContactColumns.PHONE_NUMBER, "123456");
+ data.put(EabProvider.ContactColumns.RAW_CONTACT_ID, 1);
+ mContext.getContentResolver().insert(CONTACT_URI, data);
+
+ Cursor cursor = mContext.getContentResolver().query(CONTACT_URI,
+ null,
+ null,
+ null,
+ null);
+
+ assertEquals(1, cursor.getCount());
+ }
+
+ @Test
+ @SmallTest
+ public void testContactIsUnique() {
+ ContentValues data = new ContentValues();
+ data.put(EabProvider.ContactColumns.PHONE_NUMBER, "123456");
+ mContext.getContentResolver().insert(CONTACT_URI, data);
+
+ data = new ContentValues();
+ data.put(EabProvider.ContactColumns.PHONE_NUMBER, "123456");
+ mContext.getContentResolver().insert(CONTACT_URI, data);
+
+ Cursor cursor = mContext.getContentResolver().query(CONTACT_URI,
+ null,
+ null,
+ null,
+ null);
+
+ assertEquals(1, cursor.getCount());
+ }
+
+ @Test
+ @SmallTest
+ public void testQueryCommonInfo() {
+ ContentValues data = new ContentValues();
+ data.put(EabProvider.EabCommonColumns.EAB_CONTACT_ID, 1);
+ data.put(EabProvider.EabCommonColumns.MECHANISM, CAPABILITY_MECHANISM_PRESENCE);
+ data.put(EabProvider.EabCommonColumns.REQUEST_RESULT, REQUEST_RESULT_FOUND);
+ mContext.getContentResolver().insert(COMMON_URI, data);
+
+ Cursor cursor = mContext.getContentResolver().query(COMMON_URI,
+ null,
+ null,
+ null,
+ null);
+
+ assertEquals(1, cursor.getCount());
+ }
+
+ @Test
+ @SmallTest
+ public void testCommonIsUnique() {
+ ContentValues data = new ContentValues();
+ data.put(EabProvider.EabCommonColumns.EAB_CONTACT_ID, 1);
+ data.put(EabProvider.EabCommonColumns.MECHANISM, CAPABILITY_MECHANISM_PRESENCE);
+ data.put(EabProvider.EabCommonColumns.REQUEST_RESULT, REQUEST_RESULT_FOUND);
+ mContext.getContentResolver().insert(COMMON_URI, data);
+
+ data = new ContentValues();
+ data.put(EabProvider.EabCommonColumns.EAB_CONTACT_ID, 1);
+ data.put(EabProvider.EabCommonColumns.MECHANISM, CAPABILITY_MECHANISM_PRESENCE);
+ data.put(EabProvider.EabCommonColumns.REQUEST_RESULT, REQUEST_RESULT_FOUND);
+ mContext.getContentResolver().insert(COMMON_URI, data);
+
+ Cursor cursor = mContext.getContentResolver().query(COMMON_URI,
+ null,
+ null,
+ null,
+ null);
+
+ assertEquals(1, cursor.getCount());
+ }
+
+ @Test
+ @SmallTest
+ public void testQueryPresentInfo() {
+ ContentValues data = new ContentValues();
+ data.put(EabProvider.PresenceTupleColumns.EAB_COMMON_ID, 1);
+ data.put(EabProvider.PresenceTupleColumns.AUDIO_CAPABLE, false);
+ data.put(EabProvider.PresenceTupleColumns.VIDEO_CAPABLE, true);
+ mContext.getContentResolver().insert(PRESENCE_URI, data);
+
+ Cursor cursor = mContext.getContentResolver().query(PRESENCE_URI,
+ null,
+ null,
+ null,
+ null);
+
+ assertEquals(1, cursor.getCount());
+ }
+
+ @Test
+ @SmallTest
+ public void testPresentTupleIsNotUnique() {
+ ContentValues data = new ContentValues();
+ data.put(EabProvider.PresenceTupleColumns.EAB_COMMON_ID, 1);
+ data.put(EabProvider.PresenceTupleColumns.AUDIO_CAPABLE, false);
+ data.put(EabProvider.PresenceTupleColumns.VIDEO_CAPABLE, true);
+ mContext.getContentResolver().insert(PRESENCE_URI, data);
+
+ data = new ContentValues();
+ data.put(EabProvider.PresenceTupleColumns.EAB_COMMON_ID, 1);
+ data.put(EabProvider.PresenceTupleColumns.SERVICE_ID, "Android is the best.");
+ data.put(EabProvider.PresenceTupleColumns.SERVICE_VERSION, "Android is the best.");
+ mContext.getContentResolver().insert(PRESENCE_URI, data);
+
+ Cursor cursor = mContext.getContentResolver().query(PRESENCE_URI,
+ null,
+ null,
+ null,
+ null);
+
+ assertEquals(2, cursor.getCount());
+ }
+
+ @Test
+ @SmallTest
+ public void testQueryOptionInfo() {
+ ContentValues data = new ContentValues();
+ data.put(EabProvider.OptionsColumns.EAB_COMMON_ID, 1);
+ data.put(EabProvider.OptionsColumns.FEATURE_TAG, "Android is the best.");
+ mContext.getContentResolver().insert(OPTIONS_URI, data);
+
+ Cursor cursor = mContext.getContentResolver().query(OPTIONS_URI,
+ null,
+ null,
+ null,
+ null);
+
+ assertEquals(1, cursor.getCount());
+ }
+
+ @Test
+ @SmallTest
+ public void testOptionIsNotUnique() {
+ ContentValues data = new ContentValues();
+ data.put(EabProvider.OptionsColumns.EAB_COMMON_ID, 1);
+ data.put(EabProvider.OptionsColumns.FEATURE_TAG, "Android is the best.");
+ mContext.getContentResolver().insert(OPTIONS_URI, data);
+
+ data = new ContentValues();
+ data.put(EabProvider.OptionsColumns.EAB_COMMON_ID, 1);
+ data.put(EabProvider.OptionsColumns.FEATURE_TAG, "Android is the best!");
+ mContext.getContentResolver().insert(OPTIONS_URI, data);
+
+ Cursor cursor = mContext.getContentResolver().query(OPTIONS_URI,
+ null,
+ null,
+ null,
+ null);
+
+ assertEquals(2, cursor.getCount());
+ }
+
+
+ @Test
+ @SmallTest
+ public void testQueryByAllDataURI() {
+
+ ContentValues data = new ContentValues();
+ data.put(EabProvider.ContactColumns._ID, 1);
+ data.put(EabProvider.ContactColumns.PHONE_NUMBER, "123456");
+ data.put(EabProvider.ContactColumns.RAW_CONTACT_ID, 1);
+ mContext.getContentResolver().insert(CONTACT_URI, data);
+
+ data = new ContentValues();
+ data.put(EabProvider.EabCommonColumns.EAB_CONTACT_ID, 1);
+ data.put(EabProvider.EabCommonColumns.MECHANISM, CAPABILITY_MECHANISM_PRESENCE);
+ data.put(EabProvider.EabCommonColumns.REQUEST_RESULT, REQUEST_RESULT_FOUND);
+ mContext.getContentResolver().insert(COMMON_URI, data);
+
+ data = new ContentValues();
+ data.put(EabProvider.PresenceTupleColumns.EAB_COMMON_ID, 1);
+ data.put(EabProvider.PresenceTupleColumns.AUDIO_CAPABLE, false);
+ data.put(EabProvider.PresenceTupleColumns.VIDEO_CAPABLE, true);
+ mContext.getContentResolver().insert(PRESENCE_URI, data);
+
+ Cursor cursor = mContext.getContentResolver().query(ALL_DATA_URI,
+ null,
+ null,
+ null,
+ null);
+
+ assertEquals(1, cursor.getCount());
+ }
+
+ @Test
+ @SmallTest
+ public void testQueryBySubIdAndPhoneNumber() {
+ int subid = 1;
+ int incorrectSubid = 2;
+
+ // Insert a contact that request by presence
+ ContentValues data = new ContentValues();
+ data.put(EabProvider.ContactColumns._ID, 1);
+ data.put(EabProvider.ContactColumns.PHONE_NUMBER, "123456");
+ data.put(EabProvider.ContactColumns.RAW_CONTACT_ID, 1);
+ mContext.getContentResolver().insert(CONTACT_URI, data);
+
+ data = new ContentValues();
+ data.put(EabProvider.EabCommonColumns.EAB_CONTACT_ID, 1);
+ data.put(EabProvider.EabCommonColumns.MECHANISM, CAPABILITY_MECHANISM_PRESENCE);
+ data.put(EabProvider.EabCommonColumns.REQUEST_RESULT, REQUEST_RESULT_FOUND);
+ data.put(EabProvider.EabCommonColumns.SUBSCRIPTION_ID, subid);
+ mContext.getContentResolver().insert(COMMON_URI, data);
+
+ data = new ContentValues();
+ data.put(EabProvider.PresenceTupleColumns.EAB_COMMON_ID, 1);
+ data.put(EabProvider.PresenceTupleColumns.AUDIO_CAPABLE, false);
+ data.put(EabProvider.PresenceTupleColumns.VIDEO_CAPABLE, true);
+ mContext.getContentResolver().insert(PRESENCE_URI, data);
+
+ // Insert a contact that request by option
+ data = new ContentValues();
+ data.put(EabProvider.ContactColumns._ID, 2);
+ data.put(EabProvider.ContactColumns.PHONE_NUMBER, "654321");
+ data.put(EabProvider.ContactColumns.RAW_CONTACT_ID, 2);
+ mContext.getContentResolver().insert(CONTACT_URI, data);
+
+ data = new ContentValues();
+ data.put(EabProvider.EabCommonColumns.EAB_CONTACT_ID, 2);
+ data.put(EabProvider.EabCommonColumns.SUBSCRIPTION_ID, incorrectSubid);
+ data.put(EabProvider.EabCommonColumns.MECHANISM, CAPABILITY_MECHANISM_OPTIONS);
+ data.put(EabProvider.EabCommonColumns.REQUEST_RESULT, REQUEST_RESULT_FOUND);
+ mContext.getContentResolver().insert(COMMON_URI, data);
+
+ data = new ContentValues();
+ data.put(EabProvider.OptionsColumns.EAB_COMMON_ID, 2);
+ data.put(EabProvider.OptionsColumns.FEATURE_TAG, "Android is the best.");
+ mContext.getContentResolver().insert(OPTIONS_URI, data);
+
+ Uri testUri = Uri.withAppendedPath(
+ Uri.withAppendedPath(ALL_DATA_URI, String.valueOf(1)), "123456");
+ Cursor cursor = mContext.getContentResolver().query(testUri,
+ null,
+ null,
+ null,
+ null);
+
+ assertEquals(1, cursor.getCount());
+ cursor.moveToFirst();
+ assertEquals(1, cursor.getInt(cursor.getColumnIndex(
+ EabProvider.PresenceTupleColumns.VIDEO_CAPABLE)));
+ }
+
+ @Test
+ @SmallTest
+ public void testBulkInsert() {
+ ContentValues[] data = new ContentValues[2];
+ ContentValues insertData = new ContentValues();
+ insertData.put(EabProvider.ContactColumns._ID, 1);
+ insertData.put(EabProvider.ContactColumns.PHONE_NUMBER, "123456");
+ insertData.put(EabProvider.ContactColumns.RAW_CONTACT_ID, 1);
+ data[0] = insertData;
+ insertData = new ContentValues();
+ insertData.put(EabProvider.ContactColumns._ID, 2);
+ insertData.put(EabProvider.ContactColumns.PHONE_NUMBER, "1234567");
+ insertData.put(EabProvider.ContactColumns.RAW_CONTACT_ID, 2);
+ data[1] = insertData;
+
+ mContext.getContentResolver().bulkInsert(CONTACT_URI, data);
+
+ Cursor cursor = mContext.getContentResolver().query(CONTACT_URI,
+ null,
+ null,
+ null,
+ null);
+ assertEquals(2, cursor.getCount());
+ }
+}
diff --git a/tests/src/com/android/ims/rcs/uce/eab/EabProviderTestable.java b/tests/src/com/android/ims/rcs/uce/eab/EabProviderTestable.java
new file mode 100644
index 00000000..79ebd549
--- /dev/null
+++ b/tests/src/com/android/ims/rcs/uce/eab/EabProviderTestable.java
@@ -0,0 +1,105 @@
+/*
+ * 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.ims.rcs.uce.eab;
+
+import static com.android.ims.rcs.uce.eab.EabProvider.EabDatabaseHelper.SQL_CREATE_COMMON_TABLE;
+import static com.android.ims.rcs.uce.eab.EabProvider.EabDatabaseHelper.SQL_CREATE_CONTACT_TABLE;
+import static com.android.ims.rcs.uce.eab.EabProvider.EabDatabaseHelper.SQL_CREATE_OPTIONS_TABLE;
+import static com.android.ims.rcs.uce.eab.EabProvider.EabDatabaseHelper.SQL_CREATE_PRESENCE_TUPLE_TABLE;
+
+import android.content.Context;
+import android.content.pm.ProviderInfo;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.net.Uri;
+import android.util.Log;
+
+public class EabProviderTestable extends EabProvider {
+ private static final String TAG = EabProviderTestable.class.getSimpleName();
+
+ private InMemoryEabProviderDbHelper mDbHelper;
+
+ @Override
+ public boolean onCreate() {
+ Log.d(TAG, "onCreate called");
+ mDbHelper = new InMemoryEabProviderDbHelper();
+ return true;
+ }
+
+ // close mDbHelper database object
+ protected void closeDatabase() {
+ mDbHelper.close();
+ }
+
+ void initializeForTesting(Context context) {
+ ProviderInfo providerInfo = new ProviderInfo();
+ providerInfo.authority = EabProvider.AUTHORITY;
+
+ attachInfoForTesting(context, providerInfo);
+ }
+
+ @Override
+ public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+ String sortOrder) {
+ Cursor cursor = super.query(uri, projection, selection, selectionArgs, sortOrder);
+ Log.d(TAG, "InMemoryEabProviderDbHelper query" + DatabaseUtils.dumpCursorToString(cursor));
+ return cursor;
+ }
+
+ @Override
+ public SQLiteDatabase getReadableDatabase() {
+ Log.d(TAG, "getReadableDatabase called" + mDbHelper.getReadableDatabase());
+ return mDbHelper.getReadableDatabase();
+ }
+
+ @Override
+ public SQLiteDatabase getWritableDatabase() {
+ Log.d(TAG, "getWritableDatabase called" + mDbHelper.getWritableDatabase());
+ return mDbHelper.getWritableDatabase();
+ }
+
+ /**
+ * An in memory DB for EabProviderTestable to use
+ */
+ public static class InMemoryEabProviderDbHelper extends SQLiteOpenHelper {
+ public InMemoryEabProviderDbHelper() {
+ super(null, // no context is needed for in-memory db
+ null, // db file name is null for in-memory db
+ null, // CursorFactory is null by default
+ 1); // db version is no-op for tests
+ Log.d(TAG, "InMemoryEabProviderDbHelper creating in-memory database");
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ //set up the EAB table
+ Log.d(TAG, "InMemoryEabProviderDbHelper onCreate");
+ db.execSQL(SQL_CREATE_CONTACT_TABLE);
+ db.execSQL(SQL_CREATE_COMMON_TABLE);
+ db.execSQL(SQL_CREATE_PRESENCE_TUPLE_TABLE);
+ db.execSQL(SQL_CREATE_OPTIONS_TABLE);
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ Log.d(TAG, "InMemoryEabProviderDbHelper onUpgrade");
+ return;
+ }
+ }
+}
diff --git a/tests/src/com/android/ims/rcs/uce/presence/pidfparser/PidfParserTest.java b/tests/src/com/android/ims/rcs/uce/presence/pidfparser/PidfParserTest.java
new file mode 100644
index 00000000..f8038be1
--- /dev/null
+++ b/tests/src/com/android/ims/rcs/uce/presence/pidfparser/PidfParserTest.java
@@ -0,0 +1,500 @@
+/*
+ * 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.ims.rcs.uce.presence.pidfparser;
+
+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.junit.Assert.fail;
+
+import android.net.Uri;
+import android.telephony.ims.RcsContactPresenceTuple;
+import android.telephony.ims.RcsContactPresenceTuple.ServiceCapabilities;
+import android.telephony.ims.RcsContactUceCapability;
+import android.telephony.ims.RcsContactUceCapability.PresenceBuilder;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.ims.ImsTestBase;
+
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+@RunWith(AndroidJUnit4.class)
+public class PidfParserTest extends ImsTestBase {
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ @Test
+ @SmallTest
+ public void testConvertToPidf() throws Exception {
+ RcsContactUceCapability capability = getRcsContactUceCapability();
+
+ String pidfResult = PidfParser.convertToPidf(capability);
+
+ String contact = "<contact>sip:test</contact>";
+ String audioSupported = "<caps:audio>true</caps:audio>";
+ String videoSupported = "<caps:video>true</caps:video>";
+ String description = "<op:version>1.0</op:version>";
+ assertTrue(pidfResult.contains(contact));
+ assertTrue(pidfResult.contains(audioSupported));
+ assertTrue(pidfResult.contains(videoSupported));
+ assertTrue(pidfResult.contains(description));
+ }
+
+ @Test
+ @SmallTest
+ public void testConvertFromPidfToRcsContactUceCapability() throws Exception {
+ final String contact = "sip:+11234567890@test";
+ final String serviceId = "org.3gpp.urn:urn-7:3gpp-service.ims.icsi.mmtel";
+ final String serviceDescription = "MMTEL feature service";
+ final boolean isAudioSupported = true;
+ final boolean isVideoSupported = false;
+
+ // Create the first PIDF data
+ String pidfData = getPidfData(contact, serviceId, serviceDescription, isAudioSupported,
+ isVideoSupported);
+
+ // Convert to the class RcsContactUceCapability
+ RcsContactUceCapability capabilities = PidfParser.getRcsContactUceCapability(pidfData);
+ assertNotNull(capabilities);
+ assertEquals(Uri.parse(contact), capabilities.getContactUri());
+ assertEquals(RcsContactUceCapability.SOURCE_TYPE_NETWORK, capabilities.getSourceType());
+ assertEquals(RcsContactUceCapability.CAPABILITY_MECHANISM_PRESENCE,
+ capabilities.getCapabilityMechanism());
+
+ List<RcsContactPresenceTuple> presenceTupleList = capabilities.getCapabilityTuples();
+ assertNotNull(presenceTupleList);
+ assertEquals(1, presenceTupleList.size());
+
+ RcsContactPresenceTuple presenceTuple1 = presenceTupleList.get(0);
+ assertEquals(serviceId, presenceTuple1.getServiceId());
+ assertEquals("1.0", presenceTuple1.getServiceVersion());
+ assertEquals(serviceDescription, presenceTuple1.getServiceDescription());
+ assertEquals(Uri.parse(contact), presenceTuple1.getContactUri());
+ assertEquals("2001-01-01T01:00:00Z", presenceTuple1.getTime().toString());
+ assertTrue(presenceTuple1.getServiceCapabilities().isAudioCapable());
+ assertFalse(presenceTuple1.getServiceCapabilities().isVideoCapable());
+ }
+
+ @Test
+ @SmallTest
+ public void testConvertFromNewlineIncludedPidfToRcsContactUceCapability() throws Exception {
+ final String contact = "tel:+11234567890";
+
+ final RcsContactPresenceTuple.Builder tuple1Builder = new RcsContactPresenceTuple.Builder(
+ "open",
+ "org.3gpp.urn:urn-7:3gpp-application.ims.iari.rcse.dp",
+ "1.0");
+ tuple1Builder.setServiceDescription("DiscoveryPresence")
+ .setContactUri(Uri.parse(contact));
+
+ final RcsContactPresenceTuple.Builder tuple2Builder = new RcsContactPresenceTuple.Builder(
+ "open",
+ "org.openmobilealliance:StandaloneMsg",
+ "2.0");
+ tuple2Builder.setServiceDescription("StandaloneMsg")
+ .setContactUri(Uri.parse(contact));
+
+ final RcsContactPresenceTuple.Builder tuple3Builder = new RcsContactPresenceTuple.Builder(
+ "open",
+ "org.openmobilealliance:ChatSession",
+ "2.0");
+ tuple3Builder.setServiceDescription("Session Mode Messaging")
+ .setContactUri(Uri.parse(contact));
+
+ final RcsContactPresenceTuple.Builder tuple4Builder = new RcsContactPresenceTuple.Builder(
+ "open",
+ "org.openmobilealliance:File-Transfer",
+ "1.0");
+ tuple4Builder.setServiceDescription("File Transfer")
+ .setContactUri(Uri.parse(contact));
+
+ final RcsContactPresenceTuple.Builder tuple5Builder = new RcsContactPresenceTuple.Builder(
+ "open",
+ "org.3gpp.urn:urn-7:3gpp-service.ims.icsi.mmtel",
+ "1.0");
+ tuple5Builder.setServiceDescription("VoLTE service");
+ ServiceCapabilities.Builder capBuilder = new ServiceCapabilities.Builder(true, true);
+ tuple5Builder.setServiceCapabilities(capBuilder.build())
+ .setContactUri(Uri.parse(contact));
+
+ final List<RcsContactPresenceTuple> expectedTupleList = new ArrayList<>(5);
+ expectedTupleList.add(tuple1Builder.build());
+ expectedTupleList.add(tuple2Builder.build());
+ expectedTupleList.add(tuple3Builder.build());
+ expectedTupleList.add(tuple4Builder.build());
+ expectedTupleList.add(tuple5Builder.build());
+
+ // Create the newline included PIDF data
+ String pidfData = getPidfDataWithNewlineAndWhitespaceCharacters();
+
+ // Convert to the class RcsContactUceCapability
+ RcsContactUceCapability capabilities = PidfParser.getRcsContactUceCapability(pidfData);
+
+ assertNotNull(capabilities);
+ assertEquals(Uri.parse(contact), capabilities.getContactUri());
+ assertEquals(RcsContactUceCapability.SOURCE_TYPE_NETWORK, capabilities.getSourceType());
+ assertEquals(RcsContactUceCapability.CAPABILITY_MECHANISM_PRESENCE,
+ capabilities.getCapabilityMechanism());
+
+ List<RcsContactPresenceTuple> presenceTupleList = capabilities.getCapabilityTuples();
+ assertNotNull(presenceTupleList);
+ assertEquals(expectedTupleList.size(), presenceTupleList.size());
+
+ for(RcsContactPresenceTuple tuple : presenceTupleList) {
+ String serviceId = tuple.getServiceId();
+ RcsContactPresenceTuple expectedTuple = findTuple(serviceId, expectedTupleList);
+ if (expectedTuple == null) {
+ fail("The service ID is invalid");
+ }
+
+ assertEquals(expectedTuple.getStatus(), tuple.getStatus());
+ assertEquals(expectedTuple.getServiceVersion(), tuple.getServiceVersion());
+ assertEquals(expectedTuple.getServiceDescription(), tuple.getServiceDescription());
+ assertEquals(expectedTuple.getTime(), tuple.getTime());
+ assertEquals(expectedTuple.getContactUri(), tuple.getContactUri());
+
+ ServiceCapabilities expectedCap = expectedTuple.getServiceCapabilities();
+ ServiceCapabilities resultCap = tuple.getServiceCapabilities();
+ if (expectedCap != null) {
+ assertNotNull(resultCap);
+ assertEquals(expectedCap.isAudioCapable(), resultCap.isAudioCapable());
+ assertEquals(expectedCap.isVideoCapable(), resultCap.isVideoCapable());
+ } else {
+ assertNull(resultCap);
+ }
+ }
+ }
+
+ private RcsContactPresenceTuple findTuple(String serviceId,
+ List<RcsContactPresenceTuple> expectedTupleList) {
+ if (serviceId == null) {
+ return null;
+ }
+ for (RcsContactPresenceTuple tuple : expectedTupleList) {
+ if (serviceId.equalsIgnoreCase(tuple.getServiceId())) {
+ return tuple;
+ }
+ }
+ return null;
+ }
+
+ @Test
+ @SmallTest
+ public void testConvertToRcsContactUceCapabilityForMultipleTuples() throws Exception {
+ final String contact = "sip:+11234567890@test";
+ final String serviceId1 = "org.3gpp.urn:urn-7:3gpp-application.ims.iari.rcse.dp";
+ final String serviceDescription1 = "capabilities discovery";
+ final String serviceId2 = "org.3gpp.urn:urn-7:3gpp-service.ims.icsi.mmtel";
+ final String serviceDescription2 = "MMTEL feature service";
+ final boolean isAudioSupported = true;
+ final boolean isVideoSupported = false;
+
+ // Create the PIDF data
+ String pidfData = getPidfDataWithMultiTuples(contact, serviceId1, serviceDescription1,
+ serviceId2, serviceDescription2, isAudioSupported, isVideoSupported);
+
+ // Convert to the class RcsContactUceCapability
+ RcsContactUceCapability capabilities = PidfParser.getRcsContactUceCapability(pidfData);
+
+ assertNotNull(capabilities);
+ assertEquals(Uri.parse(contact), capabilities.getContactUri());
+
+ List<RcsContactPresenceTuple> presenceTupleList = capabilities.getCapabilityTuples();
+ assertNotNull(presenceTupleList);
+ assertEquals(2, presenceTupleList.size());
+
+ // Verify the first tuple information
+ RcsContactPresenceTuple presenceTuple1 = presenceTupleList.get(0);
+ assertEquals(serviceId1, presenceTuple1.getServiceId());
+ assertEquals("1.0", presenceTuple1.getServiceVersion());
+ assertEquals(serviceDescription1, presenceTuple1.getServiceDescription());
+ assertEquals(Uri.parse(contact), presenceTuple1.getContactUri());
+ assertEquals("2001-01-01T01:00:00Z", presenceTuple1.getTime().toString());
+ assertNull(presenceTuple1.getServiceCapabilities());
+
+ // Verify the second tuple information
+ RcsContactPresenceTuple presenceTuple2 = presenceTupleList.get(1);
+ assertEquals(serviceId2, presenceTuple2.getServiceId());
+ assertEquals("1.0", presenceTuple2.getServiceVersion());
+ assertTrue(presenceTuple2.getServiceCapabilities().isAudioCapable());
+ assertFalse(presenceTuple2.getServiceCapabilities().isVideoCapable());
+ assertEquals(serviceDescription2, presenceTuple2.getServiceDescription());
+ assertEquals(Uri.parse(contact), presenceTuple2.getContactUri());
+ assertEquals("2001-02-02T01:00:00Z", presenceTuple2.getTime().toString());
+ assertNotNull(presenceTuple2.getServiceCapabilities());
+ assertEquals(isAudioSupported, presenceTuple2.getServiceCapabilities().isAudioCapable());
+ assertEquals(isVideoSupported, presenceTuple2.getServiceCapabilities().isVideoCapable());
+ }
+
+ @Test
+ @SmallTest
+ public void testConversionAndRestoration() throws Exception {
+ // Create the capability
+ final RcsContactUceCapability capability = getRcsContactUceCapability();
+
+ // Convert the capability to the pidf
+ final String pidf = PidfParser.convertToPidf(capability);
+
+ // Restore to the RcsContactUceCapability from the pidf
+ final RcsContactUceCapability restoredCapability =
+ PidfParser.getRcsContactUceCapability(pidf);
+
+ assertEquals(capability.getContactUri(), restoredCapability.getContactUri());
+ assertEquals(capability.getCapabilityMechanism(),
+ restoredCapability.getCapabilityMechanism());
+ assertEquals(capability.getSourceType(), restoredCapability.getSourceType());
+
+ // Assert all the tuples are equal
+ List<RcsContactPresenceTuple> originalTuples = capability.getCapabilityTuples();
+ List<RcsContactPresenceTuple> restoredTuples = restoredCapability.getCapabilityTuples();
+
+ assertNotNull(restoredTuples);
+ assertEquals(originalTuples.size(), restoredTuples.size());
+
+ for (int i = 0; i < originalTuples.size(); i++) {
+ RcsContactPresenceTuple tuple = originalTuples.get(i);
+ RcsContactPresenceTuple restoredTuple = restoredTuples.get(i);
+
+ assertEquals(tuple.getContactUri(), restoredTuple.getContactUri());
+ assertEquals(tuple.getStatus(), restoredTuple.getStatus());
+ assertEquals(tuple.getServiceId(), restoredTuple.getServiceId());
+ assertEquals(tuple.getServiceVersion(), restoredTuple.getServiceVersion());
+ assertEquals(tuple.getServiceDescription(), restoredTuple.getServiceDescription());
+
+ boolean isAudioCapable = false;
+ boolean isVideoCapable = false;
+ boolean isRestoredAudioCapable = false;
+ boolean isRestoredVideoCapable = false;
+
+ ServiceCapabilities servCaps = tuple.getServiceCapabilities();
+ if (servCaps != null) {
+ isAudioCapable = servCaps.isAudioCapable();
+ isVideoCapable = servCaps.isVideoCapable();
+ }
+
+ ServiceCapabilities restoredServCaps = restoredTuple.getServiceCapabilities();
+ if (restoredServCaps != null) {
+ isRestoredAudioCapable = restoredServCaps.isAudioCapable();
+ isRestoredVideoCapable = restoredServCaps.isVideoCapable();
+ }
+
+ assertEquals(isAudioCapable, isRestoredAudioCapable);
+ assertEquals(isVideoCapable, isRestoredVideoCapable);
+ }
+ }
+
+ private String getPidfData(String contact, String serviceId, String serviceDescription,
+ boolean isAudioSupported, boolean isVideoSupported) {
+ StringBuilder pidfBuilder = new StringBuilder();
+ pidfBuilder.append("<?xml version='1.0' encoding='utf-8' standalone='yes' ?>")
+ .append("<presence entity=\"" + contact + "\"")
+ .append(" xmlns=\"urn:ietf:params:xml:ns:pidf\"")
+ .append(" xmlns:op=\"urn:oma:xml:prs:pidf:oma-pres\"")
+ .append(" xmlns:op=\"urn:oma:xml:prs:pidf:oma-pres\"")
+ .append(" xmlns:caps=\"urn:ietf:params:xml:ns:pidf:caps\">")
+ // tuple data
+ .append("<tuple id=\"tid0\">")
+ .append("<status><basic>open</basic></status>")
+ .append("<op:service-description>")
+ .append("<op:service-id>").append(serviceId).append("</op:service-id>")
+ .append("<op:version>1.0</op:version>")
+ .append("<op:description>").append(serviceDescription).append("</op:description>")
+ .append("</op:service-description>")
+ // is audio supported
+ .append("<caps:servcaps>")
+ .append("<caps:audio>").append(isAudioSupported).append("</caps:audio>")
+ // is video supported
+ .append("<caps:video>").append(isVideoSupported).append("</caps:video>")
+ .append("</caps:servcaps>")
+ .append("<contact>").append(contact).append("</contact>")
+ .append("<timestamp>2001-01-01T01:00:00.00Z</timestamp>")
+ .append("</tuple></presence>");
+ return pidfBuilder.toString();
+ }
+
+ private String getPidfDataWithNewlineAndWhitespaceCharacters() {
+ String pidf = "<presence xmlns=\"urn:ietf:params:xml:ns:pidf\" "
+ + "xmlns:op=\"urn:oma:xml:prs:pidf:oma-pres\" "
+ + "xmlns:b=\"urn:ietf:params:xml:ns:pidf:caps\" "
+ + "entity=\"tel:+11234567890\">\n"
+ // Tuple: Discovery
+ + " <tuple id=\"DiscoveryPres\">\n\t"
+ + " <status>\n\t"
+ + " <basic>open</basic>\n\t"
+ + " </status>\n\t"
+ + " <op:service-description>\n\t"
+ + " <op:service-id>org.3gpp.urn:urn-7:3gpp-application.ims.iari.rcse.dp"
+ + "</op:service-id>\n\t"
+ + " <op:version>1.0</op:version>\n\t"
+ + " <op:description>DiscoveryPresence</op:description>\n\t"
+ + " </op:service-description>\n\t"
+ + " <contact>tel:+11234567890</contact>\n\t"
+ + " </tuple>\n\t"
+ // Tuple: VoLTE
+ + " <tuple id=\"VoLTE\">\n"
+ + " <status>\n"
+ + " <basic>open</basic>\n"
+ + " </status>\n"
+ + " <b:servcaps>\n"
+ + " <b:audio>true</b:audio>\n"
+ + " <b:video>true</b:video>\n"
+ + " <b:duplex>\n"
+ + " <b:supported>\n"
+ + " <b:full/>\n"
+ + " </b:supported>\n"
+ + " </b:duplex>\n"
+ + " </b:servcaps>\n"
+ + " <op:service-description>\n"
+ + " <op:service-id>org.3gpp.urn:urn-7:3gpp-service.ims.icsi.mmtel"
+ + "</op:service-id>\n"
+ + " <op:version>1.0</op:version>\n"
+ + " <op:description>VoLTE service</op:description>\n"
+ + " </op:service-description>\n"
+ + " <contact>tel:+11234567890</contact>\n"
+ + " </tuple>\n"
+ // Tuple: Standalone Message
+ + " <tuple id=\"StandaloneMsg\">\n"
+ + " <status>\n"
+ + " <basic>open</basic>\n"
+ + " </status>\n"
+ + " <op:service-description>\n"
+ + " <op:service-id>org.openmobilealliance:StandaloneMsg</op:service-id>\n"
+ + " <op:version>2.0</op:version>\n"
+ + " <op:description>StandaloneMsg</op:description>\n"
+ + " </op:service-description>\n"
+ + " <contact>tel:+11234567890</contact>\n"
+ + " </tuple>\n"
+ // Tuple: Session Mode Message
+ + " <tuple id=\"SessModeMessa\">\n"
+ + " <status>\n"
+ + " <basic>open</basic>\n"
+ + " </status>\n"
+ + " <op:service-description>\n"
+ + " <op:service-id>org.openmobilealliance:ChatSession</op:service-id>\n"
+ + " <op:version>2.0</op:version>\n"
+ + " <op:description>Session Mode Messaging</op:description>\n"
+ + " </op:service-description>\n"
+ + " <contact>tel:+11234567890</contact>\n"
+ + " </tuple>\n"
+ // Tuple: File Transfer
+ + " <tuple id=\"FileTransfer\">\n"
+ + " <status>\n"
+ + " <basic>open</basic>\n"
+ + " </status>\n"
+ + " <op:service-description>\n"
+ + " <op:service-id>org.openmobilealliance:File-Transfer</op:service-id>\n"
+ + " <op:version>1.0</op:version>\n"
+ + " <op:description>File Transfer</op:description>\n"
+ + " </op:service-description>\n"
+ + " <contact>tel:+11234567890</contact>\n"
+ + " </tuple>\n"
+ + " </presence>";
+
+ return pidf;
+ }
+
+ private String getPidfDataWithMultiTuples(String contact, String serviceId1,
+ String serviceDescription1, String serviceId2, String serviceDescription2,
+ boolean audioSupported, boolean videoSupported) {
+ return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
+ + "<presence xmlns=\"urn:ietf:params:xml:ns:pidf\""
+ + " xmlns:op=\"urn:oma:xml:prs:pidf:oma-pres\""
+ + " xmlns:caps=\"urn:ietf:params:xml:ns:pidf:caps\""
+ + " entity=\"" + contact + "\">"
+ // tuple 1
+ + "<tuple id=\"a0\">"
+ + "<status><basic>open</basic></status>"
+ + "<op:service-description>"
+ + "<op:service-id>" + serviceId1 + "</op:service-id>"
+ + "<op:version>1.0</op:version>"
+ + "<op:description>" + serviceDescription1 + "</op:description>"
+ + "</op:service-description>"
+ + "<contact>" + contact + "</contact>"
+ + "<timestamp>2001-01-01T01:00:00.00Z</timestamp>"
+ + "</tuple>"
+ // tuple 2
+ + "<tuple id=\"a1\">"
+ + "<status><basic>open</basic></status>"
+ + "<op:service-description>"
+ + "<op:service-id>" + serviceId2 + "</op:service-id>"
+ + "<op:version>1.0</op:version>"
+ + "<op:description>" + serviceDescription2 + "</op:description>"
+ + "</op:service-description>"
+ + "<caps:servcaps>"
+ + "<caps:audio>" + audioSupported + "</caps:audio>"
+ + "<caps:duplex>"
+ + "<caps:supported><caps:full></caps:full></caps:supported>"
+ + "</caps:duplex>"
+ + "<caps:video>" + videoSupported + "</caps:video>"
+ + "</caps:servcaps>"
+ + "<contact>" + contact + "</contact>"
+ + "<timestamp>2001-02-02T01:00:00.00Z</timestamp>"
+ + "</tuple>"
+ + "</presence>";
+ }
+
+ private RcsContactUceCapability getRcsContactUceCapability() {
+ final Uri contact = Uri.fromParts("sip", "test", null);
+ final boolean isAudioCapable = true;
+ final boolean isVideoCapable = true;
+ final String duplexMode = ServiceCapabilities.DUPLEX_MODE_FULL;
+ final String basicStatus = RcsContactPresenceTuple.TUPLE_BASIC_STATUS_OPEN;
+ final String version = "1.0";
+ final String description = "description test";
+ final Instant nowTime = Instant.now();
+
+ // init the capabilities
+ ServiceCapabilities.Builder servCapsBuilder =
+ new ServiceCapabilities.Builder(isAudioCapable, isVideoCapable);
+ servCapsBuilder.addSupportedDuplexMode(duplexMode);
+
+ // init the presence tuple
+ RcsContactPresenceTuple.Builder tupleBuilder = new RcsContactPresenceTuple.Builder(
+ basicStatus, RcsContactPresenceTuple.SERVICE_ID_MMTEL, version);
+ tupleBuilder.setContactUri(contact)
+ .setServiceDescription(description)
+ .setTime(nowTime)
+ .setServiceCapabilities(servCapsBuilder.build());
+
+ PresenceBuilder presenceBuilder = new PresenceBuilder(contact,
+ RcsContactUceCapability.SOURCE_TYPE_NETWORK,
+ RcsContactUceCapability.REQUEST_RESULT_FOUND);
+ presenceBuilder.addCapabilityTuple(tupleBuilder.build());
+
+ return presenceBuilder.build();
+ }
+}
diff --git a/tests/src/com/android/ims/rcs/uce/presence/pidfparser/capabilities/AudioTest.java b/tests/src/com/android/ims/rcs/uce/presence/pidfparser/capabilities/AudioTest.java
new file mode 100644
index 00000000..5bf97153
--- /dev/null
+++ b/tests/src/com/android/ims/rcs/uce/presence/pidfparser/capabilities/AudioTest.java
@@ -0,0 +1,135 @@
+/*
+ * 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.ims.rcs.uce.presence.pidfparser.capabilities;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.ims.ImsTestBase;
+import com.android.ims.rcs.uce.presence.pidfparser.PidfParserConstant;
+
+import com.android.ims.rcs.uce.presence.pidfparser.pidf.PidfConstant;
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.StringWriter;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+import org.xmlpull.v1.XmlSerializer;
+
+@RunWith(AndroidJUnit4.class)
+public class AudioTest extends ImsTestBase {
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ @Test
+ @SmallTest
+ public void testElementName() throws Exception {
+ Audio audio = new Audio();
+
+ assertEquals(CapsConstant.NAMESPACE, audio.getNamespace());
+ assertEquals(Audio.ELEMENT_NAME, audio.getElementName());
+ }
+
+ @Test
+ @SmallTest
+ public void testSerializing() throws Exception {
+ Audio audio = new Audio(true);
+ StringWriter writer = new StringWriter();
+ XmlSerializer serializer = getXmlSerializer(writer);
+
+ // Serializing
+ serializer.startDocument(PidfParserConstant.ENCODING_UTF_8, true);
+ audio.serialize(serializer);
+ serializer.endDocument();
+ serializer.flush();
+
+ String result = writer.toString();
+ StringBuilder verificationBuilder = new StringBuilder();
+ verificationBuilder.append("<caps:audio")
+ .append(" xmlns=\"").append(PidfConstant.NAMESPACE).append("\"")
+ .append(" xmlns:caps=\"").append(CapsConstant.NAMESPACE).append("\">")
+ .append("true")
+ .append("</caps:audio>");
+
+ assertTrue(result.contains(verificationBuilder.toString()));
+ }
+
+
+ @Test
+ @SmallTest
+ public void testParsing() throws Exception {
+ StringBuilder audioExample = new StringBuilder();
+ audioExample.append("<?xml version='1.0' encoding='utf-8' standalone='yes' ?>")
+ .append("<caps:audio xmlns=\"urn:ietf:params:xml:ns:pidf\"")
+ .append(" xmlns:caps=\"urn:ietf:params:xml:ns:pidf:caps\">")
+ .append(true)
+ .append("</caps:audio>");
+
+ XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser();
+ parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
+ Reader reader = new StringReader(audioExample.toString());
+ parser.setInput(reader);
+
+ Audio audio = null;
+ int nextType = parser.next();
+
+ // Find the start tag
+ do {
+ if (nextType == XmlPullParser.START_TAG
+ && Audio.ELEMENT_NAME.equals(parser.getName())) {
+ audio = new Audio();
+ audio.parse(parser);
+ break;
+ }
+ nextType = parser.next();
+ } while(nextType != XmlPullParser.END_DOCUMENT);
+
+ reader.close();
+
+ assertNotNull(audio);
+ assertTrue(audio.isAudioSupported());
+ }
+
+ private XmlSerializer getXmlSerializer(StringWriter writer)
+ throws XmlPullParserException, IOException {
+ XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
+ XmlSerializer serializer = factory.newSerializer();
+ serializer.setOutput(writer);
+ serializer.setPrefix("", PidfConstant.NAMESPACE);
+ serializer.setPrefix("caps", CapsConstant.NAMESPACE);
+ return serializer;
+ }
+}
diff --git a/tests/src/com/android/ims/rcs/uce/presence/pidfparser/capabilities/DuplexTest.java b/tests/src/com/android/ims/rcs/uce/presence/pidfparser/capabilities/DuplexTest.java
new file mode 100644
index 00000000..8852c3d5
--- /dev/null
+++ b/tests/src/com/android/ims/rcs/uce/presence/pidfparser/capabilities/DuplexTest.java
@@ -0,0 +1,147 @@
+/*
+ * 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.ims.rcs.uce.presence.pidfparser.capabilities;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.ims.ImsTestBase;
+import com.android.ims.rcs.uce.presence.pidfparser.PidfParserConstant;
+import com.android.ims.rcs.uce.presence.pidfparser.pidf.PidfConstant;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.util.List;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+import org.xmlpull.v1.XmlSerializer;
+
+@RunWith(AndroidJUnit4.class)
+public class DuplexTest extends ImsTestBase {
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ @Test
+ @SmallTest
+ public void testElementName() throws Exception {
+ Duplex duplex = new Duplex();
+
+ assertEquals(CapsConstant.NAMESPACE, duplex.getNamespace());
+ assertEquals(Duplex.ELEMENT_NAME, duplex.getElementName());
+ }
+
+ @Test
+ @SmallTest
+ public void testSerializing() throws Exception {
+ Duplex duplex = new Duplex();
+ duplex.addSupportedType(Duplex.DUPLEX_FULL);
+
+ StringWriter writer = new StringWriter();
+ XmlSerializer serializer = getXmlSerializer(writer);
+
+ // Serializing
+ serializer.startDocument(PidfParserConstant.ENCODING_UTF_8, true);
+ duplex.serialize(serializer);
+ serializer.endDocument();
+ serializer.flush();
+
+ String result = writer.toString();
+
+ StringBuilder verificationBuilder = new StringBuilder();
+ verificationBuilder.append("<caps:duplex")
+ .append(" xmlns=\"").append(PidfConstant.NAMESPACE).append("\"")
+ .append(" xmlns:caps=\"").append(CapsConstant.NAMESPACE).append("\">")
+ .append("<caps:supported>")
+ .append("<caps:full />")
+ .append("</caps:supported>").append("</caps:duplex>");
+
+ assertTrue(result.contains(verificationBuilder.toString()));
+ }
+
+
+ @Test
+ @SmallTest
+ public void testParsing() throws Exception {
+ StringBuilder duplexExample = new StringBuilder();
+ duplexExample.append("<?xml version='1.0' encoding='utf-8' standalone='yes' ?>")
+ .append("<caps:duplex xmlns=\"urn:ietf:params:xml:ns:pidf\"")
+ .append(" xmlns:caps=\"urn:ietf:params:xml:ns:pidf:caps\">")
+ .append("<caps:supported>")
+ .append("<caps:full />")
+ .append("</caps:supported>").append("</caps:duplex>");
+
+ XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser();
+ parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
+ Reader reader = new StringReader(duplexExample.toString());
+ parser.setInput(reader);
+
+ Duplex duplex = null;
+ int nextType = parser.next();
+
+ do {
+ // Find the start tag
+ if (nextType == XmlPullParser.START_TAG
+ && Duplex.ELEMENT_NAME.equals(parser.getName())) {
+ duplex = new Duplex();
+ duplex.parse(parser);
+ break;
+ }
+ nextType = parser.next();
+ } while(nextType != XmlPullParser.END_DOCUMENT);
+
+ reader.close();
+
+ assertNotNull(duplex);
+
+ List<String> supportedTypes = duplex.getSupportedTypes();
+ assertEquals(1, supportedTypes.size());
+ assertEquals(Duplex.DUPLEX_FULL, supportedTypes.get(0));
+
+ List<String> notSupportedTypes = duplex.getNotSupportedTypes();
+ assertTrue(notSupportedTypes.isEmpty());
+ }
+
+ private XmlSerializer getXmlSerializer(StringWriter writer)
+ throws XmlPullParserException, IOException {
+ XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
+ XmlSerializer serializer = factory.newSerializer();
+ serializer.setOutput(writer);
+ serializer.setPrefix("", PidfConstant.NAMESPACE);
+ serializer.setPrefix("caps", CapsConstant.NAMESPACE);
+ return serializer;
+ }
+}
diff --git a/tests/src/com/android/ims/rcs/uce/presence/pidfparser/capabilities/ServiceCapsTest.java b/tests/src/com/android/ims/rcs/uce/presence/pidfparser/capabilities/ServiceCapsTest.java
new file mode 100644
index 00000000..d18a7ebd
--- /dev/null
+++ b/tests/src/com/android/ims/rcs/uce/presence/pidfparser/capabilities/ServiceCapsTest.java
@@ -0,0 +1,181 @@
+/*
+ * 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.ims.rcs.uce.presence.pidfparser.capabilities;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.ims.ImsTestBase;
+import com.android.ims.rcs.uce.presence.pidfparser.ElementBase;
+import com.android.ims.rcs.uce.presence.pidfparser.PidfParserConstant;
+import com.android.ims.rcs.uce.presence.pidfparser.pidf.PidfConstant;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.StringWriter;
+
+import java.util.List;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+import org.xmlpull.v1.XmlSerializer;
+
+@RunWith(AndroidJUnit4.class)
+public class ServiceCapsTest extends ImsTestBase {
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ @Test
+ @SmallTest
+ public void testElementName() throws Exception {
+ ServiceCaps serviceCaps = new ServiceCaps();
+
+ assertEquals(CapsConstant.NAMESPACE, serviceCaps.getNamespace());
+ assertEquals(ServiceCaps.ELEMENT_NAME, serviceCaps.getElementName());
+ }
+
+ @Test
+ @SmallTest
+ public void testSerializing() throws Exception {
+ Audio audio = new Audio(true);
+ Video video = new Video(true);
+ Duplex duplex = new Duplex();
+ duplex.addSupportedType(Duplex.DUPLEX_FULL);
+
+ ServiceCaps serviceCaps = new ServiceCaps();
+ serviceCaps.addElement(audio);
+ serviceCaps.addElement(video);
+ serviceCaps.addElement(duplex);
+
+ StringWriter writer = new StringWriter();
+ XmlSerializer serializer = getXmlSerializer(writer);
+
+ // Serializing
+ serializer.startDocument(PidfParserConstant.ENCODING_UTF_8, true);
+ serviceCaps.serialize(serializer);
+ serializer.endDocument();
+ serializer.flush();
+
+ String result = writer.toString();
+
+ String verificationAudio = "<caps:audio>true</caps:audio>";
+ String verificationVideo = "<caps:video>true</caps:video>";
+ StringBuilder verificationDuplex = new StringBuilder();
+ verificationDuplex.append("<caps:duplex>")
+ .append("<caps:supported>")
+ .append("<caps:full />")
+ .append("</caps:supported>")
+ .append("</caps:duplex>");
+
+ assertTrue(result.contains(verificationAudio));
+ assertTrue(result.contains(verificationVideo));
+ assertTrue(result.contains(verificationDuplex.toString()));
+ }
+
+
+ @Test
+ @SmallTest
+ public void testParsing() throws Exception {
+ StringBuilder serviceCapsExample = new StringBuilder();
+ serviceCapsExample.append("<?xml version='1.0' encoding='utf-8' standalone='yes' ?>")
+ .append("<caps:servcaps xmlns=\"urn:ietf:params:xml:ns:pidf\"")
+ .append(" xmlns:caps=\"urn:ietf:params:xml:ns:pidf:caps\">")
+ .append("<caps:audio>true</caps:audio>")
+ .append("<caps:video>true</caps:video>")
+ .append("<caps:duplex><caps:supported>")
+ .append("<caps:full />")
+ .append("</caps:supported></caps:duplex>")
+ .append("</caps:servcaps>");
+
+ XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser();
+ parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
+ Reader reader = new StringReader(serviceCapsExample.toString());
+ parser.setInput(reader);
+
+ ServiceCaps serviceCaps = null;
+ int nextType = parser.next();
+
+ // Find the start tag
+ do {
+ if (nextType == XmlPullParser.START_TAG
+ && ServiceCaps.ELEMENT_NAME.equals(parser.getName())) {
+ serviceCaps = new ServiceCaps();
+ serviceCaps.parse(parser);
+ break;
+ }
+ nextType = parser.next();
+ } while(nextType != XmlPullParser.END_DOCUMENT);
+
+ reader.close();
+
+ assertNotNull(serviceCaps);
+
+ List<ElementBase> elements = serviceCaps.getElements();
+ Audio resultAudio = null;
+ Video resultVideo = null;
+ Duplex resultDuplex = null;
+ for (ElementBase element : elements) {
+ String elementName = element.getElementName();
+ if (Audio.ELEMENT_NAME.equals(elementName)) {
+ resultAudio = (Audio) element;
+ } else if (Video.ELEMENT_NAME.equals(elementName)) {
+ resultVideo = (Video) element;
+ } else if (Duplex.ELEMENT_NAME.equals(elementName)) {
+ resultDuplex = (Duplex) element;
+ }
+ }
+
+ assertNotNull(resultAudio);
+ assertTrue(resultAudio.isAudioSupported());
+
+ assertNotNull(resultVideo);
+ assertTrue(resultVideo.isVideoSupported());
+
+ assertNotNull(resultDuplex);
+
+ List<String> supportedTypes = resultDuplex.getSupportedTypes();
+ assertEquals(1, supportedTypes.size());
+ assertEquals(Duplex.DUPLEX_FULL, supportedTypes.get(0));
+ }
+
+ private XmlSerializer getXmlSerializer(StringWriter writer)
+ throws XmlPullParserException, IOException {
+ XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
+ XmlSerializer serializer = factory.newSerializer();
+ serializer.setOutput(writer);
+ serializer.setPrefix("", PidfConstant.NAMESPACE);
+ serializer.setPrefix("caps", CapsConstant.NAMESPACE);
+ return serializer;
+ }
+}
diff --git a/tests/src/com/android/ims/rcs/uce/presence/pidfparser/capabilities/VideoTest.java b/tests/src/com/android/ims/rcs/uce/presence/pidfparser/capabilities/VideoTest.java
new file mode 100644
index 00000000..c102176c
--- /dev/null
+++ b/tests/src/com/android/ims/rcs/uce/presence/pidfparser/capabilities/VideoTest.java
@@ -0,0 +1,135 @@
+/*
+ * 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.ims.rcs.uce.presence.pidfparser.capabilities;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.ims.ImsTestBase;
+import com.android.ims.rcs.uce.presence.pidfparser.PidfParserConstant;
+
+import com.android.ims.rcs.uce.presence.pidfparser.pidf.PidfConstant;
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.StringWriter;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+import org.xmlpull.v1.XmlSerializer;
+
+@RunWith(AndroidJUnit4.class)
+public class VideoTest extends ImsTestBase {
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ @Test
+ @SmallTest
+ public void testElementName() throws Exception {
+ Video video = new Video();
+
+ assertEquals(CapsConstant.NAMESPACE, video.getNamespace());
+ assertEquals(Video.ELEMENT_NAME, video.getElementName());
+ }
+
+ @Test
+ @SmallTest
+ public void testSerializing() throws Exception {
+ Video video = new Video(true);
+ StringWriter writer = new StringWriter();
+ XmlSerializer serializer = getXmlSerializer(writer);
+
+ // Serializing
+ serializer.startDocument(PidfParserConstant.ENCODING_UTF_8, true);
+ video.serialize(serializer);
+ serializer.endDocument();
+ serializer.flush();
+
+ String result = writer.toString();
+
+ StringBuilder verificationBuilder = new StringBuilder();
+ verificationBuilder.append("<caps:video")
+ .append(" xmlns=\"").append(PidfConstant.NAMESPACE).append("\"")
+ .append(" xmlns:caps=\"").append(CapsConstant.NAMESPACE).append("\">")
+ .append("true")
+ .append("</caps:video>");
+
+ assertTrue(result.contains(verificationBuilder.toString()));
+ }
+
+ @Test
+ @SmallTest
+ public void testParsing() throws Exception {
+ StringBuilder videoExample = new StringBuilder();
+ videoExample.append("<?xml version='1.0' encoding='utf-8' standalone='yes' ?>")
+ .append("<caps:video xmlns=\"urn:ietf:params:xml:ns:pidf\"")
+ .append(" xmlns:caps=\"urn:ietf:params:xml:ns:pidf:caps\">")
+ .append(true)
+ .append("</caps:video>");
+
+ XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser();
+ parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
+ Reader reader = new StringReader(videoExample.toString());
+ parser.setInput(reader);
+
+ Video video = null;
+ int nextType = parser.next();
+
+ // Find the start tag
+ do {
+ if (nextType == XmlPullParser.START_TAG
+ && Video.ELEMENT_NAME.equals(parser.getName())) {
+ video = new Video();
+ video.parse(parser);
+ break;
+ }
+ nextType = parser.next();
+ } while(nextType != XmlPullParser.END_DOCUMENT);
+
+ reader.close();
+
+ assertNotNull(video);
+ assertTrue(video.isVideoSupported());
+ }
+
+ private XmlSerializer getXmlSerializer(StringWriter writer)
+ throws XmlPullParserException, IOException {
+ XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
+ XmlSerializer serializer = factory.newSerializer();
+ serializer.setOutput(writer);
+ serializer.setPrefix("", PidfConstant.NAMESPACE);
+ serializer.setPrefix("caps", CapsConstant.NAMESPACE);
+ return serializer;
+ }
+}
diff --git a/tests/src/com/android/ims/rcs/uce/presence/pidfparser/omapres/DescriptionTest.java b/tests/src/com/android/ims/rcs/uce/presence/pidfparser/omapres/DescriptionTest.java
new file mode 100644
index 00000000..d60b664d
--- /dev/null
+++ b/tests/src/com/android/ims/rcs/uce/presence/pidfparser/omapres/DescriptionTest.java
@@ -0,0 +1,139 @@
+/*
+ * 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.ims.rcs.uce.presence.pidfparser.omapres;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.ims.ImsTestBase;
+import com.android.ims.rcs.uce.presence.pidfparser.PidfParserConstant;
+import com.android.ims.rcs.uce.presence.pidfparser.pidf.PidfConstant;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.StringWriter;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+import org.xmlpull.v1.XmlSerializer;
+
+@RunWith(AndroidJUnit4.class)
+public class DescriptionTest extends ImsTestBase {
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ @Test
+ @SmallTest
+ public void testElementName() throws Exception {
+ Description description = new Description();
+
+ assertEquals(OmaPresConstant.NAMESPACE, description.getNamespace());
+ assertEquals(Description.ELEMENT_NAME, description.getElementName());
+ }
+
+ @Test
+ @SmallTest
+ public void testSerializing() throws Exception {
+ final String descriptionValue = "Description test";
+ Description description = new Description(descriptionValue);
+
+ StringWriter writer = new StringWriter();
+ XmlSerializer serializer = getXmlSerializer(writer);
+
+ // Serializing
+ serializer.startDocument(PidfParserConstant.ENCODING_UTF_8, true);
+ description.serialize(serializer);
+ serializer.endDocument();
+ serializer.flush();
+
+ String result = writer.toString();
+
+ StringBuilder verificationBuilder = new StringBuilder();
+ verificationBuilder.append("<op:description")
+ .append(" xmlns=\"").append(PidfConstant.NAMESPACE).append("\"")
+ .append(" xmlns:op=\"").append(OmaPresConstant.NAMESPACE).append("\">")
+ .append(descriptionValue)
+ .append("</op:description>");
+
+ assertTrue(result.contains(verificationBuilder.toString()));
+ }
+
+ @Test
+ @SmallTest
+ public void testParsing() throws Exception {
+ final String descriptionValue = "Description test";
+
+ StringBuilder descriptionExample = new StringBuilder();
+ descriptionExample.append("<?xml version='1.0' encoding='utf-8' standalone='yes' ?>")
+ .append("<op:description xmlns=\"urn:ietf:params:xml:ns:pidf\"")
+ .append(" xmlns:op=\"urn:oma:xml:prs:pidf:oma-pres\">")
+ .append(descriptionValue)
+ .append("</op:description>");
+
+ XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser();
+ parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
+ Reader reader = new StringReader(descriptionExample.toString());
+ parser.setInput(reader);
+
+ Description description = null;
+ int nextType = parser.next();
+
+ // Find the start tag
+ do {
+ if (nextType == XmlPullParser.START_TAG
+ && Description.ELEMENT_NAME.equals(parser.getName())) {
+ description = new Description();
+ description.parse(parser);
+ break;
+ }
+ nextType = parser.next();
+ } while(nextType != XmlPullParser.END_DOCUMENT);
+
+ reader.close();
+
+ assertNotNull(description);
+ assertEquals(descriptionValue, description.getValue());
+ }
+
+ private XmlSerializer getXmlSerializer(StringWriter writer)
+ throws XmlPullParserException, IOException {
+ XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
+ XmlSerializer serializer = factory.newSerializer();
+ serializer.setOutput(writer);
+ serializer.setPrefix("", PidfConstant.NAMESPACE);
+ serializer.setPrefix("op", OmaPresConstant.NAMESPACE);
+ return serializer;
+ }
+}
diff --git a/tests/src/com/android/ims/rcs/uce/presence/pidfparser/omapres/ServiceDescriptionTest.java b/tests/src/com/android/ims/rcs/uce/presence/pidfparser/omapres/ServiceDescriptionTest.java
new file mode 100644
index 00000000..5e064cf8
--- /dev/null
+++ b/tests/src/com/android/ims/rcs/uce/presence/pidfparser/omapres/ServiceDescriptionTest.java
@@ -0,0 +1,150 @@
+/*
+ * 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.ims.rcs.uce.presence.pidfparser.omapres;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.ims.ImsTestBase;
+import com.android.ims.rcs.uce.presence.pidfparser.PidfParserConstant;
+import com.android.ims.rcs.uce.presence.pidfparser.pidf.PidfConstant;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.StringWriter;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+import org.xmlpull.v1.XmlSerializer;
+
+@RunWith(AndroidJUnit4.class)
+public class ServiceDescriptionTest extends ImsTestBase {
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ @Test
+ @SmallTest
+ public void testElementName() throws Exception {
+ ServiceDescription serviceDescription = new ServiceDescription();
+
+ assertEquals(OmaPresConstant.NAMESPACE, serviceDescription.getNamespace());
+ assertEquals(ServiceDescription.ELEMENT_NAME, serviceDescription.getElementName());
+ }
+
+ @Test
+ @SmallTest
+ public void testSerializing() throws Exception {
+ final ServiceId serviceId = new ServiceId("service_id_001");
+ final Version version = new Version(1, 0);
+ final Description description = new Description("description_test");
+
+ ServiceDescription serviceDescription = new ServiceDescription();
+ serviceDescription.setServiceId(serviceId);
+ serviceDescription.setVersion(version);
+ serviceDescription.setDescription(description);
+
+ StringWriter writer = new StringWriter();
+ XmlSerializer serializer = getXmlSerializer(writer);
+
+ // Serializing
+ serializer.startDocument(PidfParserConstant.ENCODING_UTF_8, true);
+ serviceDescription.serialize(serializer);
+ serializer.endDocument();
+ serializer.flush();
+
+ String result = writer.toString();
+
+ assertTrue(result.contains(serviceId.getValue()));
+ assertTrue(result.contains(version.getValue()));
+ assertTrue(result.contains(description.getValue()));
+ }
+
+ @Test
+ @SmallTest
+ public void testParsing() throws Exception {
+ final String serviceIdValue = "service_id_001";
+ final String version = "1.0";
+ final String description = "description test";
+
+ StringBuilder serviceDescriptionExample = new StringBuilder();
+ serviceDescriptionExample.append("<?xml version='1.0' encoding='utf-8' standalone='yes' ?>")
+ .append("<op:service-description xmlns=\"urn:ietf:params:xml:ns:pidf\"")
+ .append(" xmlns:op=\"urn:oma:xml:prs:pidf:oma-pres\">")
+ .append("<op:service-id>").append(serviceIdValue).append("</op:service-id>")
+ .append("<op:version>").append(version).append("</op:version>")
+ .append("<op:description>").append(description).append("</op:description>")
+ .append("</op:service-description>");
+
+ XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser();
+ parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
+ Reader reader = new StringReader(serviceDescriptionExample.toString());
+ parser.setInput(reader);
+
+ ServiceDescription serviceDescription = null;
+ int nextType = parser.next();
+
+ // Find the start tag
+ do {
+ if (nextType == XmlPullParser.START_TAG
+ && ServiceDescription.ELEMENT_NAME.equals(parser.getName())) {
+ serviceDescription = new ServiceDescription();
+ serviceDescription.parse(parser);
+ break;
+ }
+ nextType = parser.next();
+ } while(nextType != XmlPullParser.END_DOCUMENT);
+
+ reader.close();
+
+ assertNotNull(serviceDescription);
+ assertNotNull(serviceDescription.getServiceId());
+ assertNotNull(serviceDescription.getVersion());
+ assertNotNull(serviceDescription.getDescription());
+
+ assertEquals(serviceIdValue, serviceDescription.getServiceId().getValue());
+ assertEquals(version, serviceDescription.getVersion().getValue());
+ assertEquals(description, serviceDescription.getDescription().getValue());
+ }
+
+ private XmlSerializer getXmlSerializer(StringWriter writer)
+ throws XmlPullParserException, IOException {
+ XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
+ XmlSerializer serializer = factory.newSerializer();
+ serializer.setOutput(writer);
+ serializer.setPrefix("", PidfConstant.NAMESPACE);
+ serializer.setPrefix("op", OmaPresConstant.NAMESPACE);
+ return serializer;
+ }
+}
diff --git a/tests/src/com/android/ims/rcs/uce/presence/pidfparser/omapres/ServiceIdTest.java b/tests/src/com/android/ims/rcs/uce/presence/pidfparser/omapres/ServiceIdTest.java
new file mode 100644
index 00000000..d648ccf0
--- /dev/null
+++ b/tests/src/com/android/ims/rcs/uce/presence/pidfparser/omapres/ServiceIdTest.java
@@ -0,0 +1,139 @@
+/*
+ * 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.ims.rcs.uce.presence.pidfparser.omapres;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.ims.ImsTestBase;
+import com.android.ims.rcs.uce.presence.pidfparser.PidfParserConstant;
+import com.android.ims.rcs.uce.presence.pidfparser.pidf.PidfConstant;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.StringWriter;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+import org.xmlpull.v1.XmlSerializer;
+
+@RunWith(AndroidJUnit4.class)
+public class ServiceIdTest extends ImsTestBase {
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ @Test
+ @SmallTest
+ public void testElementName() throws Exception {
+ ServiceId serviceId = new ServiceId();
+
+ assertEquals(OmaPresConstant.NAMESPACE, serviceId.getNamespace());
+ assertEquals(ServiceId.ELEMENT_NAME, serviceId.getElementName());
+ }
+
+ @Test
+ @SmallTest
+ public void testSerializing() throws Exception {
+ final String serviceIdValue = "service_id_001";
+ ServiceId serviceId = new ServiceId(serviceIdValue);
+
+ StringWriter writer = new StringWriter();
+ XmlSerializer serializer = getXmlSerializer(writer);
+
+ // Serializing
+ serializer.startDocument(PidfParserConstant.ENCODING_UTF_8, true);
+ serviceId.serialize(serializer);
+ serializer.endDocument();
+ serializer.flush();
+
+ String result = writer.toString();
+
+ StringBuilder verificationBuilder = new StringBuilder();
+ verificationBuilder.append("<op:service-id")
+ .append(" xmlns=\"").append(PidfConstant.NAMESPACE).append("\"")
+ .append(" xmlns:op=\"").append(OmaPresConstant.NAMESPACE).append("\">")
+ .append(serviceIdValue)
+ .append("</op:service-id>");
+
+ assertTrue(result.contains(verificationBuilder.toString()));
+ }
+
+ @Test
+ @SmallTest
+ public void testParsing() throws Exception {
+ final String serviceIdValue = "service_id_001";
+
+ StringBuilder serviceIdExample = new StringBuilder();
+ serviceIdExample.append("<?xml version='1.0' encoding='utf-8' standalone='yes' ?>")
+ .append("<op:service-id xmlns=\"urn:ietf:params:xml:ns:pidf\"")
+ .append(" xmlns:op=\"urn:oma:xml:prs:pidf:oma-pres\">")
+ .append(serviceIdValue)
+ .append("</op:service-id>");
+
+ XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser();
+ parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
+ Reader reader = new StringReader(serviceIdExample.toString());
+ parser.setInput(reader);
+
+ ServiceId serviceId = null;
+ int nextType = parser.next();
+
+ // Find the start tag
+ do {
+ if (nextType == XmlPullParser.START_TAG
+ && ServiceId.ELEMENT_NAME.equals(parser.getName())) {
+ serviceId = new ServiceId();
+ serviceId.parse(parser);
+ break;
+ }
+ nextType = parser.next();
+ } while(nextType != XmlPullParser.END_DOCUMENT);
+
+ reader.close();
+
+ assertNotNull(serviceId);
+ assertEquals(serviceIdValue, serviceId.getValue());
+ }
+
+ private XmlSerializer getXmlSerializer(StringWriter writer)
+ throws XmlPullParserException, IOException {
+ XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
+ XmlSerializer serializer = factory.newSerializer();
+ serializer.setOutput(writer);
+ serializer.setPrefix("", PidfConstant.NAMESPACE);
+ serializer.setPrefix("op", OmaPresConstant.NAMESPACE);
+ return serializer;
+ }
+}
diff --git a/tests/src/com/android/ims/rcs/uce/presence/pidfparser/omapres/VersionTest.java b/tests/src/com/android/ims/rcs/uce/presence/pidfparser/omapres/VersionTest.java
new file mode 100644
index 00000000..ae7d0c49
--- /dev/null
+++ b/tests/src/com/android/ims/rcs/uce/presence/pidfparser/omapres/VersionTest.java
@@ -0,0 +1,140 @@
+/*
+ * 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.ims.rcs.uce.presence.pidfparser.omapres;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.ims.ImsTestBase;
+import com.android.ims.rcs.uce.presence.pidfparser.PidfParserConstant;
+import com.android.ims.rcs.uce.presence.pidfparser.pidf.PidfConstant;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.StringWriter;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+import org.xmlpull.v1.XmlSerializer;
+
+@RunWith(AndroidJUnit4.class)
+public class VersionTest extends ImsTestBase {
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ @Test
+ @SmallTest
+ public void testElementName() throws Exception {
+ Version version = new Version();
+
+ assertEquals(OmaPresConstant.NAMESPACE, version.getNamespace());
+ assertEquals(Version.ELEMENT_NAME, version.getElementName());
+ }
+
+ @Test
+ @SmallTest
+ public void testSerializing() throws Exception {
+ final int majorVersion = 1;
+ final int minorVersion = 0;
+ Version version = new Version(majorVersion, minorVersion);
+
+ StringWriter writer = new StringWriter();
+ XmlSerializer serializer = getXmlSerializer(writer);
+
+ // Serializing
+ serializer.startDocument(PidfParserConstant.ENCODING_UTF_8, true);
+ version.serialize(serializer);
+ serializer.endDocument();
+ serializer.flush();
+
+ String result = writer.toString();
+
+ StringBuilder verificationBuilder = new StringBuilder();
+ verificationBuilder.append("<op:version")
+ .append(" xmlns=\"").append(PidfConstant.NAMESPACE).append("\"")
+ .append(" xmlns:op=\"").append(OmaPresConstant.NAMESPACE).append("\">")
+ .append(majorVersion + "." + minorVersion)
+ .append("</op:version>");
+
+ assertTrue(result.contains(verificationBuilder.toString()));
+ }
+
+ @Test
+ @SmallTest
+ public void testParsing() throws Exception {
+ final String versionValue = "1.0";
+
+ StringBuilder versionExample = new StringBuilder();
+ versionExample.append("<?xml version='1.0' encoding='utf-8' standalone='yes' ?>")
+ .append("<op:version xmlns=\"urn:ietf:params:xml:ns:pidf\"")
+ .append(" xmlns:op=\"urn:oma:xml:prs:pidf:oma-pres\">")
+ .append(versionValue)
+ .append("</op:version>");
+
+ XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser();
+ parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
+ Reader reader = new StringReader(versionExample.toString());
+ parser.setInput(reader);
+
+ Version version = null;
+ int nextType = parser.next();
+
+ // Find the start tag
+ do {
+ if (nextType == XmlPullParser.START_TAG
+ && Version.ELEMENT_NAME.equals(parser.getName())) {
+ version = new Version();
+ version.parse(parser);
+ break;
+ }
+ nextType = parser.next();
+ } while(nextType != XmlPullParser.END_DOCUMENT);
+
+ reader.close();
+
+ assertNotNull(version);
+ assertEquals(versionValue, version.getValue());
+ }
+
+ private XmlSerializer getXmlSerializer(StringWriter writer)
+ throws XmlPullParserException, IOException {
+ XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
+ XmlSerializer serializer = factory.newSerializer();
+ serializer.setOutput(writer);
+ serializer.setPrefix("", PidfConstant.NAMESPACE);
+ serializer.setPrefix("op", OmaPresConstant.NAMESPACE);
+ return serializer;
+ }
+}
diff --git a/tests/src/com/android/ims/rcs/uce/presence/pidfparser/pidf/BasicTest.java b/tests/src/com/android/ims/rcs/uce/presence/pidfparser/pidf/BasicTest.java
new file mode 100644
index 00000000..cb0ec0cf
--- /dev/null
+++ b/tests/src/com/android/ims/rcs/uce/presence/pidfparser/pidf/BasicTest.java
@@ -0,0 +1,145 @@
+/*
+ * 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.ims.rcs.uce.presence.pidfparser.pidf;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.ims.ImsTestBase;
+import com.android.ims.rcs.uce.presence.pidfparser.PidfParserConstant;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.StringWriter;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+import org.xmlpull.v1.XmlSerializer;
+
+@RunWith(AndroidJUnit4.class)
+public class BasicTest extends ImsTestBase {
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ @Test
+ @SmallTest
+ public void testElementName() throws Exception {
+ Basic basic = new Basic();
+
+ assertEquals(PidfConstant.NAMESPACE, basic.getNamespace());
+ assertEquals(Basic.ELEMENT_NAME, basic.getElementName());
+ }
+
+ @Test
+ @SmallTest
+ public void testSerializingWithBasicOpen() throws Exception {
+ Basic basic = new Basic(Basic.OPEN);
+ StringWriter writer = new StringWriter();
+ XmlSerializer serializer = getXmlSerializer(writer);
+
+ // Serializing
+ serializer.startDocument(PidfParserConstant.ENCODING_UTF_8, true);
+ basic.serialize(serializer);
+ serializer.endDocument();
+ serializer.flush();
+
+ final String result = writer.toString();
+ final String basicElementWithOpenValue =
+ "<basic xmlns=\"" + PidfConstant.NAMESPACE + "\">open</basic>";
+ assertTrue(result.contains(basicElementWithOpenValue));
+ }
+
+ @Test
+ @SmallTest
+ public void testSerializingWithBasicClosed() throws Exception {
+ Basic basic = new Basic(Basic.CLOSED);
+ StringWriter writer = new StringWriter();
+ XmlSerializer serializer = getXmlSerializer(writer);
+
+ // Serializing
+ serializer.startDocument(PidfParserConstant.ENCODING_UTF_8, true);
+ basic.serialize(serializer);
+ serializer.endDocument();
+ serializer.flush();
+
+ final String result = writer.toString();
+ final String basicElementWithClosedValue =
+ "<basic xmlns=\"" + PidfConstant.NAMESPACE + "\">closed</basic>";
+ assertTrue(result.contains(basicElementWithClosedValue));
+ }
+
+ @Test
+ @SmallTest
+ public void testParsingWithBasicOpen() throws Exception {
+ StringBuilder basicExample = new StringBuilder();
+ basicExample.append("<?xml version='1.0' encoding='utf-8' standalone='yes' ?>")
+ .append("<basic xmlns=\"urn:ietf:params:xml:ns:pidf\">")
+ .append(Basic.OPEN)
+ .append("</basic>");
+
+ XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser();
+ parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
+ Reader reader = new StringReader(basicExample.toString());
+ parser.setInput(reader);
+
+ Basic basic = null;
+ int nextType = parser.next();
+
+ // Find the start tag
+ do {
+ if (nextType == XmlPullParser.START_TAG
+ && Basic.ELEMENT_NAME.equals(parser.getName())) {
+ basic = new Basic();
+ basic.parse(parser);
+ break;
+ }
+ nextType = parser.next();
+ } while(nextType != XmlPullParser.END_DOCUMENT);
+
+ reader.close();
+
+ assertNotNull(basic);
+ assertEquals(Basic.OPEN, basic.getValue());
+ }
+
+ private XmlSerializer getXmlSerializer(StringWriter writer)
+ throws XmlPullParserException, IOException {
+ XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
+ XmlSerializer serializer = factory.newSerializer();
+ serializer.setOutput(writer);
+ serializer.setPrefix("", PidfConstant.NAMESPACE);
+ return serializer;
+ }
+}
diff --git a/tests/src/com/android/ims/rcs/uce/presence/pidfparser/pidf/ContactTest.java b/tests/src/com/android/ims/rcs/uce/presence/pidfparser/pidf/ContactTest.java
new file mode 100644
index 00000000..cfab77af
--- /dev/null
+++ b/tests/src/com/android/ims/rcs/uce/presence/pidfparser/pidf/ContactTest.java
@@ -0,0 +1,134 @@
+/*
+ * 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.ims.rcs.uce.presence.pidfparser.pidf;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import android.net.Uri;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.ims.ImsTestBase;
+import com.android.ims.rcs.uce.presence.pidfparser.PidfParserConstant;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.StringWriter;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+import org.xmlpull.v1.XmlSerializer;
+
+@RunWith(AndroidJUnit4.class)
+public class ContactTest extends ImsTestBase {
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ @Test
+ @SmallTest
+ public void testElementName() throws Exception {
+ Contact contact = new Contact();
+
+ assertEquals(PidfConstant.NAMESPACE, contact.getNamespace());
+ assertEquals(Contact.ELEMENT_NAME, contact.getElementName());
+ }
+
+ @Test
+ @SmallTest
+ public void testSerializing() throws Exception {
+ String testedContact = Uri.fromParts("sip", "test", null).toString();
+
+ Contact contact = new Contact();
+ contact.setContact(testedContact);
+ StringWriter writer = new StringWriter();
+ XmlSerializer serializer = getXmlSerializer(writer);
+
+ // Serializing
+ serializer.startDocument(PidfParserConstant.ENCODING_UTF_8, true);
+ contact.serialize(serializer);
+ serializer.endDocument();
+ serializer.flush();
+
+ final String result = writer.toString();
+
+ StringBuilder verificationBuilder = new StringBuilder();
+ verificationBuilder.append("<contact xmlns=\"").append(PidfConstant.NAMESPACE).append("\">")
+ .append(testedContact).append("</contact>");
+ assertTrue(result.contains(verificationBuilder.toString()));
+ }
+
+ @Test
+ @SmallTest
+ public void testParsing() throws Exception {
+ final String testedContact = Uri.fromParts("sip", "test", null).toString();
+
+ StringBuilder contactExample = new StringBuilder();
+ contactExample.append("<?xml version='1.0' encoding='utf-8' standalone='yes' ?>")
+ .append("<contact xmlns=\"urn:ietf:params:xml:ns:pidf\">")
+ .append(testedContact)
+ .append("</contact>");
+
+ XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser();
+ parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
+ Reader reader = new StringReader(contactExample.toString());
+ parser.setInput(reader);
+
+ Contact contact = null;
+ int nextType = parser.next();
+
+ // Find the start tag
+ do {
+ if (nextType == XmlPullParser.START_TAG
+ && Contact.ELEMENT_NAME.equals(parser.getName())) {
+ contact = new Contact();
+ contact.parse(parser);
+ break;
+ }
+ nextType = parser.next();
+ } while(nextType != XmlPullParser.END_DOCUMENT);
+
+ reader.close();
+
+ assertNotNull(contact);
+ assertEquals(testedContact, contact.getContact());
+ }
+
+ private XmlSerializer getXmlSerializer(StringWriter writer)
+ throws XmlPullParserException, IOException {
+ XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
+ XmlSerializer serializer = factory.newSerializer();
+ serializer.setOutput(writer);
+ serializer.setPrefix("", PidfConstant.NAMESPACE);
+ return serializer;
+ }
+}
diff --git a/tests/src/com/android/ims/rcs/uce/presence/pidfparser/pidf/NoteTest.java b/tests/src/com/android/ims/rcs/uce/presence/pidfparser/pidf/NoteTest.java
new file mode 100644
index 00000000..8ee5ce4b
--- /dev/null
+++ b/tests/src/com/android/ims/rcs/uce/presence/pidfparser/pidf/NoteTest.java
@@ -0,0 +1,132 @@
+/*
+ * 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.ims.rcs.uce.presence.pidfparser.pidf;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.ims.ImsTestBase;
+import com.android.ims.rcs.uce.presence.pidfparser.PidfParserConstant;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.StringWriter;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+import org.xmlpull.v1.XmlSerializer;
+
+@RunWith(AndroidJUnit4.class)
+public class NoteTest extends ImsTestBase {
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ @Test
+ @SmallTest
+ public void testElementName() throws Exception {
+ Note note = new Note();
+
+ assertEquals(PidfConstant.NAMESPACE, note.getNamespace());
+ assertEquals(Note.ELEMENT_NAME, note.getElementName());
+ }
+
+ @Test
+ @SmallTest
+ public void testSerializing() throws Exception {
+ final String noteValue = "Note test";
+
+ Note note = new Note(noteValue);
+ StringWriter writer = new StringWriter();
+ XmlSerializer serializer = getXmlSerializer(writer);
+
+ // Serializing
+ serializer.startDocument(PidfParserConstant.ENCODING_UTF_8, true);
+ note.serialize(serializer);
+ serializer.endDocument();
+ serializer.flush();
+
+ final String result = writer.toString();
+
+ StringBuilder verificationBuilder = new StringBuilder();
+ verificationBuilder.append("<note xmlns=\"").append(PidfConstant.NAMESPACE).append("\">")
+ .append(noteValue).append("</note>");
+ assertTrue(result.contains(verificationBuilder.toString()));
+ }
+
+ @Test
+ @SmallTest
+ public void testParsing() throws Exception {
+ final String noteValue = "Note test";
+
+ StringBuilder noteExample = new StringBuilder();
+ noteExample.append("<?xml version='1.0' encoding='utf-8' standalone='yes' ?>")
+ .append("<note xmlns=\"urn:ietf:params:xml:ns:pidf\">")
+ .append(noteValue)
+ .append("</note>");
+
+ XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser();
+ parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
+ Reader reader = new StringReader(noteExample.toString());
+ parser.setInput(reader);
+
+ Note note = null;
+ int nextType = parser.next();
+
+ // Find the start tag
+ do {
+ if (nextType == XmlPullParser.START_TAG
+ && Note.ELEMENT_NAME.equals(parser.getName())) {
+ note = new Note();
+ note.parse(parser);
+ break;
+ }
+ nextType = parser.next();
+ } while(nextType != XmlPullParser.END_DOCUMENT);
+
+ reader.close();
+
+ assertNotNull(note);
+ assertEquals(noteValue, note.getNote());
+ }
+
+ private XmlSerializer getXmlSerializer(StringWriter writer)
+ throws XmlPullParserException, IOException {
+ XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
+ XmlSerializer serializer = factory.newSerializer();
+ serializer.setOutput(writer);
+ serializer.setPrefix("", PidfConstant.NAMESPACE);
+ return serializer;
+ }
+}
diff --git a/tests/src/com/android/ims/rcs/uce/presence/pidfparser/pidf/PresenceTest.java b/tests/src/com/android/ims/rcs/uce/presence/pidfparser/pidf/PresenceTest.java
new file mode 100644
index 00000000..99606f90
--- /dev/null
+++ b/tests/src/com/android/ims/rcs/uce/presence/pidfparser/pidf/PresenceTest.java
@@ -0,0 +1,209 @@
+/*
+ * 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.ims.rcs.uce.presence.pidfparser.pidf;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import android.net.Uri;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.ims.ImsTestBase;
+import com.android.ims.rcs.uce.presence.pidfparser.PidfParserConstant;
+import com.android.ims.rcs.uce.presence.pidfparser.PidfParserUtils;
+import com.android.ims.rcs.uce.presence.pidfparser.capabilities.CapsConstant;
+import com.android.ims.rcs.uce.presence.pidfparser.omapres.Description;
+import com.android.ims.rcs.uce.presence.pidfparser.omapres.OmaPresConstant;
+import com.android.ims.rcs.uce.presence.pidfparser.omapres.ServiceDescription;
+import com.android.ims.rcs.uce.presence.pidfparser.omapres.ServiceId;
+import com.android.ims.rcs.uce.presence.pidfparser.omapres.Version;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.util.List;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+import org.xmlpull.v1.XmlSerializer;
+
+@RunWith(AndroidJUnit4.class)
+public class PresenceTest extends ImsTestBase {
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ @Test
+ @SmallTest
+ public void testElementName() throws Exception {
+ Presence presence = new Presence();
+
+ assertEquals(PidfConstant.NAMESPACE, presence.getNamespace());
+ assertEquals(Presence.ELEMENT_NAME, presence.getElementName());
+ }
+
+ @Test
+ @SmallTest
+ public void testSerializing() throws Exception {
+ String contact = Uri.fromParts("sip", "test", null).toString();
+
+ String serviceId1 = "service_id_01";
+ String description1 = "description_test1";
+ Tuple tuple1 = getTuple(Basic.OPEN, serviceId1, description1, contact);
+
+ String serviceId2 = "service_id_02";
+ String description2 = "description_test2";
+ Tuple tuple2 = getTuple(Basic.OPEN, serviceId2, description2, contact);
+
+ Presence presence = new Presence();
+ presence.setEntity(contact);
+ presence.addTuple(tuple1);
+ presence.addTuple(tuple2);
+
+ StringWriter writer = new StringWriter();
+ XmlSerializer serializer = getXmlSerializer(writer);
+
+ // Serializing
+ serializer.startDocument(PidfParserConstant.ENCODING_UTF_8, true);
+ presence.serialize(serializer);
+ serializer.endDocument();
+ serializer.flush();
+
+ String result = writer.toString();
+
+ String verificationServiceId1 = "<op:service-id>service_id_01</op:service-id>";
+ String verificationDescription1 = "<op:description>description_test1</op:description>";
+ String verificationServiceId2 = "<op:service-id>service_id_02</op:service-id>";
+ String verificationDescription2 = "<op:description>description_test2</op:description>";
+ String verificationContact = "<contact>sip:test</contact>";
+
+ assertTrue(result.contains(verificationServiceId1));
+ assertTrue(result.contains(verificationDescription1));
+ assertTrue(result.contains(verificationServiceId2));
+ assertTrue(result.contains(verificationDescription2));
+ assertTrue(result.contains(verificationContact));
+ }
+
+ @Test
+ @SmallTest
+ public void testParsing() throws Exception {
+ final String contact = Uri.fromParts("sip", "test", null).toString();
+ final String serviceId = "service_id_01";
+ final String version = "1.0";
+ final String description = "description_test";
+
+ StringBuilder presenceExample = new StringBuilder();
+ presenceExample.append("<?xml version='1.0' encoding='utf-8' standalone='yes' ?>")
+ .append("<presence entity=\"").append(contact).append("\"")
+ .append(" xmlns=\"urn:ietf:params:xml:ns:pidf\"")
+ .append(" xmlns:op=\"urn:oma:xml:prs:pidf:oma-pres\"")
+ .append(" xmlns:caps=\"urn:ietf:params:xml:ns:pidf:caps\">")
+ .append("<tuple id=\"tid0\"><status><basic>open</basic></status>")
+ .append("<op:service-description>")
+ .append("<op:service-id>").append(serviceId).append("</op:service-id>")
+ .append("<op:version>").append(version).append("</op:version>")
+ .append("<op:description>").append(description).append("</op:description>")
+ .append("</op:service-description>")
+ .append("<contact>sip:test</contact></tuple></presence>");
+
+ XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser();
+ parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
+ Reader reader = new StringReader(presenceExample.toString());
+ parser.setInput(reader);
+
+ Presence presence = null;
+ int nextType = parser.next();
+
+ // Find the start tag
+ do {
+ if (nextType == XmlPullParser.START_TAG
+ && Presence.ELEMENT_NAME.equals(parser.getName())) {
+ presence = new Presence();
+ presence.parse(parser);
+ break;
+ }
+ nextType = parser.next();
+ } while(nextType != XmlPullParser.END_DOCUMENT);
+
+ reader.close();
+
+ assertNotNull(presence);
+ assertEquals(contact, presence.getEntity());
+
+ List<Tuple> tupleList = presence.getTupleList();
+ assertNotNull(tupleList);
+ assertEquals(1, tupleList.size());
+
+ Tuple tuple = tupleList.get(0);
+ assertEquals(serviceId, PidfParserUtils.getTupleServiceId(tuple));
+ assertEquals(version, PidfParserUtils.getTupleServiceVersion(tuple));
+ assertEquals(description, PidfParserUtils.getTupleServiceDescription(tuple));
+ assertEquals(contact, PidfParserUtils.getTupleContact(tuple));
+ }
+
+ private Tuple getTuple(String statusValue, String serviceIdValue, String descValue,
+ String contactValue) {
+ Basic basic = new Basic(statusValue);
+ Status status = new Status();
+ status.setBasic(basic);
+
+ ServiceId serviceId = new ServiceId(serviceIdValue);
+ Version version = new Version(1, 0);
+ Description description = new Description(descValue);
+ ServiceDescription serviceDescription = new ServiceDescription();
+ serviceDescription.setServiceId(serviceId);
+ serviceDescription.setVersion(version);
+ serviceDescription.setDescription(description);
+
+ Contact contact = new Contact();
+ contact.setContact(contactValue);
+
+ Tuple tuple = new Tuple();
+ tuple.setStatus(status);
+ tuple.setServiceDescription(serviceDescription);
+ tuple.setContact(contact);
+
+ return tuple;
+ }
+
+ private XmlSerializer getXmlSerializer(StringWriter writer)
+ throws XmlPullParserException, IOException {
+ XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
+ XmlSerializer serializer = factory.newSerializer();
+ serializer.setOutput(writer);
+ serializer.setPrefix("", PidfConstant.NAMESPACE);
+ serializer.setPrefix("op", OmaPresConstant.NAMESPACE);
+ serializer.setPrefix("caps", CapsConstant.NAMESPACE);
+ return serializer;
+ }
+}
diff --git a/tests/src/com/android/ims/rcs/uce/presence/pidfparser/pidf/StatusTest.java b/tests/src/com/android/ims/rcs/uce/presence/pidfparser/pidf/StatusTest.java
new file mode 100644
index 00000000..945cb096
--- /dev/null
+++ b/tests/src/com/android/ims/rcs/uce/presence/pidfparser/pidf/StatusTest.java
@@ -0,0 +1,130 @@
+/*
+ * 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.ims.rcs.uce.presence.pidfparser.pidf;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.ims.ImsTestBase;
+import com.android.ims.rcs.uce.presence.pidfparser.PidfParserConstant;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.StringWriter;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+import org.xmlpull.v1.XmlSerializer;
+
+@RunWith(AndroidJUnit4.class)
+public class StatusTest extends ImsTestBase {
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ @Test
+ @SmallTest
+ public void testElementName() throws Exception {
+ Status status = new Status();
+
+ assertEquals(PidfConstant.NAMESPACE, status.getNamespace());
+ assertEquals(Status.ELEMENT_NAME, status.getElementName());
+ }
+
+ @Test
+ @SmallTest
+ public void testSerializing() throws Exception {
+ Basic basic = new Basic(Basic.OPEN);
+ Status status = new Status();
+ status.setBasic(basic);
+
+ StringWriter writer = new StringWriter();
+ XmlSerializer serializer = getXmlSerializer(writer);
+
+ // Serializing
+ serializer.startDocument(PidfParserConstant.ENCODING_UTF_8, true);
+ status.serialize(serializer);
+ serializer.endDocument();
+ serializer.flush();
+
+ String result = writer.toString();
+ String verification =
+ "<status xmlns=\"" + PidfConstant.NAMESPACE + "\"><basic>open</basic></status>";
+ assertTrue(result.contains(verification));
+ }
+
+ @Test
+ @SmallTest
+ public void testParsing() throws Exception {
+ StringBuilder statusExample = new StringBuilder();
+ statusExample.append("<?xml version='1.0' encoding='utf-8' standalone='yes' ?>")
+ .append("<status xmlns=\"urn:ietf:params:xml:ns:pidf\">")
+ .append("<basic>").append(Basic.OPEN).append("</basic>")
+ .append("</status>");
+
+ XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser();
+ parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
+ Reader reader = new StringReader(statusExample.toString());
+ parser.setInput(reader);
+
+ Status status = null;
+ int nextType = parser.next();
+
+ // Find the start tag
+ do {
+ if (nextType == XmlPullParser.START_TAG
+ && Status.ELEMENT_NAME.equals(parser.getName())) {
+ status = new Status();
+ status.parse(parser);
+ break;
+ }
+ nextType = parser.next();
+ } while(nextType != XmlPullParser.END_DOCUMENT);
+
+ reader.close();
+
+ assertNotNull(status);
+ assertNotNull(status.getBasic());
+ assertEquals(Basic.OPEN, status.getBasic().getValue());
+ }
+
+ private XmlSerializer getXmlSerializer(StringWriter writer)
+ throws XmlPullParserException, IOException {
+ XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
+ XmlSerializer serializer = factory.newSerializer();
+ serializer.setOutput(writer);
+ serializer.setPrefix("", PidfConstant.NAMESPACE);
+ return serializer;
+ }
+}
diff --git a/tests/src/com/android/ims/rcs/uce/presence/pidfparser/pidf/TimestampTest.java b/tests/src/com/android/ims/rcs/uce/presence/pidfparser/pidf/TimestampTest.java
new file mode 100644
index 00000000..1dc76e6c
--- /dev/null
+++ b/tests/src/com/android/ims/rcs/uce/presence/pidfparser/pidf/TimestampTest.java
@@ -0,0 +1,134 @@
+/*
+ * 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.ims.rcs.uce.presence.pidfparser.pidf;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.ims.ImsTestBase;
+import com.android.ims.rcs.uce.presence.pidfparser.PidfParserConstant;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.StringWriter;
+
+import java.time.Instant;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+import org.xmlpull.v1.XmlSerializer;
+
+@RunWith(AndroidJUnit4.class)
+public class TimestampTest extends ImsTestBase {
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ @Test
+ @SmallTest
+ public void testElementName() throws Exception {
+ Timestamp timestamp = new Timestamp();
+
+ assertEquals(PidfConstant.NAMESPACE, timestamp.getNamespace());
+ assertEquals(Timestamp.ELEMENT_NAME, timestamp.getElementName());
+ }
+
+ @Test
+ @SmallTest
+ public void testSerializing() throws Exception {
+ final String timestamp = Instant.now().toString();
+
+ Timestamp timestampElement = new Timestamp(timestamp);
+ StringWriter writer = new StringWriter();
+ XmlSerializer serializer = getXmlSerializer(writer);
+
+ // Serializing
+ serializer.startDocument(PidfParserConstant.ENCODING_UTF_8, true);
+ timestampElement.serialize(serializer);
+ serializer.endDocument();
+ serializer.flush();
+
+ String result = writer.toString();
+
+ StringBuilder verificationBuilder = new StringBuilder();
+ verificationBuilder.append("<timestamp xmlns=\"").append(PidfConstant.NAMESPACE).append("\">")
+ .append(timestamp).append("</timestamp>");
+
+ assertTrue(result.contains(verificationBuilder.toString()));
+ }
+
+ @Test
+ @SmallTest
+ public void testParsing() throws Exception {
+ final String timestamp = Instant.now().toString();
+
+ StringBuilder timestampExample = new StringBuilder();
+ timestampExample.append("<?xml version='1.0' encoding='utf-8' standalone='yes' ?>")
+ .append("<timestamp xmlns=\"urn:ietf:params:xml:ns:pidf\">")
+ .append(timestamp)
+ .append("</timestamp>");
+
+ XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser();
+ parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
+ Reader reader = new StringReader(timestampExample.toString());
+ parser.setInput(reader);
+
+ Timestamp timestampElement = null;
+ int nextType = parser.next();
+
+ do {
+ // Find the start tag
+ if (nextType == XmlPullParser.START_TAG
+ && Timestamp.ELEMENT_NAME.equals(parser.getName())) {
+ timestampElement = new Timestamp();
+ timestampElement.parse(parser);
+ break;
+ }
+ nextType = parser.next();
+ } while(nextType != XmlPullParser.END_DOCUMENT);
+
+ reader.close();
+
+ assertNotNull(timestampElement);
+ assertEquals(timestamp, timestampElement.getValue());
+ }
+
+ private XmlSerializer getXmlSerializer(StringWriter writer)
+ throws XmlPullParserException, IOException {
+ XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
+ XmlSerializer serializer = factory.newSerializer();
+ serializer.setOutput(writer);
+ serializer.setPrefix("", PidfConstant.NAMESPACE);
+ return serializer;
+ }
+}
diff --git a/tests/src/com/android/ims/rcs/uce/presence/pidfparser/pidf/TupleTest.java b/tests/src/com/android/ims/rcs/uce/presence/pidfparser/pidf/TupleTest.java
new file mode 100644
index 00000000..3c44bd2b
--- /dev/null
+++ b/tests/src/com/android/ims/rcs/uce/presence/pidfparser/pidf/TupleTest.java
@@ -0,0 +1,238 @@
+/*
+ * 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.ims.rcs.uce.presence.pidfparser.pidf;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.ims.ImsTestBase;
+import com.android.ims.rcs.uce.presence.pidfparser.ElementBase;
+import com.android.ims.rcs.uce.presence.pidfparser.PidfParserConstant;
+import com.android.ims.rcs.uce.presence.pidfparser.PidfParserUtils;
+import com.android.ims.rcs.uce.presence.pidfparser.capabilities.Audio;
+import com.android.ims.rcs.uce.presence.pidfparser.capabilities.CapsConstant;
+import com.android.ims.rcs.uce.presence.pidfparser.capabilities.Duplex;
+import com.android.ims.rcs.uce.presence.pidfparser.capabilities.ServiceCaps;
+import com.android.ims.rcs.uce.presence.pidfparser.capabilities.Video;
+import com.android.ims.rcs.uce.presence.pidfparser.omapres.Description;
+import com.android.ims.rcs.uce.presence.pidfparser.omapres.OmaPresConstant;
+import com.android.ims.rcs.uce.presence.pidfparser.omapres.ServiceDescription;
+import com.android.ims.rcs.uce.presence.pidfparser.omapres.ServiceId;
+import com.android.ims.rcs.uce.presence.pidfparser.omapres.Version;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.time.Instant;
+import java.util.List;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+import org.xmlpull.v1.XmlSerializer;
+
+@RunWith(AndroidJUnit4.class)
+public class TupleTest extends ImsTestBase {
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ @Test
+ @SmallTest
+ public void testElementName() throws Exception {
+ Tuple tuple = new Tuple();
+
+ assertEquals(PidfConstant.NAMESPACE, tuple.getNamespace());
+ assertEquals(Tuple.ELEMENT_NAME, tuple.getElementName());
+ }
+
+ @Test
+ @SmallTest
+ public void testSerializing() throws Exception {
+ Tuple tuple = new Tuple();
+
+ Basic basic = new Basic(Basic.OPEN);
+ Status status = new Status();
+ status.setBasic(basic);
+ tuple.setStatus(status);
+
+ ServiceId serviceId = new ServiceId("service_id_001");
+ Version version = new Version(1, 0);
+ Description description = new Description("description test");
+ ServiceDescription serviceDescription = new ServiceDescription();
+ serviceDescription.setServiceId(serviceId);
+ serviceDescription.setVersion(version);
+ serviceDescription.setDescription(description);
+ tuple.setServiceDescription(serviceDescription);
+
+ Audio audio = new Audio(true);
+ Video video = new Video(true);
+ Duplex duplex = new Duplex();
+ duplex.addSupportedType(Duplex.DUPLEX_FULL);
+
+ ServiceCaps serviceCaps = new ServiceCaps();
+ serviceCaps.addElement(audio);
+ serviceCaps.addElement(video);
+ serviceCaps.addElement(duplex);
+ tuple.setServiceCaps(serviceCaps);
+
+ Note note = new Note("Note test");
+ tuple.addNote(note);
+
+ String nowTime = Instant.now().toString();
+ Timestamp timestamp = new Timestamp(nowTime);
+ tuple.setTimestamp(timestamp);
+
+ StringWriter writer = new StringWriter();
+ XmlSerializer serializer = getXmlSerializer(writer);
+
+ // Serializing
+ serializer.startDocument(PidfParserConstant.ENCODING_UTF_8, true);
+ tuple.serialize(serializer);
+ serializer.endDocument();
+ serializer.flush();
+
+ String result = writer.toString();
+
+ String verificationStatus = "<status><basic>open</basic></status>";
+ String verificationServiceId = "<op:service-id>service_id_001</op:service-id>";
+ String verificationVersion = "<op:version>1.0</op:version>";
+ String verificationDescription = "<op:description>description test</op:description>";
+ String verificationAudio = "<caps:audio>true</caps:audio>";
+ String verificationVideo = "<caps:video>true</caps:video>";
+ String verificationNote = "<note>Note test</note>";
+ String verificationTimestamp = "<timestamp>" + nowTime + "</timestamp>";
+
+ assertTrue(result.contains(verificationStatus));
+ assertTrue(result.contains(verificationServiceId));
+ assertTrue(result.contains(verificationVersion));
+ assertTrue(result.contains(verificationDescription));
+ assertTrue(result.contains(verificationAudio));
+ assertTrue(result.contains(verificationVideo));
+ assertTrue(result.contains(verificationNote));
+ assertTrue(result.contains(verificationTimestamp));
+ }
+
+ @Test
+ @SmallTest
+ public void testParsing() throws Exception {
+ final String status = Basic.OPEN;
+ final String serviceId = "service_id_001";
+ final String version = "1.0";
+ final String description = "description test";
+ final boolean audio = true;
+ final boolean video = true;
+ final String note = "note test";
+ final String nowTime = Instant.now().toString();
+
+ StringBuilder tupleExample = new StringBuilder();
+ tupleExample.append("<?xml version='1.0' encoding='utf-8' standalone='yes' ?>")
+ .append("<tuple id=\"tid0\" xmlns=\"urn:ietf:params:xml:ns:pidf\"")
+ .append(" xmlns:op=\"urn:oma:xml:prs:pidf:oma-pres\"")
+ .append(" xmlns:caps=\"urn:ietf:params:xml:ns:pidf:caps\">")
+ .append("<status><basic>").append(status).append("</basic></status>")
+ .append("<op:service-description>")
+ .append("<op:service-id>").append(serviceId).append("</op:service-id>")
+ .append("<op:version>").append(version).append("</op:version>")
+ .append("<op:description>").append(description).append("</op:description>")
+ .append("</op:service-description>")
+ .append("<caps:servcaps><caps:audio>").append(audio).append("</caps:audio>")
+ .append("<caps:video>").append(video).append("</caps:video>")
+ .append("<caps:duplex><caps:supported>")
+ .append("<caps:full />")
+ .append("</caps:supported></caps:duplex>")
+ .append("</caps:servcaps>")
+ .append("<note>").append(note).append("</note>")
+ .append("<timestamp>").append(nowTime).append("</timestamp></tuple>");
+
+ XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser();
+ parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
+ Reader reader = new StringReader(tupleExample.toString());
+ parser.setInput(reader);
+
+ Tuple tuple = null;
+ int nextType = parser.next();
+
+ // Find the start tag
+ do {
+ if (nextType == XmlPullParser.START_TAG
+ && Tuple.ELEMENT_NAME.equals(parser.getName())) {
+ tuple = new Tuple();
+ tuple.parse(parser);
+ break;
+ }
+ nextType = parser.next();
+ } while(nextType != XmlPullParser.END_DOCUMENT);
+
+ reader.close();
+
+ assertNotNull(tuple);
+ assertEquals(Basic.OPEN, PidfParserUtils.getTupleStatus(tuple));
+ assertEquals(serviceId, PidfParserUtils.getTupleServiceId(tuple));
+ assertEquals(version, PidfParserUtils.getTupleServiceVersion(tuple));
+ assertEquals(description, PidfParserUtils.getTupleServiceDescription(tuple));
+
+ boolean resultAudio = false;
+ boolean resultVideo = false;
+ List<ElementBase> elements = tuple.getServiceCaps().getElements();
+ for (ElementBase element : elements) {
+ if (element instanceof Audio) {
+ resultAudio = ((Audio) element).isAudioSupported();
+ } else if (element instanceof Video) {
+ resultVideo = ((Video) element).isVideoSupported();
+ }
+ }
+ assertTrue(resultAudio);
+ assertTrue(resultVideo);
+
+ String resultNote = null;
+ List<Note> noteList = tuple.getNoteList();
+ if (noteList != null && !noteList.isEmpty()) {
+ resultNote = noteList.get(0).getNote();
+ }
+
+ assertTrue(note.equals(resultNote));
+ assertEquals(nowTime, PidfParserUtils.getTupleTimestamp(tuple));
+ }
+
+ private XmlSerializer getXmlSerializer(StringWriter writer)
+ throws XmlPullParserException, IOException {
+ XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
+ XmlSerializer serializer = factory.newSerializer();
+ serializer.setOutput(writer);
+ serializer.setPrefix("", PidfConstant.NAMESPACE);
+ serializer.setPrefix("op", OmaPresConstant.NAMESPACE);
+ serializer.setPrefix("caps", CapsConstant.NAMESPACE);
+ return serializer;
+ }
+}
diff --git a/tests/src/com/android/ims/rcs/uce/presence/publish/DeviceCapabilityListenerTest.java b/tests/src/com/android/ims/rcs/uce/presence/publish/DeviceCapabilityListenerTest.java
new file mode 100644
index 00000000..bf33103f
--- /dev/null
+++ b/tests/src/com/android/ims/rcs/uce/presence/publish/DeviceCapabilityListenerTest.java
@@ -0,0 +1,246 @@
+/*
+ * 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.ims.rcs.uce.presence.publish;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.verify;
+
+import android.content.BroadcastReceiver;
+import android.content.Intent;
+import android.os.Handler;
+import android.telecom.TelecomManager;
+import android.telephony.ims.ImsMmTelManager;
+import android.telephony.ims.ImsRcsManager;
+import android.telephony.ims.ImsReasonInfo;
+import android.telephony.ims.ImsRegistrationAttributes;
+import android.telephony.ims.ProvisioningManager;
+import android.telephony.ims.RegistrationManager.RegistrationCallback;
+import android.telephony.ims.feature.MmTelFeature;
+import android.telephony.ims.stub.ImsRegistrationImplBase;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.ims.ImsTestBase;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+@RunWith(AndroidJUnit4.class)
+public class DeviceCapabilityListenerTest extends ImsTestBase {
+
+ private static final long HANDLER_WAIT_TIMEOUT_MS = 2000L;
+ private static final long HANDLER_SENT_DELAY_MS = 1000L;
+
+ @Mock DeviceCapabilityInfo mDeviceCapability;
+ @Mock PublishController.PublishControllerCallback mCallback;
+ @Mock ImsMmTelManager mImsMmTelManager;
+ @Mock ImsRcsManager mImsRcsManager;
+ @Mock ProvisioningManager mProvisioningManager;
+ @Mock DeviceCapabilityListener.ImsMmTelManagerFactory mImsMmTelMgrFactory;
+ @Mock DeviceCapabilityListener.ImsRcsManagerFactory mImsRcsMgrFactory;
+ @Mock DeviceCapabilityListener.ProvisioningManagerFactory mProvisioningMgrFactory;
+
+ int mSubId = 1;
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ doReturn(mImsMmTelManager).when(mImsMmTelMgrFactory).getImsMmTelManager(anyInt());
+ doReturn(mImsRcsManager).when(mImsRcsMgrFactory).getImsRcsManager(anyInt());
+ doReturn(mProvisioningManager).when(mProvisioningMgrFactory).
+ getProvisioningManager(anyInt());
+
+ doReturn(true).when(mDeviceCapability).updateTtyPreferredMode(anyInt());
+ doReturn(true).when(mDeviceCapability).updateAirplaneMode(anyBoolean());
+ doReturn(true).when(mDeviceCapability).updateMobileData(anyBoolean());
+ doReturn(true).when(mDeviceCapability).updateVtSetting(anyBoolean());
+ doReturn(true).when(mDeviceCapability).updateVtSetting(anyBoolean());
+ doReturn(true).when(mDeviceCapability).updateMmtelCapabilitiesChanged(any());
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ @Test
+ @SmallTest
+ public void testTurnOnListener() throws Exception {
+ DeviceCapabilityListener deviceCapListener = createDeviceCapabilityListener();
+
+ deviceCapListener.initialize();
+
+ verify(mContext).registerReceiver(any(), any());
+ verify(mProvisioningManager).registerProvisioningChangedCallback(any(), any());
+ }
+
+ @Test
+ @SmallTest
+ public void testDestroy() throws Exception {
+ DeviceCapabilityListener deviceCapListener = createDeviceCapabilityListener();
+ deviceCapListener.initialize();
+
+ // The listener is destroyed.
+ deviceCapListener.onDestroy();
+
+ verify(mContext).unregisterReceiver(any());
+ verify(mProvisioningManager).unregisterProvisioningChangedCallback(any());
+ }
+
+ @Test
+ @SmallTest
+ public void testTtyPreferredModeChange() throws Exception {
+ DeviceCapabilityListener deviceCapListener = createDeviceCapabilityListener();
+ final BroadcastReceiver receiver = deviceCapListener.mReceiver;
+
+ Intent intent = new Intent(TelecomManager.ACTION_TTY_PREFERRED_MODE_CHANGED);
+ receiver.onReceive(mContext, intent);
+
+ Handler handler = deviceCapListener.getHandler();
+ waitForHandlerActionDelayed(handler, HANDLER_WAIT_TIMEOUT_MS, HANDLER_SENT_DELAY_MS);
+
+ verify(mDeviceCapability).updateTtyPreferredMode(anyInt());
+ verify(mCallback).requestPublishFromInternal(
+ PublishController.PUBLISH_TRIGGER_TTY_PREFERRED_CHANGE);
+ }
+
+ @Test
+ @SmallTest
+ public void testAirplaneModeChange() throws Exception {
+ DeviceCapabilityListener deviceCapListener = createDeviceCapabilityListener();
+ final BroadcastReceiver receiver = deviceCapListener.mReceiver;
+
+ Intent intent = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
+ receiver.onReceive(mContext, intent);
+
+ Handler handler = deviceCapListener.getHandler();
+ waitForHandlerActionDelayed(handler, HANDLER_WAIT_TIMEOUT_MS, HANDLER_SENT_DELAY_MS);
+
+ verify(mDeviceCapability).updateAirplaneMode(anyBoolean());
+ verify(mCallback).requestPublishFromInternal(
+ PublishController.PUBLISH_TRIGGER_AIRPLANE_MODE_CHANGE);
+ }
+
+ @Test
+ @SmallTest
+ public void testMmtelRegistration() throws Exception {
+ DeviceCapabilityListener deviceCapListener = createDeviceCapabilityListener();
+ deviceCapListener.setImsCallbackRegistered(true);
+ RegistrationCallback registrationCallback = deviceCapListener.mMmtelRegistrationCallback;
+
+ registrationCallback.onRegistered(1);
+
+ Handler handler = deviceCapListener.getHandler();
+ waitForHandlerActionDelayed(handler, HANDLER_WAIT_TIMEOUT_MS, HANDLER_SENT_DELAY_MS);
+
+ verify(mDeviceCapability).updateImsMmtelRegistered(1);
+ verify(mCallback).requestPublishFromInternal(
+ PublishController.PUBLISH_TRIGGER_MMTEL_REGISTERED);
+ }
+
+ @Test
+ @SmallTest
+ public void testMmTelUnregistration() throws Exception {
+ DeviceCapabilityListener deviceCapListener = createDeviceCapabilityListener();
+ deviceCapListener.setImsCallbackRegistered(true);
+ RegistrationCallback registrationCallback = deviceCapListener.mMmtelRegistrationCallback;
+
+ ImsReasonInfo info = new ImsReasonInfo(ImsReasonInfo.CODE_LOCAL_NOT_REGISTERED, -1, "");
+ registrationCallback.onUnregistered(info);
+
+ Handler handler = deviceCapListener.getHandler();
+ waitForHandlerActionDelayed(handler, HANDLER_WAIT_TIMEOUT_MS, HANDLER_SENT_DELAY_MS);
+
+ verify(mDeviceCapability).updateImsMmtelUnregistered();
+ verify(mCallback).requestPublishFromInternal(
+ PublishController.PUBLISH_TRIGGER_MMTEL_UNREGISTERED);
+ }
+
+ @Test
+ @SmallTest
+ public void testRcsRegistration() throws Exception {
+ DeviceCapabilityListener deviceCapListener = createDeviceCapabilityListener();
+ deviceCapListener.setImsCallbackRegistered(true);
+ RegistrationCallback registrationCallback = deviceCapListener.mRcsRegistrationCallback;
+ ImsRegistrationAttributes attr = new ImsRegistrationAttributes.Builder(
+ ImsRegistrationImplBase.REGISTRATION_TECH_LTE).build();
+ // Notify DeviceCapabilityListener that registered has caused a change and requires publish
+ doReturn(true).when(mDeviceCapability).updateImsRcsRegistered(attr);
+
+ registrationCallback.onRegistered(attr);
+ Handler handler = deviceCapListener.getHandler();
+ waitForHandlerActionDelayed(handler, HANDLER_WAIT_TIMEOUT_MS, HANDLER_SENT_DELAY_MS);
+
+ verify(mDeviceCapability).updateImsRcsRegistered(attr);
+ verify(mCallback).requestPublishFromInternal(
+ PublishController.PUBLISH_TRIGGER_RCS_REGISTERED);
+ }
+
+ @Test
+ @SmallTest
+ public void testRcsUnregistration() throws Exception {
+ DeviceCapabilityListener deviceCapListener = createDeviceCapabilityListener();
+ deviceCapListener.setImsCallbackRegistered(true);
+ RegistrationCallback registrationCallback = deviceCapListener.mRcsRegistrationCallback;
+ // Notify DeviceCapabilityListener that unregistered has caused a change and requires
+ // publish.
+ doReturn(true).when(mDeviceCapability).updateImsRcsUnregistered();
+
+ ImsReasonInfo info = new ImsReasonInfo(ImsReasonInfo.CODE_LOCAL_NOT_REGISTERED, -1, "");
+ registrationCallback.onUnregistered(info);
+
+ Handler handler = deviceCapListener.getHandler();
+ waitForHandlerActionDelayed(handler, HANDLER_WAIT_TIMEOUT_MS, HANDLER_SENT_DELAY_MS);
+
+ verify(mDeviceCapability).updateImsRcsUnregistered();
+ verify(mCallback).requestPublishFromInternal(
+ PublishController.PUBLISH_TRIGGER_RCS_UNREGISTERED);
+ }
+
+ @Test
+ @SmallTest
+ public void testMmtelCapabilityChange() throws Exception {
+ DeviceCapabilityListener deviceCapListener = createDeviceCapabilityListener();
+ ImsMmTelManager.CapabilityCallback callback = deviceCapListener.mMmtelCapabilityCallback;
+
+ MmTelFeature.MmTelCapabilities capabilities = new MmTelFeature.MmTelCapabilities();
+ callback.onCapabilitiesStatusChanged(capabilities);
+
+ Handler handler = deviceCapListener.getHandler();
+ waitForHandlerActionDelayed(handler, HANDLER_WAIT_TIMEOUT_MS, HANDLER_SENT_DELAY_MS);
+
+ verify(mDeviceCapability).updateMmtelCapabilitiesChanged(capabilities);
+ verify(mCallback).requestPublishFromInternal(
+ PublishController.PUBLISH_TRIGGER_MMTEL_CAPABILITY_CHANGE);
+ }
+
+ private DeviceCapabilityListener createDeviceCapabilityListener() {
+ DeviceCapabilityListener deviceCapListener = new DeviceCapabilityListener(mContext,
+ mSubId, mDeviceCapability, mCallback);
+ deviceCapListener.setImsMmTelManagerFactory(mImsMmTelMgrFactory);
+ deviceCapListener.setImsRcsManagerFactory(mImsRcsMgrFactory);
+ deviceCapListener.setProvisioningMgrFactory(mProvisioningMgrFactory);
+ return deviceCapListener;
+ }
+}
diff --git a/tests/src/com/android/ims/rcs/uce/presence/publish/PublishControllerImplTest.java b/tests/src/com/android/ims/rcs/uce/presence/publish/PublishControllerImplTest.java
new file mode 100644
index 00000000..b4c9b873
--- /dev/null
+++ b/tests/src/com/android/ims/rcs/uce/presence/publish/PublishControllerImplTest.java
@@ -0,0 +1,357 @@
+/*
+ * 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.ims.rcs.uce.presence.publish;
+
+import static com.android.ims.rcs.uce.presence.publish.PublishController.PUBLISH_TRIGGER_RETRY;
+import static com.android.ims.rcs.uce.presence.publish.PublishController.PUBLISH_TRIGGER_VT_SETTING_CHANGE;
+
+import static junit.framework.Assert.assertFalse;
+
+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.doReturn;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.os.RemoteCallbackList;
+import android.telephony.ims.RcsUceAdapter;
+import android.telephony.ims.aidl.IImsCapabilityCallback;
+import android.telephony.ims.aidl.IRcsUcePublishStateCallback;
+import android.telephony.ims.feature.RcsFeature.RcsImsCapabilities;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.ims.RcsFeatureManager;
+import com.android.ims.rcs.uce.UceController;
+import com.android.ims.rcs.uce.UceDeviceState.DeviceStateResult;
+import com.android.ims.rcs.uce.presence.publish.PublishController.PublishControllerCallback;
+import com.android.ims.rcs.uce.presence.publish.PublishControllerImpl.DeviceCapListenerFactory;
+import com.android.ims.rcs.uce.presence.publish.PublishControllerImpl.PublishProcessorFactory;
+import com.android.ims.ImsTestBase;
+
+import java.time.Instant;
+import java.util.Optional;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+@RunWith(AndroidJUnit4.class)
+public class PublishControllerImplTest extends ImsTestBase {
+
+ @Mock RcsFeatureManager mFeatureManager;
+ @Mock PublishProcessor mPublishProcessor;
+ @Mock PublishProcessorFactory mPublishProcessorFactory;
+ @Mock DeviceCapabilityListener mDeviceCapListener;
+ @Mock DeviceCapListenerFactory mDeviceCapListenerFactory;
+ @Mock UceController.UceControllerCallback mUceCtrlCallback;
+ @Mock RemoteCallbackList<IRcsUcePublishStateCallback> mPublishStateCallbacks;
+ @Mock DeviceStateResult mDeviceStateResult;
+
+ private int mSubId = 1;
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ doReturn(mPublishProcessor).when(mPublishProcessorFactory).createPublishProcessor(any(),
+ eq(mSubId), any(), any());
+ doReturn(mDeviceCapListener).when(mDeviceCapListenerFactory).createDeviceCapListener(any(),
+ eq(mSubId), any(), any());
+ doReturn(mDeviceStateResult).when(mUceCtrlCallback).getDeviceState();
+ doReturn(false).when(mDeviceStateResult).isRequestForbidden();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ @Test
+ @SmallTest
+ public void testRcsConnected() throws Exception {
+ PublishControllerImpl publishController = createPublishController();
+
+ publishController.onRcsConnected(mFeatureManager);
+ Handler handler = publishController.getPublishHandler();
+ waitForHandlerAction(handler, 1000);
+
+ verify(mPublishProcessor).onRcsConnected(mFeatureManager);
+ }
+
+ @Test
+ @SmallTest
+ public void testRcsDisconnected() throws Exception {
+ PublishControllerImpl publishController = createPublishController();
+
+ publishController.onRcsDisconnected();
+ Handler handler = publishController.getPublishHandler();
+ waitForHandlerAction(handler, 1000);
+
+ verify(mPublishProcessor).onRcsDisconnected();
+ }
+
+ @Test
+ @SmallTest
+ public void testDestroyed() throws Exception {
+ PublishControllerImpl publishController = createPublishController();
+
+ publishController.onDestroy();
+
+ verify(mPublishProcessor, never()).doPublish(anyInt());
+ }
+
+ @Test
+ @SmallTest
+ public void testGetPublishState() throws Exception {
+ PublishControllerImpl publishController = createPublishController();
+
+ int initState = publishController.getUcePublishState();
+ assertEquals(RcsUceAdapter.PUBLISH_STATE_NOT_PUBLISHED, initState);
+
+ publishController.getPublishControllerCallback().updatePublishRequestResult(
+ RcsUceAdapter.PUBLISH_STATE_OK, Instant.now(), null);
+ Handler handler = publishController.getPublishHandler();
+ waitForHandlerAction(handler, 1000);
+
+ int latestState = publishController.getUcePublishState();
+ assertEquals(RcsUceAdapter.PUBLISH_STATE_OK, latestState);
+ }
+
+ @Test
+ @SmallTest
+ public void testRegisterPublishStateCallback() throws Exception {
+ PublishControllerImpl publishController = createPublishController();
+
+ publishController.registerPublishStateCallback(any());
+
+ verify(mPublishStateCallbacks).register(any());
+ }
+
+ @Test
+ @SmallTest
+ public void unregisterPublishStateCallback() throws Exception {
+ PublishControllerImpl publishController = createPublishController();
+
+ publishController.unregisterPublishStateCallback(any());
+
+ verify(mPublishStateCallbacks).unregister(any());
+ }
+
+ @Test
+ @SmallTest
+ public void testUnpublish() throws Exception {
+ PublishControllerImpl publishController = createPublishController();
+
+ publishController.onUnpublish();
+
+ Handler handler = publishController.getPublishHandler();
+ waitForHandlerAction(handler, 1000);
+ int publishState = publishController.getUcePublishState();
+ assertEquals(RcsUceAdapter.PUBLISH_STATE_NOT_PUBLISHED, publishState);
+ }
+
+ @Test
+ @SmallTest
+ public void testRequestPublishFromServiceWithoutRcsPresenceCapability() throws Exception {
+ PublishControllerImpl publishController = createPublishController();
+
+ // Trigger the PUBLISH request from the service
+ publishController.requestPublishCapabilitiesFromService(
+ RcsUceAdapter.CAPABILITY_UPDATE_TRIGGER_MOVE_TO_IWLAN);
+
+ Handler handler = publishController.getPublishHandler();
+ waitForHandlerAction(handler, 1000);
+ verify(mPublishProcessor, never()).doPublish(PublishController.PUBLISH_TRIGGER_SERVICE);
+
+ IImsCapabilityCallback callback = publishController.getRcsCapabilitiesCallback();
+ callback.onCapabilitiesStatusChanged(RcsUceAdapter.CAPABILITY_TYPE_PRESENCE_UCE);
+ waitForHandlerAction(handler, 1000);
+
+ verify(mPublishProcessor).checkAndSendPendingRequest();
+ }
+
+ @Test
+ @SmallTest
+ public void testRequestPublishFromServiceWithRcsCapability() throws Exception {
+ PublishControllerImpl publishController = createPublishController();
+ doReturn(Optional.of(0L)).when(mPublishProcessor).getPublishingDelayTime();
+
+ // Set the PRESENCE is capable
+ IImsCapabilityCallback RcsCapCallback = publishController.getRcsCapabilitiesCallback();
+ RcsCapCallback.onCapabilitiesStatusChanged(RcsUceAdapter.CAPABILITY_TYPE_PRESENCE_UCE);
+
+ // Trigger the PUBLISH request from the service.
+ publishController.requestPublishCapabilitiesFromService(
+ RcsUceAdapter.CAPABILITY_UPDATE_TRIGGER_MOVE_TO_IWLAN);
+
+ Handler handler = publishController.getPublishHandler();
+ waitForHandlerAction(handler, 1000);
+ verify(mPublishProcessor).doPublish(PublishController.PUBLISH_TRIGGER_SERVICE);
+ }
+
+ @Test
+ @SmallTest
+ public void testFirstRequestPublishIsTriggeredFromService() throws Exception {
+ PublishControllerImpl publishController = createPublishController();
+ doReturn(Optional.of(0L)).when(mPublishProcessor).getPublishingDelayTime();
+
+ // Set the PRESENCE is capable
+ IImsCapabilityCallback RcsCapCallback = publishController.getRcsCapabilitiesCallback();
+ RcsCapCallback.onCapabilitiesStatusChanged(RcsUceAdapter.CAPABILITY_TYPE_PRESENCE_UCE);
+
+ // Trigger a publish request (VT changes)
+ PublishControllerCallback callback = publishController.getPublishControllerCallback();
+ callback.requestPublishFromInternal(PUBLISH_TRIGGER_VT_SETTING_CHANGE);
+ Handler handler = publishController.getPublishHandler();
+ waitForHandlerAction(handler, 1000);
+
+ // Verify it cannot be processed because the first request should triggred from service.
+ verify(mPublishProcessor, never()).doPublish(PUBLISH_TRIGGER_VT_SETTING_CHANGE);
+
+ // Trigger the PUBLISH request from the service.
+ publishController.requestPublishCapabilitiesFromService(
+ RcsUceAdapter.CAPABILITY_UPDATE_TRIGGER_MOVE_TO_IWLAN);
+ waitForHandlerAction(handler, 1000);
+
+ // Verify the request which is from the service can be processed
+ verify(mPublishProcessor).doPublish(PublishController.PUBLISH_TRIGGER_SERVICE);
+
+ // Trigger the third publish request (VT changes)
+ callback.requestPublishFromInternal(PUBLISH_TRIGGER_VT_SETTING_CHANGE);
+ waitForHandlerAction(handler, 1000);
+
+ // Verify the publish request can be processed this time.
+ verify(mPublishProcessor).doPublish(PublishController.PUBLISH_TRIGGER_VT_SETTING_CHANGE);
+ }
+
+ @Test
+ @SmallTest
+ public void testRequestPublishWhenDeviceCapabilitiesChange() throws Exception {
+ PublishControllerImpl publishController = createPublishController();
+ doReturn(Optional.of(0L)).when(mPublishProcessor).getPublishingDelayTime();
+
+ // Set the PRESENCE is capable
+ IImsCapabilityCallback RcsCapCallback = publishController.getRcsCapabilitiesCallback();
+ RcsCapCallback.onCapabilitiesStatusChanged(RcsUceAdapter.CAPABILITY_TYPE_PRESENCE_UCE);
+
+ // Trigger the PUBLISH request from the service.
+ publishController.requestPublishCapabilitiesFromService(
+ RcsUceAdapter.CAPABILITY_UPDATE_TRIGGER_MOVE_TO_IWLAN);
+ Handler handler = publishController.getPublishHandler();
+ waitForHandlerAction(handler, 1000);
+
+ // Verify the request which is from the service can be processed
+ verify(mPublishProcessor).doPublish(PublishController.PUBLISH_TRIGGER_SERVICE);
+
+ // Trigger the sedond publish (RETRY), it should be processed after 10 seconds.
+ PublishControllerCallback callback = publishController.getPublishControllerCallback();
+ callback.requestPublishFromInternal(PUBLISH_TRIGGER_RETRY);
+
+ // Trigger another publish request (VT changes)
+ callback.requestPublishFromInternal(PUBLISH_TRIGGER_VT_SETTING_CHANGE);
+ waitForHandlerAction(handler, 1000);
+
+ // Verify the publish request can be processed immediately
+ verify(mPublishProcessor).doPublish(PUBLISH_TRIGGER_VT_SETTING_CHANGE);
+ }
+
+ @Test
+ @SmallTest
+ public void testRemoveNumber() {
+ // Contrived example, usually formatting of URIs will be consistent in doc.
+ final String testString = "<?xml version='1.0' encoding='utf-8' standalone='yes' "
+ + "?><presence entity=\"sip:15555551212@example.com\" "
+ + "xmlns=\"urn:ietf:params:xml:ns:pidf\" "
+ + "xmlns:op=\"urn:oma:xml:prs:pidf:oma-pres\" "
+ + "xmlns:caps=\"urn:ietf:params:xml:ns:pidf:caps\"><tuple "
+ + "id=\"tid0\"><status><basic>open</basic></status><op:service-description><op"
+ + ":service-id>org.3gpp.urn:urn-7:3gpp-application.ims.iari.rcse"
+ + ".dp</op:service-id><op:version>1.0</op:version><op:description>Capabilities "
+ + "Discovery Service</op:description></op:service-description><contact>sips"
+ + ":15555551212@example.com</contact></tuple><tuple "
+ + "id=\"tid1\"><status><basic>open</basic></status><op:service-description><op"
+ + ":service-id>org.3gpp.urn:urn-7:3gpp-service.ims.icsi"
+ + ".mmtel</op:service-id><op:version>1.0</op:version><op:description>Voice and "
+ + "Video Service</op:description></op:service-description><caps:servcaps><caps"
+ + ":audio>true</caps:audio><caps:video>true</caps:video><caps:duplex><caps"
+ + ":supported><caps:full /></caps:supported></caps:duplex></caps:servcaps"
+ + "><contact>tel:15555551212@example.com</contact></tuple><tuple "
+ + "id=\"tid2\"><status><basic>open</basic></status><op:service-description><op"
+ + ":service-id>org.3gpp.urn:urn-7:3gpp-application.ims.iari.rcs"
+ + ".geopush</op:service-id><op:version>1"
+ + ".0</op:version></op:service-description><contact>sip:1-555-555-1212@example.com"
+ + "</contact></tuple><tuple "
+ + "id=\"tid3\"><status><basic>open</basic></status><op:service-description><op"
+ + ":service-id>org.openmobilealliance:File-Transfer-HTTP</op:service-id><op"
+ + ":version>1.0</op:version></op:service-description><contact>tel:1-555-555-1212@"
+ + "example.com</contact></tuple><tuple "
+ + "id=\"tid4\"><status><basic>open</basic></status><op:service-description><op"
+ + ":service-id>org.openmobilealliance:ChatSession</op:service-id><op:version>2"
+ + ".0</op:version></op:service-description><contact>sip:+15555551212@example.com"
+ + "</contact></tuple></presence>";
+ String result = PublishUtils.removeNumbersFromUris(testString);
+ // only check for substrings of the full number and variations.
+ assertFalse("still contained 5555551212: " + testString, result.contains("5555551212"));
+ assertFalse("still contained 555-555-1212: " + testString, result.contains("555-555-1212"));
+ }
+
+ @Test
+ @SmallTest
+ public void testNotPublishWhitSipOptions() throws Exception {
+ PublishControllerImpl publishController = createPublishController();
+ publishController.setCapabilityType(RcsImsCapabilities.CAPABILITY_TYPE_OPTIONS_UCE);
+ doReturn(Optional.of(0L)).when(mPublishProcessor).getPublishingDelayTime();
+
+ // Trigger a publish request (VT changes)
+ PublishControllerCallback callback = publishController.getPublishControllerCallback();
+ callback.requestPublishFromInternal(PUBLISH_TRIGGER_VT_SETTING_CHANGE);
+ Handler handler = publishController.getPublishHandler();
+ waitForHandlerAction(handler, 1000);
+
+ // Verify it cannot be processed because the capability type is SIP OPTIONS and the publish
+ // request is triggered from device changed
+ verify(mPublishProcessor, never()).doPublish(PUBLISH_TRIGGER_VT_SETTING_CHANGE);
+
+ // Set the PRESENCE is capable
+ IImsCapabilityCallback RcsCapCallback = publishController.getRcsCapabilitiesCallback();
+ RcsCapCallback.onCapabilitiesStatusChanged(RcsImsCapabilities.CAPABILITY_TYPE_PRESENCE_UCE);
+
+ // Trigger the PUBLISH request from the service.
+ publishController.requestPublishCapabilitiesFromService(
+ RcsUceAdapter.CAPABILITY_UPDATE_TRIGGER_MOVE_TO_IWLAN);
+ waitForHandlerAction(handler, 1000);
+
+ // Verify the request which is from the service can be processed
+ verify(mPublishProcessor).doPublish(PublishController.PUBLISH_TRIGGER_SERVICE);
+ }
+
+ private PublishControllerImpl createPublishController() {
+ PublishControllerImpl publishController = new PublishControllerImpl(mContext, mSubId,
+ mUceCtrlCallback, Looper.getMainLooper(), mDeviceCapListenerFactory,
+ mPublishProcessorFactory);
+ publishController.setPublishStateCallback(mPublishStateCallbacks);
+ publishController.setCapabilityType(RcsImsCapabilities.CAPABILITY_TYPE_PRESENCE_UCE);
+ return publishController;
+ }
+}
diff --git a/tests/src/com/android/ims/rcs/uce/presence/publish/PublishProcessorTest.java b/tests/src/com/android/ims/rcs/uce/presence/publish/PublishProcessorTest.java
new file mode 100644
index 00000000..d83158f5
--- /dev/null
+++ b/tests/src/com/android/ims/rcs/uce/presence/publish/PublishProcessorTest.java
@@ -0,0 +1,246 @@
+/*
+ * 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.ims.rcs.uce.presence.publish;
+
+import static android.telephony.ims.RcsContactPresenceTuple.TUPLE_BASIC_STATUS_OPEN;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import android.net.Uri;
+import android.telephony.ims.RcsContactPresenceTuple;
+import android.telephony.ims.RcsContactUceCapability;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.ims.ImsTestBase;
+import com.android.ims.RcsFeatureManager;
+import com.android.ims.rcs.uce.presence.publish.PublishController.PublishControllerCallback;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+@RunWith(AndroidJUnit4.class)
+public class PublishProcessorTest extends ImsTestBase {
+
+ @Mock RcsFeatureManager mRcsFeatureManager;
+ @Mock DeviceCapabilityInfo mDeviceCapabilities;
+ @Mock PublishControllerCallback mPublishCtrlCallback;
+ @Mock PublishProcessorState mProcessorState;
+ @Mock PublishRequestResponse mResponseCallback;
+
+ private int mSub = 1;
+ private long mTaskId = 1L;
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ doReturn(true).when(mProcessorState).isPublishAllowedAtThisTime();
+ doReturn(mTaskId).when(mProcessorState).getCurrentTaskId();
+
+ doReturn(true).when(mDeviceCapabilities).isImsRegistered();
+ RcsContactUceCapability capability = getRcsContactUceCapability();
+ doReturn(capability).when(mDeviceCapabilities).getDeviceCapabilities(anyInt(), any());
+
+ doReturn(mTaskId).when(mResponseCallback).getTaskId();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ @Test
+ @SmallTest
+ public void testDoPublish() throws Exception {
+ PublishProcessor publishProcessor = getPublishProcessor();
+
+ publishProcessor.doPublish(PublishController.PUBLISH_TRIGGER_SERVICE);
+
+ verify(mDeviceCapabilities).getDeviceCapabilities(anyInt(), any());
+ verify(mProcessorState).setPublishingFlag(true);
+ verify(mRcsFeatureManager).requestPublication(any(), any());
+ verify(mPublishCtrlCallback).setupRequestCanceledTimer(anyLong(), anyLong());
+ }
+
+ @Test
+ @SmallTest
+ public void testPublishWithoutResetRetryCount() throws Exception {
+ PublishProcessor publishProcessor = getPublishProcessor();
+
+ publishProcessor.doPublish(PublishController.PUBLISH_TRIGGER_RETRY);
+
+ verify(mProcessorState, never()).resetRetryCount();
+ }
+
+ @Test
+ @SmallTest
+ public void testNotPublishWhenImsNotRegistered() throws Exception {
+ doReturn(false).when(mDeviceCapabilities).isImsRegistered();
+ PublishProcessor publishProcessor = getPublishProcessor();
+
+ publishProcessor.doPublish(PublishController.PUBLISH_TRIGGER_RETRY);
+
+ verify(mRcsFeatureManager, never()).requestPublication(any(), any());
+ }
+
+ @Test
+ @SmallTest
+ public void testNotPublishWhenReachMaximumRetries() throws Exception {
+ doReturn(true).when(mProcessorState).isPublishingNow();
+ doReturn(mTaskId).when(mProcessorState).getCurrentTaskId();
+ doReturn(mTaskId).when(mResponseCallback).getTaskId();
+ doReturn(true).when(mResponseCallback).needRetry();
+ doReturn(true).when(mProcessorState).isReachMaximumRetries();
+ PublishProcessor publishProcessor = getPublishProcessor();
+
+ publishProcessor.onNetworkResponse(mResponseCallback);
+
+ verify(mPublishCtrlCallback).updatePublishRequestResult(anyInt(), any(), any());
+ verify(mResponseCallback).onDestroy();
+ verify(mProcessorState).setPublishingFlag(false);
+ verify(mPublishCtrlCallback).clearRequestCanceledTimer();
+ }
+
+ @Test
+ @SmallTest
+ public void testNotPublishWhenCurrentTimeNotAllowed() throws Exception {
+ doReturn(false).when(mProcessorState).isPublishAllowedAtThisTime();
+ PublishProcessor publishProcessor = getPublishProcessor();
+
+ publishProcessor.doPublish(PublishController.PUBLISH_TRIGGER_RETRY);
+
+ verify(mPublishCtrlCallback).requestPublishFromInternal(
+ eq(PublishController.PUBLISH_TRIGGER_RETRY));
+ verify(mRcsFeatureManager, never()).requestPublication(any(), any());
+ }
+
+ @Test
+ @SmallTest
+ public void testCommandErrorWithRetry() throws Exception {
+ doReturn(true).when(mProcessorState).isPublishingNow();
+ doReturn(mTaskId).when(mProcessorState).getCurrentTaskId();
+ doReturn(mTaskId).when(mResponseCallback).getTaskId();
+ doReturn(true).when(mResponseCallback).needRetry();
+ PublishProcessor publishProcessor = getPublishProcessor();
+
+ publishProcessor.onCommandError(mResponseCallback);
+
+ verify(mProcessorState).increaseRetryCount();
+ verify(mPublishCtrlCallback).requestPublishFromInternal(
+ eq(PublishController.PUBLISH_TRIGGER_RETRY));
+ verify(mResponseCallback).onDestroy();
+ verify(mProcessorState).setPublishingFlag(false);
+ verify(mPublishCtrlCallback).clearRequestCanceledTimer();
+ }
+
+ @Test
+ @SmallTest
+ public void testCommandErrorWithoutRetry() throws Exception {
+ doReturn(true).when(mProcessorState).isPublishingNow();
+ doReturn(mTaskId).when(mProcessorState).getCurrentTaskId();
+ doReturn(mTaskId).when(mResponseCallback).getTaskId();
+ doReturn(false).when(mResponseCallback).needRetry();
+ PublishProcessor publishProcessor = getPublishProcessor();
+
+ publishProcessor.onCommandError(mResponseCallback);
+
+ verify(mPublishCtrlCallback).updatePublishRequestResult(anyInt(), any(), any());
+ verify(mResponseCallback).onDestroy();
+ verify(mProcessorState).setPublishingFlag(false);
+ verify(mPublishCtrlCallback).clearRequestCanceledTimer();
+ }
+
+ @Test
+ @SmallTest
+ public void testNetworkResponseWithRetry() throws Exception {
+ doReturn(true).when(mProcessorState).isPublishingNow();
+ doReturn(mTaskId).when(mProcessorState).getCurrentTaskId();
+ doReturn(mTaskId).when(mResponseCallback).getTaskId();
+ doReturn(true).when(mResponseCallback).needRetry();
+ PublishProcessor publishProcessor = getPublishProcessor();
+
+ publishProcessor.onNetworkResponse(mResponseCallback);
+
+ verify(mProcessorState).increaseRetryCount();
+ verify(mPublishCtrlCallback).requestPublishFromInternal(
+ eq(PublishController.PUBLISH_TRIGGER_RETRY));
+ verify(mResponseCallback).onDestroy();
+ verify(mProcessorState).setPublishingFlag(false);
+ verify(mPublishCtrlCallback).clearRequestCanceledTimer();
+ }
+
+ @Test
+ @SmallTest
+ public void testNetworkResponseSuccessful() throws Exception {
+ doReturn(true).when(mProcessorState).isPublishingNow();
+ doReturn(mTaskId).when(mProcessorState).getCurrentTaskId();
+ doReturn(mTaskId).when(mResponseCallback).getTaskId();
+ doReturn(false).when(mResponseCallback).needRetry();
+ doReturn(true).when(mResponseCallback).isRequestSuccess();
+ PublishProcessor publishProcessor = getPublishProcessor();
+
+ publishProcessor.onNetworkResponse(mResponseCallback);
+
+ verify(mPublishCtrlCallback).updatePublishRequestResult(anyInt(), any(), any());
+ verify(mResponseCallback).onDestroy();
+ verify(mProcessorState).setPublishingFlag(false);
+ verify(mPublishCtrlCallback).clearRequestCanceledTimer();
+ }
+
+ @Test
+ @SmallTest
+ public void testCancelPublishRequest() throws Exception {
+ PublishProcessor publishProcessor = getPublishProcessor();
+
+ publishProcessor.cancelPublishRequest(mTaskId);
+
+ verify(mProcessorState).setPublishingFlag(false);
+ verify(mPublishCtrlCallback).clearRequestCanceledTimer();
+ }
+
+ private PublishProcessor getPublishProcessor() {
+ PublishProcessor publishProcessor = new PublishProcessor(mContext, mSub,
+ mDeviceCapabilities, mPublishCtrlCallback);
+ publishProcessor.setProcessorState(mProcessorState);
+ publishProcessor.onRcsConnected(mRcsFeatureManager);
+ return publishProcessor;
+ }
+
+ private RcsContactUceCapability getRcsContactUceCapability() {
+ Uri contact = Uri.fromParts("sip", "test", null);
+
+ RcsContactUceCapability.PresenceBuilder builder =
+ new RcsContactUceCapability.PresenceBuilder(contact,
+ RcsContactUceCapability.SOURCE_TYPE_CACHED,
+ RcsContactUceCapability.REQUEST_RESULT_FOUND);
+
+ RcsContactPresenceTuple.Builder tupleBuilder = new RcsContactPresenceTuple.Builder(
+ TUPLE_BASIC_STATUS_OPEN, RcsContactPresenceTuple.SERVICE_ID_MMTEL, "1.0");
+
+ builder.addCapabilityTuple(tupleBuilder.build());
+ return builder.build();
+ }
+}
diff --git a/tests/src/com/android/ims/rcs/uce/presence/publish/PublishServiceDescTrackerTest.java b/tests/src/com/android/ims/rcs/uce/presence/publish/PublishServiceDescTrackerTest.java
new file mode 100644
index 00000000..6d15946c
--- /dev/null
+++ b/tests/src/com/android/ims/rcs/uce/presence/publish/PublishServiceDescTrackerTest.java
@@ -0,0 +1,260 @@
+/*
+ * Copyright (C) 2021 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.ims.rcs.uce.presence.publish;
+
+import static org.junit.Assert.assertEquals;
+
+import android.telephony.ims.RcsContactPresenceTuple;
+import android.util.ArraySet;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.ims.rcs.uce.util.FeatureTags;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Set;
+
+@RunWith(AndroidJUnit4.class)
+public class PublishServiceDescTrackerTest {
+
+ public static final ServiceDescription TEST_SERVICE_DESC_1 = new ServiceDescription(
+ "org.test.test1", "1.0", "ABC");
+ public static final ServiceDescription TEST_SERVICE_DESC_2 = new ServiceDescription(
+ "org.test.test1", "2.0", "DEF");
+ public static final ServiceDescription TEST_OVERRIDE_MMTEL_DESC = new ServiceDescription(
+ RcsContactPresenceTuple.SERVICE_ID_MMTEL, "1.0", "ABC");
+
+ public static final String TEST_FEATURE_TAG_1 = "+g.test.test1=\"testing\"";
+ public static final String TEST_FEATURE_TAG_2A = "+g.test.test2=\"testing\"";
+ public static final String TEST_FEATURE_TAG_2B = "+g.test.testAdd";
+
+ public static final String[] TEST_OVERRIDE_CONFIG = new String[] {
+ "org.test.test1|1.0|ABC|+g.test.test1=\"testing\"",
+ "org.test.test1|2.0|DEF|+g.test.test2=\"testing\";+g.test.testAdd",
+ "org.test.test3|1.0|ABC|+g.test.test3",
+ // Modify MMTEL+video to have a different description
+ "org.3gpp.urn:urn-7:3gpp-service.ims.icsi.mmtel|1.0|ABC|+g.3gpp.icsi-ref=\""
+ + "urn%3Aurn-7%3A3gpp-service.ims.icsi.mmtel\";video"
+ };
+
+ public static final String TEST_FEATURE_TAG_FT_FORMAT =
+ " +g.3gpp.iari-ref = \" urn%3AuRN-7%3A3gpp-applicatION.ims.iari.rcs.fthttp \" ";
+
+ public static final String TEST_FEATURE_TAG_FT_SMS_FORMAT =
+ " +g.3gpp.iari-ref=\" urn%3Aurn-7%3A3gpp-application.iMS.iari.rcs.ftsms \"";
+
+ public static final String TEST_FEATURE_TAG_CHATBOT_FORMAT =
+ " +g.3gpp.iari-ref= \" Urn%3Aurn-7%3A3gpp-application.ims.iari.rcs.chatbot \"";
+
+ public static final String TEST_FEATURE_TAG_BOTVERSION_FORMAT =
+ "+g.gsma.rcs.botVersion =\" #=1 , #=2 \" ";
+
+ public static final String TEST_FEATURE_TAG_MMTEL_FORMAT =
+ " +g.3gpp.icsi-ref = \"urn%3Aurn-7%3A3gpp-servIce.ims.icsi.mmtel \" ";
+
+ public static final String TEST_FEATURE_TAG_VIDEO_FORMAT = " VIDEO ";
+
+ public static final String[] TEST_OVERRIDE_CONFIG_FORMAT = new String[] {
+ " org.test.test1 | 1.0 | ABC | +g.test.tEST1= \" testing \" ",
+ " org.test.test1 |2.0 |DEF | +g.teSt.test2 = \"testing\" ; +g.test.testAdd ",
+ " org.test.test3 | 1.0 |ABC| +g.TEst.test3 ",
+ // Modify MMTEL+video to have a different description
+ " org.3gpp.urn:urn-7:3gpp-service.ims.icsi.mmtel | 1.0 | ABC | +g.3gPp.icsi-ref=\""
+ + "urn%3Aurn-7%3A3gpp-seRVice.ims.icsi.mmtel \" ; video "
+ };
+
+ @SmallTest
+ @Test
+ public void testDefaultConfigMatch() {
+ PublishServiceDescTracker t1 =
+ PublishServiceDescTracker.fromCarrierConfig(new String[]{});
+
+ Set<ServiceDescription> expectedSet = Collections.singleton(
+ ServiceDescription.SERVICE_DESCRIPTION_FT);
+ Set<String> imsReg = createImsRegistration(FeatureTags.FEATURE_TAG_FILE_TRANSFER);
+ t1.updateImsRegistration(imsReg);
+ assertEquals(expectedSet, t1.copyRegistrationCapabilities());
+
+ expectedSet = new ArraySet<>(Arrays.asList(
+ ServiceDescription.SERVICE_DESCRIPTION_FT,
+ ServiceDescription.SERVICE_DESCRIPTION_FT_SMS));
+ imsReg = createImsRegistration(FeatureTags.FEATURE_TAG_FILE_TRANSFER,
+ FeatureTags.FEATURE_TAG_FILE_TRANSFER_VIA_SMS);
+ t1.updateImsRegistration(imsReg);
+ assertEquals(expectedSet, t1.copyRegistrationCapabilities());
+
+ // Should see chatbot v1 and v2 pop up in this case (same FTs)
+ expectedSet = new ArraySet<>(Arrays.asList(
+ ServiceDescription.SERVICE_DESCRIPTION_CHATBOT_SESSION,
+ ServiceDescription.SERVICE_DESCRIPTION_CHATBOT_SESSION_V2));
+ imsReg = createImsRegistration(
+ FeatureTags.FEATURE_TAG_CHATBOT_COMMUNICATION_USING_SESSION,
+ FeatureTags.FEATURE_TAG_CHATBOT_VERSION_SUPPORTED);
+ t1.updateImsRegistration(imsReg);
+ assertEquals(expectedSet, t1.copyRegistrationCapabilities());
+
+ expectedSet = Collections.singleton(
+ ServiceDescription.SERVICE_DESCRIPTION_MMTEL_VOICE);
+ imsReg = createImsRegistration(FeatureTags.FEATURE_TAG_MMTEL);
+ t1.updateImsRegistration(imsReg);
+ assertEquals(expectedSet, t1.copyRegistrationCapabilities());
+
+ expectedSet = Collections.singleton(
+ ServiceDescription.SERVICE_DESCRIPTION_MMTEL_VOICE_VIDEO);
+ imsReg = createImsRegistration(
+ FeatureTags.FEATURE_TAG_MMTEL,
+ FeatureTags.FEATURE_TAG_VIDEO);
+ t1.updateImsRegistration(imsReg);
+ assertEquals(expectedSet, t1.copyRegistrationCapabilities());
+ }
+
+ @SmallTest
+ @Test
+ public void testOverrideCarrierConfigMatch() {
+ PublishServiceDescTracker t1 =
+ PublishServiceDescTracker.fromCarrierConfig(TEST_OVERRIDE_CONFIG);
+
+ Set<ServiceDescription> expectedSet = Collections.singleton(
+ ServiceDescription.SERVICE_DESCRIPTION_FT);
+ Set<String> imsReg = createImsRegistration(FeatureTags.FEATURE_TAG_FILE_TRANSFER);
+ t1.updateImsRegistration(imsReg);
+ assertEquals(expectedSet, t1.copyRegistrationCapabilities());
+
+ expectedSet = new ArraySet<>(Arrays.asList(
+ ServiceDescription.SERVICE_DESCRIPTION_FT, TEST_SERVICE_DESC_1));
+ imsReg = createImsRegistration(FeatureTags.FEATURE_TAG_FILE_TRANSFER,
+ TEST_FEATURE_TAG_1);
+ t1.updateImsRegistration(imsReg);
+ assertEquals(expectedSet, t1.copyRegistrationCapabilities());
+
+ // Test overrides also allow for multiple FT specifications
+ expectedSet = new ArraySet<>(Arrays.asList(
+ ServiceDescription.SERVICE_DESCRIPTION_CALL_COMPOSER_MMTEL, TEST_SERVICE_DESC_2));
+ imsReg = createImsRegistration(
+ FeatureTags.FEATURE_TAG_CALL_COMPOSER_VIA_TELEPHONY,
+ TEST_FEATURE_TAG_2B, TEST_FEATURE_TAG_2A);
+ t1.updateImsRegistration(imsReg);
+ assertEquals(expectedSet, t1.copyRegistrationCapabilities());
+
+ // Test override does not affect mmtel voice
+ expectedSet = Collections.singleton(
+ ServiceDescription.SERVICE_DESCRIPTION_MMTEL_VOICE);
+ imsReg = createImsRegistration(FeatureTags.FEATURE_TAG_MMTEL);
+ t1.updateImsRegistration(imsReg);
+ assertEquals(expectedSet, t1.copyRegistrationCapabilities());
+
+ // Test override description works for existing tags
+ expectedSet = Collections.singleton(TEST_OVERRIDE_MMTEL_DESC);
+ imsReg = createImsRegistration(
+ FeatureTags.FEATURE_TAG_MMTEL,
+ FeatureTags.FEATURE_TAG_VIDEO);
+ t1.updateImsRegistration(imsReg);
+ assertEquals(expectedSet, t1.copyRegistrationCapabilities());
+ }
+
+ @SmallTest
+ @Test
+ public void testNonstandardImsRegFormatMatch() {
+ PublishServiceDescTracker t1 =
+ PublishServiceDescTracker.fromCarrierConfig(new String[]{});
+
+ Set<ServiceDescription> expectedSet = new ArraySet<>(Arrays.asList(
+ ServiceDescription.SERVICE_DESCRIPTION_FT,
+ ServiceDescription.SERVICE_DESCRIPTION_FT_SMS));
+ Set<String> imsReg = createImsRegistration(TEST_FEATURE_TAG_FT_FORMAT,
+ TEST_FEATURE_TAG_FT_SMS_FORMAT);
+ t1.updateImsRegistration(imsReg);
+ assertEquals(expectedSet, t1.copyRegistrationCapabilities());
+
+ // Should see chatbot v1 and v2 pop up in this case (same FTs)
+ expectedSet = new ArraySet<>(Arrays.asList(
+ ServiceDescription.SERVICE_DESCRIPTION_CHATBOT_SESSION,
+ ServiceDescription.SERVICE_DESCRIPTION_CHATBOT_SESSION_V2));
+ imsReg = createImsRegistration(
+ TEST_FEATURE_TAG_CHATBOT_FORMAT,
+ TEST_FEATURE_TAG_BOTVERSION_FORMAT);
+ t1.updateImsRegistration(imsReg);
+ assertEquals(expectedSet, t1.copyRegistrationCapabilities());
+
+ expectedSet = Collections.singleton(
+ ServiceDescription.SERVICE_DESCRIPTION_MMTEL_VOICE);
+ imsReg = createImsRegistration(TEST_FEATURE_TAG_MMTEL_FORMAT);
+ t1.updateImsRegistration(imsReg);
+ assertEquals(expectedSet, t1.copyRegistrationCapabilities());
+
+ expectedSet = Collections.singleton(
+ ServiceDescription.SERVICE_DESCRIPTION_MMTEL_VOICE_VIDEO);
+ imsReg = createImsRegistration(
+ TEST_FEATURE_TAG_MMTEL_FORMAT,
+ TEST_FEATURE_TAG_VIDEO_FORMAT);
+ t1.updateImsRegistration(imsReg);
+ assertEquals(expectedSet, t1.copyRegistrationCapabilities());
+ }
+
+ @SmallTest
+ @Test
+ public void testOverrideCarrierConfigNonstandardFormatMatch() {
+ PublishServiceDescTracker t1 =
+ PublishServiceDescTracker.fromCarrierConfig(TEST_OVERRIDE_CONFIG_FORMAT);
+
+ Set<ServiceDescription> expectedSet = Collections.singleton(
+ ServiceDescription.SERVICE_DESCRIPTION_FT);
+ Set<String> imsReg = createImsRegistration(FeatureTags.FEATURE_TAG_FILE_TRANSFER);
+ t1.updateImsRegistration(imsReg);
+ assertEquals(expectedSet, t1.copyRegistrationCapabilities());
+
+ expectedSet = new ArraySet<>(Arrays.asList(
+ ServiceDescription.SERVICE_DESCRIPTION_FT, TEST_SERVICE_DESC_1));
+ imsReg = createImsRegistration(FeatureTags.FEATURE_TAG_FILE_TRANSFER,
+ TEST_FEATURE_TAG_1);
+ t1.updateImsRegistration(imsReg);
+ assertEquals(expectedSet, t1.copyRegistrationCapabilities());
+
+ // Test overrides also allow for multiple FT specifications
+ expectedSet = new ArraySet<>(Arrays.asList(
+ ServiceDescription.SERVICE_DESCRIPTION_CALL_COMPOSER_MMTEL, TEST_SERVICE_DESC_2));
+ imsReg = createImsRegistration(
+ FeatureTags.FEATURE_TAG_CALL_COMPOSER_VIA_TELEPHONY,
+ TEST_FEATURE_TAG_2B, TEST_FEATURE_TAG_2A);
+ t1.updateImsRegistration(imsReg);
+ assertEquals(expectedSet, t1.copyRegistrationCapabilities());
+
+ // Test override does not affect mmtel voice
+ expectedSet = Collections.singleton(
+ ServiceDescription.SERVICE_DESCRIPTION_MMTEL_VOICE);
+ imsReg = createImsRegistration(TEST_FEATURE_TAG_MMTEL_FORMAT);
+ t1.updateImsRegistration(imsReg);
+ assertEquals(expectedSet, t1.copyRegistrationCapabilities());
+
+ // Test override description works for existing tags
+ expectedSet = Collections.singleton(TEST_OVERRIDE_MMTEL_DESC);
+ imsReg = createImsRegistration(
+ FeatureTags.FEATURE_TAG_MMTEL,
+ FeatureTags.FEATURE_TAG_VIDEO);
+ t1.updateImsRegistration(imsReg);
+ assertEquals(expectedSet, t1.copyRegistrationCapabilities());
+ }
+
+ private Set<String> createImsRegistration(String... imsReg) {
+ return new ArraySet<>(imsReg);
+ }
+}
diff --git a/tests/src/com/android/ims/rcs/uce/request/CapabilityRequestTest.java b/tests/src/com/android/ims/rcs/uce/request/CapabilityRequestTest.java
new file mode 100644
index 00000000..4aef42e6
--- /dev/null
+++ b/tests/src/com/android/ims/rcs/uce/request/CapabilityRequestTest.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2021 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.ims.rcs.uce.request;
+
+import static android.telephony.ims.RcsContactUceCapability.REQUEST_RESULT_FOUND;
+import static android.telephony.ims.RcsContactUceCapability.SOURCE_TYPE_CACHED;
+
+import static com.android.ims.rcs.uce.eab.EabCapabilityResult.EAB_QUERY_SUCCESSFUL;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertFalse;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import android.net.Uri;
+import android.telephony.ims.RcsContactUceCapability.PresenceBuilder;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.ims.ImsTestBase;
+import com.android.ims.rcs.uce.eab.EabCapabilityResult;
+import com.android.ims.rcs.uce.UceDeviceState.DeviceStateResult;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+@RunWith(AndroidJUnit4.class)
+public class CapabilityRequestTest extends ImsTestBase {
+
+ @Mock CapabilityRequestResponse mRequestResponse;
+ @Mock UceRequestManager.RequestManagerCallback mReqMgrCallback;
+ @Mock DeviceStateResult mDeviceStateResult;
+
+ private final int mSubId = 1;
+ private final long mCoordId = 1L;
+ private final Uri contact1 = Uri.fromParts("sip", "test1", null);
+ private final Uri contact2 = Uri.fromParts("sip", "test2", null);
+
+ private boolean mRequestExecuted;
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ doReturn(mDeviceStateResult).when(mReqMgrCallback).getDeviceState();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ @Test
+ @SmallTest
+ public void testCachedCapabilityCallback() throws Exception {
+ CapabilityRequest request = getCapabilityRequest();
+
+ // Assume that all the requested capabilities can be retrieved from the cache.
+ PresenceBuilder builder1 = new PresenceBuilder(contact1, SOURCE_TYPE_CACHED,
+ REQUEST_RESULT_FOUND);
+ PresenceBuilder builder2 = new PresenceBuilder(contact2, SOURCE_TYPE_CACHED,
+ REQUEST_RESULT_FOUND);
+
+ EabCapabilityResult eabResult1 = new EabCapabilityResult(contact1, EAB_QUERY_SUCCESSFUL,
+ builder1.build());
+ EabCapabilityResult eabResult2 = new EabCapabilityResult(contact2, EAB_QUERY_SUCCESSFUL,
+ builder2.build());
+
+ List<EabCapabilityResult> eabResultList = new ArrayList<>();
+ eabResultList.add(eabResult1);
+ eabResultList.add(eabResult2);
+
+ doReturn(false).when(mDeviceStateResult).isRequestForbidden();
+ doReturn(eabResultList).when(mReqMgrCallback).getCapabilitiesFromCache(any());
+
+ // Execute the request.
+ request.executeRequest();
+
+ // Verify that it will notify the cached capabilities is updated.
+ verify(mReqMgrCallback).notifyCachedCapabilitiesUpdated(eq(mCoordId), anyLong());
+
+ // Verify that it does not need to request capabilities from network.
+ verify(mReqMgrCallback).notifyNoNeedRequestFromNetwork(eq(mCoordId), anyLong());
+ assertFalse(mRequestExecuted);
+ }
+
+ @Test
+ @SmallTest
+ public void testCachedCapabilityCallbackWithSkipGettingFromCache() throws Exception {
+ CapabilityRequest request = getCapabilityRequest();
+
+ // Assume that all the requested capabilities can be retrieved from the cache.
+ PresenceBuilder builder1 = new PresenceBuilder(contact1, SOURCE_TYPE_CACHED,
+ REQUEST_RESULT_FOUND);
+ PresenceBuilder builder2 = new PresenceBuilder(contact2, SOURCE_TYPE_CACHED,
+ REQUEST_RESULT_FOUND);
+
+ EabCapabilityResult eabResult1 = new EabCapabilityResult(contact1, EAB_QUERY_SUCCESSFUL,
+ builder1.build());
+ EabCapabilityResult eabResult2 = new EabCapabilityResult(contact2, EAB_QUERY_SUCCESSFUL,
+ builder2.build());
+
+ List<EabCapabilityResult> eabResultList = new ArrayList<>();
+ eabResultList.add(eabResult1);
+ eabResultList.add(eabResult2);
+
+ doReturn(false).when(mDeviceStateResult).isRequestForbidden();
+ doReturn(eabResultList).when(mReqMgrCallback).getCapabilitiesFromCache(any());
+
+ // Assume that skip getting capabilities from the cache.
+ request.setSkipGettingFromCache(true);
+
+ // Execute the request.
+ request.executeRequest();
+
+ // Verify that it will not notify the cached capabilities.
+ verify(mReqMgrCallback, never()).notifyCachedCapabilitiesUpdated(eq(mCoordId), anyLong());
+
+ // Verify that it will request capabilities from network.
+ verify(mReqMgrCallback, never()).notifyNoNeedRequestFromNetwork(eq(mCoordId), anyLong());
+ assertTrue(mRequestExecuted);
+ }
+
+ @Test
+ @SmallTest
+ public void testRequestCapabilities() throws Exception {
+ CapabilityRequest request = getCapabilityRequest();
+
+ // Assume that only one requested capabilities can be retrieved from the cache.
+ PresenceBuilder builder = new PresenceBuilder(contact1, SOURCE_TYPE_CACHED,
+ REQUEST_RESULT_FOUND);
+
+ EabCapabilityResult eabResult = new EabCapabilityResult(contact1, EAB_QUERY_SUCCESSFUL,
+ builder.build());
+
+ List<EabCapabilityResult> eabResultList = new ArrayList<>();
+ eabResultList.add(eabResult);
+
+ doReturn(false).when(mDeviceStateResult).isRequestForbidden();
+ doReturn(eabResultList).when(mReqMgrCallback).getCapabilitiesFromCache(any());
+
+ // Execute the request.
+ request.executeRequest();
+
+ // Verify that it will notify the cached capabilities is updated.
+ verify(mReqMgrCallback).notifyCachedCapabilitiesUpdated(eq(mCoordId), anyLong());
+
+ // Verify that it will request capability from the network.
+ verify(mReqMgrCallback, never()).notifyNoNeedRequestFromNetwork(eq(mCoordId), anyLong());
+ assertTrue(mRequestExecuted);
+ }
+
+ private CapabilityRequest getCapabilityRequest() {
+ CapabilityRequest request = new CapabilityRequest(mSubId,
+ UceRequest.REQUEST_TYPE_CAPABILITY, mReqMgrCallback, mRequestResponse) {
+ @Override
+ protected void requestCapabilities(List<Uri> requestCapUris) {
+ mRequestExecuted = true;
+ }
+ };
+ // Set the request coordinator ID
+ request.setRequestCoordinatorId(mCoordId);
+
+ // Set two contacts
+ List<Uri> uriList = new ArrayList<>();
+ uriList.add(contact1);
+ uriList.add(contact2);
+ request.setContactUri(uriList);
+ return request;
+ }
+}
diff --git a/tests/src/com/android/ims/rcs/uce/request/OptionsCoordinatorTest.java b/tests/src/com/android/ims/rcs/uce/request/OptionsCoordinatorTest.java
new file mode 100644
index 00000000..9c270fbf
--- /dev/null
+++ b/tests/src/com/android/ims/rcs/uce/request/OptionsCoordinatorTest.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2021 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.ims.rcs.uce.request;
+
+import static com.android.ims.rcs.uce.request.UceRequestCoordinator.REQUEST_UPDATE_COMMAND_ERROR;
+import static com.android.ims.rcs.uce.request.UceRequestCoordinator.REQUEST_UPDATE_ERROR;
+import static com.android.ims.rcs.uce.request.UceRequestCoordinator.REQUEST_UPDATE_NETWORK_RESPONSE;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.verify;
+
+import android.net.Uri;
+import android.telephony.ims.RcsContactUceCapability;
+import android.telephony.ims.aidl.IRcsUceControllerCallback;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.ims.ImsTestBase;
+import com.android.ims.rcs.uce.request.UceRequestCoordinator.RequestResult;
+import com.android.ims.rcs.uce.request.UceRequestManager.RequestManagerCallback;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+@RunWith(AndroidJUnit4.class)
+public class OptionsCoordinatorTest extends ImsTestBase {
+
+ @Mock OptionsRequest mRequest;
+ @Mock CapabilityRequestResponse mResponse;
+ @Mock RequestManagerCallback mRequestMgrCallback;
+ @Mock IRcsUceControllerCallback mUceCallback;
+
+ private int mSubId = 1;
+ private long mTaskId = 1L;
+ private Uri mContact = Uri.fromParts("sip", "test1", null);
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ doReturn(mTaskId).when(mRequest).getTaskId();
+ doReturn(mResponse).when(mRequest).getRequestResponse();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ @Test
+ @SmallTest
+ public void testRequestUpdatedWithError() throws Exception {
+ OptionsRequestCoordinator coordinator = getOptionsCoordinator();
+
+ coordinator.onRequestUpdated(mTaskId, REQUEST_UPDATE_ERROR);
+
+ verify(mRequest).onFinish();
+
+ Collection<UceRequest> requestList = coordinator.getActivatedRequest();
+ Collection<RequestResult> resultList = coordinator.getFinishedRequest();
+ assertTrue(requestList.isEmpty());
+ assertEquals(1, resultList.size());
+ verify(mRequestMgrCallback).notifyUceRequestFinished(anyLong(), eq(mTaskId));
+ }
+
+ @Test
+ @SmallTest
+ public void testRequestCommandError() throws Exception {
+ OptionsRequestCoordinator coordinator = getOptionsCoordinator();
+
+ coordinator.onRequestUpdated(mTaskId, REQUEST_UPDATE_COMMAND_ERROR);
+
+ verify(mRequest).onFinish();
+
+ Collection<UceRequest> requestList = coordinator.getActivatedRequest();
+ Collection<RequestResult> resultList = coordinator.getFinishedRequest();
+ assertTrue(requestList.isEmpty());
+ assertEquals(1, resultList.size());
+ verify(mRequestMgrCallback).notifyUceRequestFinished(anyLong(), eq(mTaskId));
+ }
+
+ @Test
+ @SmallTest
+ public void testRequestNetworkResponse() throws Exception {
+ OptionsRequestCoordinator coordinator = getOptionsCoordinator();
+ doReturn(true).when(mResponse).isNetworkResponseOK();
+
+ final List<RcsContactUceCapability> updatedCapList = new ArrayList<>();
+ RcsContactUceCapability updatedCapability = getContactUceCapability();
+ updatedCapList.add(updatedCapability);
+ doReturn(updatedCapList).when(mResponse).getUpdatedContactCapability();
+
+ coordinator.onRequestUpdated(mTaskId, REQUEST_UPDATE_NETWORK_RESPONSE);
+
+ verify(mRequestMgrCallback).saveCapabilities(updatedCapList);
+ verify(mUceCallback).onCapabilitiesReceived(updatedCapList);
+ verify(mResponse).removeUpdatedCapabilities(updatedCapList);
+
+ verify(mRequest).onFinish();
+
+ Collection<UceRequest> requestList = coordinator.getActivatedRequest();
+ Collection<RequestResult> resultList = coordinator.getFinishedRequest();
+ assertTrue(requestList.isEmpty());
+ assertEquals(1, resultList.size());
+ verify(mRequestMgrCallback).notifyUceRequestFinished(anyLong(), eq(mTaskId));
+ }
+
+ private OptionsRequestCoordinator getOptionsCoordinator() {
+ OptionsRequestCoordinator.Builder builder = new OptionsRequestCoordinator.Builder(
+ mSubId, Collections.singletonList(mRequest), mRequestMgrCallback);
+ builder.setCapabilitiesCallback(mUceCallback);
+ return builder.build();
+ }
+
+ private RcsContactUceCapability getContactUceCapability() {
+ int requestResult = RcsContactUceCapability.REQUEST_RESULT_FOUND;
+ RcsContactUceCapability.PresenceBuilder builder =
+ new RcsContactUceCapability.PresenceBuilder(
+ mContact, RcsContactUceCapability.SOURCE_TYPE_NETWORK, requestResult);
+ return builder.build();
+ }
+}
diff --git a/tests/src/com/android/ims/rcs/uce/request/OptionsRequestTest.java b/tests/src/com/android/ims/rcs/uce/request/OptionsRequestTest.java
new file mode 100644
index 00000000..54a02525
--- /dev/null
+++ b/tests/src/com/android/ims/rcs/uce/request/OptionsRequestTest.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2021 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.ims.rcs.uce.request;
+
+import static android.telephony.ims.RcsContactUceCapability.SOURCE_TYPE_CACHED;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import android.net.Uri;
+import android.telephony.ims.RcsContactUceCapability;
+import android.telephony.ims.RcsContactUceCapability.OptionsBuilder;
+import android.telephony.ims.RcsUceAdapter;
+import android.telephony.ims.aidl.IOptionsResponseCallback;
+import android.telephony.ims.stub.RcsCapabilityExchangeImplBase;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.ims.ImsTestBase;
+import com.android.ims.rcs.uce.options.OptionsController;
+import com.android.ims.rcs.uce.request.UceRequestManager.RequestManagerCallback;
+import com.android.ims.rcs.uce.util.NetworkSipCode;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+@RunWith(AndroidJUnit4.class)
+public class OptionsRequestTest extends ImsTestBase {
+
+ private static final String FEATURE_TAG_CHAT =
+ "+g.3gpp.icsi-ref=\"urn%3Aurn-7%3A3gpp-service.ims.icsi.oma.cpm.session\"";
+ private static final String FEATURE_TAG_FILE_TRANSFER =
+ "+g.3gpp.iari-ref=\"urn%3Aurn-7%3A3gpp-application.ims.iari.rcs.fthttp\"";
+ private static final String FEATURE_TAG_MMTEL_AUDIO_CALL =
+ "+g.3gpp.icsi-ref=\"urn%3Aurn-7%3A3gpp-service.ims.icsi.mmtel\"";
+ private static final String FEATURE_TAG_MMTEL_VIDEO_CALL =
+ "+g.3gpp.icsi-ref=\"urn%3Aurn-7%3A3gpp-service.ims.icsi.mmtel\";video";
+
+ private int mSubId = 1;
+ private long mCoordId = 1L;
+ private int mRequestType = UceRequest.REQUEST_TYPE_CAPABILITY;
+ private Uri mTestContact;
+ private Set<String> mFeatureTags;
+ private RcsContactUceCapability mDeviceCapability;
+
+ @Mock OptionsController mOptionsController;
+ @Mock CapabilityRequestResponse mRequestResponse;
+ @Mock RequestManagerCallback mRequestManagerCallback;
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ mTestContact = Uri.fromParts("sip", "test", null);
+
+ mFeatureTags = new HashSet<>();
+ mFeatureTags.add(FEATURE_TAG_CHAT);
+ mFeatureTags.add(FEATURE_TAG_FILE_TRANSFER);
+ mFeatureTags.add(FEATURE_TAG_MMTEL_AUDIO_CALL);
+ mFeatureTags.add(FEATURE_TAG_MMTEL_VIDEO_CALL);
+
+ OptionsBuilder builder = new OptionsBuilder(mTestContact, SOURCE_TYPE_CACHED);
+ builder.addFeatureTag(FEATURE_TAG_CHAT);
+ builder.addFeatureTag(FEATURE_TAG_FILE_TRANSFER);
+ builder.addFeatureTag(FEATURE_TAG_MMTEL_AUDIO_CALL);
+ builder.addFeatureTag(FEATURE_TAG_MMTEL_VIDEO_CALL);
+ mDeviceCapability = builder.build();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ @Test
+ @SmallTest
+ public void testRequestCapabilities() throws Exception {
+ OptionsRequest request = getOptionsRequest();
+ doReturn(mDeviceCapability).when(mRequestManagerCallback).getDeviceCapabilities(anyInt());
+
+ List<Uri> uriList = Collections.singletonList(mTestContact);
+ request.requestCapabilities(uriList);
+
+ verify(mOptionsController).sendCapabilitiesRequest(eq(mTestContact),
+ eq(mFeatureTags), any());
+ }
+
+ @Test
+ @SmallTest
+ public void testsendCapabilitiesRequestWhenDestroy() throws Exception {
+ OptionsRequest request = getOptionsRequest();
+ request.onFinish();
+
+ List<Uri> uriList = Collections.singletonList(mTestContact);
+ request.requestCapabilities(uriList);
+
+ verify(mRequestResponse).setRequestInternalError(RcsUceAdapter.ERROR_GENERIC_FAILURE);
+ verify(mRequestManagerCallback).notifyRequestError(eq(mCoordId), anyLong());
+ }
+
+ @Test
+ @SmallTest
+ public void testCommandErrorCallback() throws Exception {
+ OptionsRequest request = getOptionsRequest();
+ IOptionsResponseCallback callback = request.getResponseCallback();
+
+ int errorCode = RcsCapabilityExchangeImplBase.COMMAND_CODE_NOT_SUPPORTED;
+ callback.onCommandError(errorCode);
+
+ verify(mRequestResponse).setCommandError(errorCode);
+ verify(mRequestManagerCallback).notifyCommandError(eq(mCoordId), anyLong());
+ }
+
+ @Test
+ @SmallTest
+ public void testNetworkResponse() throws Exception {
+ OptionsRequest request = getOptionsRequest();
+ IOptionsResponseCallback callback = request.getResponseCallback();
+
+ int sipCode = NetworkSipCode.SIP_CODE_ACCEPTED;
+ String reason = NetworkSipCode.SIP_ACCEPTED;
+ callback.onNetworkResponse(sipCode, reason, new ArrayList<>(mFeatureTags));
+
+ verify(mRequestResponse).setNetworkResponseCode(sipCode, reason);
+ verify(mRequestResponse).setRemoteCapabilities(eq(mFeatureTags));
+ verify(mRequestManagerCallback).notifyNetworkResponse(eq(mCoordId), anyLong());
+ }
+
+ private OptionsRequest getOptionsRequest() {
+ OptionsRequest request = new OptionsRequest(mSubId, mRequestType, mRequestManagerCallback,
+ mOptionsController, mRequestResponse);
+ request.setRequestCoordinatorId(mCoordId);
+ return request;
+ }
+}
diff --git a/tests/src/com/android/ims/rcs/uce/request/RemoteOptionsCoordinatorTest.java b/tests/src/com/android/ims/rcs/uce/request/RemoteOptionsCoordinatorTest.java
new file mode 100644
index 00000000..1a6ed4a4
--- /dev/null
+++ b/tests/src/com/android/ims/rcs/uce/request/RemoteOptionsCoordinatorTest.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2021 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.ims.rcs.uce.request;
+
+import static com.android.ims.rcs.uce.request.UceRequestCoordinator.REQUEST_UPDATE_REMOTE_REQUEST_DONE;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.verify;
+
+import android.net.Uri;
+import android.telephony.ims.RcsContactUceCapability;
+import android.telephony.ims.aidl.IOptionsRequestCallback;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.ims.ImsTestBase;
+import com.android.ims.rcs.uce.request.RemoteOptionsRequest.RemoteOptResponse;
+import com.android.ims.rcs.uce.request.UceRequestCoordinator.RequestResult;
+import com.android.ims.rcs.uce.request.UceRequestManager.RequestManagerCallback;
+
+import java.util.Collection;
+import java.util.Collections;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+@RunWith(AndroidJUnit4.class)
+public class RemoteOptionsCoordinatorTest extends ImsTestBase {
+
+ @Mock RemoteOptionsRequest mRequest;
+ @Mock RemoteOptResponse mResponse;
+ @Mock RequestManagerCallback mRequestMgrCallback;
+ @Mock IOptionsRequestCallback mOptRequestCallback;
+
+ private int mSubId = 1;
+ private long mTaskId = 1L;
+ private Uri mContact = Uri.fromParts("sip", "test1", null);
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ doReturn(mTaskId).when(mRequest).getTaskId();
+ doReturn(mResponse).when(mRequest).getRemoteOptResponse();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ @Test
+ @SmallTest
+ public void testRemoteRequestFinished() throws Exception {
+ RemoteOptionsCoordinator coordinator = getRemoteOptCoordinator();
+ RcsContactUceCapability updatedCapability = getContactUceCapability();
+ doReturn(updatedCapability).when(mResponse).getRcsContactCapability();
+ doReturn(true).when(mResponse).isNumberBlocked();
+
+ coordinator.onRequestUpdated(mTaskId, REQUEST_UPDATE_REMOTE_REQUEST_DONE);
+
+ verify(mOptRequestCallback).respondToCapabilityRequest(updatedCapability, true);
+
+ verify(mRequest).onFinish();
+
+ Collection<UceRequest> requestList = coordinator.getActivatedRequest();
+ Collection<RequestResult> resultList = coordinator.getFinishedRequest();
+ assertTrue(requestList.isEmpty());
+ assertEquals(1, resultList.size());
+ verify(mRequestMgrCallback).notifyUceRequestFinished(anyLong(), eq(mTaskId));
+ }
+
+ private RemoteOptionsCoordinator getRemoteOptCoordinator() {
+ RemoteOptionsCoordinator.Builder builder = new RemoteOptionsCoordinator.Builder(
+ mSubId, Collections.singletonList(mRequest), mRequestMgrCallback);
+ builder.setOptionsRequestCallback(mOptRequestCallback);
+ return builder.build();
+ }
+
+ private RcsContactUceCapability getContactUceCapability() {
+ int requestResult = RcsContactUceCapability.REQUEST_RESULT_FOUND;
+ RcsContactUceCapability.PresenceBuilder builder =
+ new RcsContactUceCapability.PresenceBuilder(
+ mContact, RcsContactUceCapability.SOURCE_TYPE_NETWORK, requestResult);
+ return builder.build();
+ }
+}
diff --git a/tests/src/com/android/ims/rcs/uce/request/RemoteOptionsRequestTest.java b/tests/src/com/android/ims/rcs/uce/request/RemoteOptionsRequestTest.java
new file mode 100644
index 00000000..13777f37
--- /dev/null
+++ b/tests/src/com/android/ims/rcs/uce/request/RemoteOptionsRequestTest.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2021 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.ims.rcs.uce.request;
+
+import static android.telephony.ims.RcsContactUceCapability.SOURCE_TYPE_CACHED;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import android.net.Uri;
+import android.telephony.ims.RcsContactUceCapability;
+import android.telephony.ims.RcsContactUceCapability.OptionsBuilder;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.ims.ImsTestBase;
+import com.android.ims.rcs.uce.request.RemoteOptionsRequest.RemoteOptResponse;
+import com.android.ims.rcs.uce.util.NetworkSipCode;
+import com.android.ims.rcs.uce.request.UceRequestManager.RequestManagerCallback;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+@RunWith(AndroidJUnit4.class)
+public class RemoteOptionsRequestTest extends ImsTestBase {
+
+ private static final String FEATURE_TAG_CHAT =
+ "+g.3gpp.icsi-ref=\"urn%3Aurn-7%3A3gpp-service.ims.icsi.oma.cpm.session\"";
+ private static final String FEATURE_TAG_FILE_TRANSFER =
+ "+g.3gpp.iari-ref=\"urn%3Aurn-7%3A3gpp-application.ims.iari.rcs.fthttp\"";
+
+ private int mSubId = 1;
+ private long mCoordId = 1L;
+ private Uri mTestContact;
+ private RcsContactUceCapability mDeviceCapability;
+
+ @Mock RequestManagerCallback mRequestManagerCallback;
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ mTestContact = Uri.fromParts("sip", "test", null);
+
+ List<String> featureTags = new ArrayList<>();
+ featureTags.add(FEATURE_TAG_CHAT);
+ featureTags.add(FEATURE_TAG_FILE_TRANSFER);
+
+ OptionsBuilder builder = new OptionsBuilder(mTestContact, SOURCE_TYPE_CACHED);
+ builder.addFeatureTag(FEATURE_TAG_CHAT);
+ builder.addFeatureTag(FEATURE_TAG_FILE_TRANSFER);
+ mDeviceCapability = builder.build();
+
+ doReturn(mDeviceCapability).when(mRequestManagerCallback).getDeviceCapabilities(anyInt());
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ @Test
+ @SmallTest
+ public void testRequestCapabilities() throws Exception {
+ RemoteOptionsRequest request = getRequest();
+ List<String> featureTags = Arrays.asList(FEATURE_TAG_CHAT, FEATURE_TAG_FILE_TRANSFER);
+ request.setRemoteFeatureTags(featureTags);
+ request.setIsRemoteNumberBlocked(false);
+
+ request.executeRequest();
+
+ verify(mRequestManagerCallback).saveCapabilities(any());
+
+ RemoteOptResponse response = request.getRemoteOptResponse();
+ assertEquals(mDeviceCapability, response.getRcsContactCapability());
+ assertFalse(response.isNumberBlocked());
+
+ verify(mRequestManagerCallback).notifyRemoteRequestDone(eq(mCoordId), anyLong());
+ }
+
+ @Test
+ @SmallTest
+ public void testRequestCapabilitiesWhenBlocked() throws Exception {
+ RemoteOptionsRequest request = getRequest();
+ List<String> featureTags = Arrays.asList(FEATURE_TAG_CHAT, FEATURE_TAG_FILE_TRANSFER);
+ request.setRemoteFeatureTags(featureTags);
+ request.setIsRemoteNumberBlocked(true);
+
+ request.executeRequest();
+
+ verify(mRequestManagerCallback).saveCapabilities(any());
+
+ RemoteOptResponse response = request.getRemoteOptResponse();
+ assertEquals(mDeviceCapability, response.getRcsContactCapability());
+ assertTrue(response.isNumberBlocked());
+
+ verify(mRequestManagerCallback).notifyRemoteRequestDone(eq(mCoordId), anyLong());
+ }
+
+ @Test
+ @SmallTest
+ public void testsendCapabilitiesRequestWhenDestroy() throws Exception {
+ RemoteOptionsRequest request = getRequest();
+ request.onFinish();
+
+ request.executeRequest();
+
+ RemoteOptResponse response = request.getRemoteOptResponse();
+ int errorSipCode = response.getErrorSipCode().orElse(-1);
+ String reason = response.getErrorReason().orElse("");
+ assertEquals(NetworkSipCode.SIP_CODE_SERVICE_UNAVAILABLE, errorSipCode);
+ assertEquals(NetworkSipCode.SIP_SERVICE_UNAVAILABLE, reason);
+
+ verify(mRequestManagerCallback).notifyRemoteRequestDone(eq(mCoordId), anyLong());
+ verify(mRequestManagerCallback, never()).saveCapabilities(any());
+ }
+
+ private RemoteOptionsRequest getRequest() {
+ RemoteOptionsRequest request = new RemoteOptionsRequest(mSubId, mRequestManagerCallback);
+ request.setRequestCoordinatorId(mCoordId);
+ request.setContactUri(Collections.singletonList(mTestContact));
+ return request;
+ }
+}
diff --git a/tests/src/com/android/ims/rcs/uce/request/SubscribeCoordinatorTest.java b/tests/src/com/android/ims/rcs/uce/request/SubscribeCoordinatorTest.java
new file mode 100644
index 00000000..137b4ac7
--- /dev/null
+++ b/tests/src/com/android/ims/rcs/uce/request/SubscribeCoordinatorTest.java
@@ -0,0 +1,249 @@
+/*
+ * Copyright (C) 2021 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.ims.rcs.uce.request;
+
+import static com.android.ims.rcs.uce.request.UceRequestCoordinator.REQUEST_UPDATE_CACHED_CAPABILITY_UPDATE;
+import static com.android.ims.rcs.uce.request.UceRequestCoordinator.REQUEST_UPDATE_CAPABILITY_UPDATE;
+import static com.android.ims.rcs.uce.request.UceRequestCoordinator.REQUEST_UPDATE_COMMAND_ERROR;
+import static com.android.ims.rcs.uce.request.UceRequestCoordinator.REQUEST_UPDATE_ERROR;
+import static com.android.ims.rcs.uce.request.UceRequestCoordinator.REQUEST_UPDATE_NETWORK_RESPONSE;
+import static com.android.ims.rcs.uce.request.UceRequestCoordinator.REQUEST_UPDATE_NO_NEED_REQUEST_FROM_NETWORK;
+import static com.android.ims.rcs.uce.request.UceRequestCoordinator.REQUEST_UPDATE_RESOURCE_TERMINATED;
+import static com.android.ims.rcs.uce.request.UceRequestCoordinator.REQUEST_UPDATE_TERMINATED;
+
+import static java.lang.Boolean.TRUE;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import android.net.Uri;
+import android.telephony.ims.RcsContactUceCapability;
+import android.telephony.ims.aidl.IRcsUceControllerCallback;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.ims.ImsTestBase;
+import com.android.ims.rcs.uce.UceDeviceState.DeviceStateResult;
+import com.android.ims.rcs.uce.request.UceRequestCoordinator.RequestResult;
+import com.android.ims.rcs.uce.request.UceRequestManager.RequestManagerCallback;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+@RunWith(AndroidJUnit4.class)
+public class SubscribeCoordinatorTest extends ImsTestBase {
+
+ @Mock SubscribeRequest mRequest;
+ @Mock CapabilityRequestResponse mResponse;
+ @Mock RequestManagerCallback mRequestMgrCallback;
+ @Mock IRcsUceControllerCallback mUceCallback;
+ @Mock DeviceStateResult mDeviceStateResult;
+
+ private int mSubId = 1;
+ private long mTaskId = 1L;
+ private Uri mContact = Uri.fromParts("sip", "test1", null);
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ doReturn(mTaskId).when(mRequest).getTaskId();
+ doReturn(mResponse).when(mRequest).getRequestResponse();
+ doReturn(Optional.empty()).when(mResponse).getReasonHeaderCause();
+ doReturn(mDeviceStateResult).when(mRequestMgrCallback).getDeviceState();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ @Test
+ @SmallTest
+ public void testRequestUpdatedWithError() throws Exception {
+ SubscribeRequestCoordinator coordinator = getSubscribeCoordinator();
+
+ coordinator.onRequestUpdated(mTaskId, REQUEST_UPDATE_ERROR);
+
+ Collection<UceRequest> requestList = coordinator.getActivatedRequest();
+ Collection<RequestResult> resultList = coordinator.getFinishedRequest();
+ assertTrue(requestList.isEmpty());
+ assertEquals(1, resultList.size());
+ verify(mRequest).onFinish();
+ }
+
+ @Test
+ @SmallTest
+ public void testRequestCommandError() throws Exception {
+ SubscribeRequestCoordinator coordinator = getSubscribeCoordinator();
+
+ coordinator.onRequestUpdated(mTaskId, REQUEST_UPDATE_COMMAND_ERROR);
+
+ Collection<UceRequest> requestList = coordinator.getActivatedRequest();
+ Collection<RequestResult> resultList = coordinator.getFinishedRequest();
+ assertTrue(requestList.isEmpty());
+ assertEquals(1, resultList.size());
+ verify(mRequest).onFinish();
+ }
+
+ @Test
+ @SmallTest
+ public void testRequestNetworkRespSuccess() throws Exception {
+ SubscribeRequestCoordinator coordinator = getSubscribeCoordinator();
+ doReturn(true).when(mResponse).isNetworkResponseOK();
+
+ coordinator.onRequestUpdated(mTaskId, REQUEST_UPDATE_NETWORK_RESPONSE);
+
+ Collection<UceRequest> requestList = coordinator.getActivatedRequest();
+ Collection<RequestResult> resultList = coordinator.getFinishedRequest();
+ assertEquals(1, requestList.size());
+ assertTrue(resultList.isEmpty());
+ verify(mRequest, never()).onFinish();
+ }
+
+ @Test
+ @SmallTest
+ public void testRequestNetworkRespError() throws Exception {
+ SubscribeRequestCoordinator coordinator = getSubscribeCoordinator();
+ doReturn(false).when(mResponse).isNetworkResponseOK();
+ doReturn(true).when(mResponse).isRequestForbidden();
+ Optional<Integer> respSipCode = Optional.of(400);
+ Optional<String> respReason = Optional.of("Bad Request");
+ doReturn(respSipCode).when(mResponse).getResponseSipCode();
+ doReturn(respReason).when(mResponse).getResponseReason();
+ doReturn(false).when(mDeviceStateResult).isRequestForbidden();
+
+ coordinator.onRequestUpdated(mTaskId, REQUEST_UPDATE_NETWORK_RESPONSE);
+
+ verify(mRequestMgrCallback).refreshDeviceState(respSipCode.get(), respReason.get());
+ verify(mRequest).onFinish();
+
+ Collection<UceRequest> requestList = coordinator.getActivatedRequest();
+ Collection<RequestResult> resultList = coordinator.getFinishedRequest();
+ assertTrue(requestList.isEmpty());
+ assertEquals(1, resultList.size());
+ verify(mRequestMgrCallback).notifyUceRequestFinished(anyLong(), eq(mTaskId));
+ }
+
+ @Test
+ @SmallTest
+ public void testRequestCapabilityUpdated() throws Exception {
+ SubscribeRequestCoordinator coordinator = getSubscribeCoordinator();
+
+ final List<RcsContactUceCapability> updatedCapList = new ArrayList<>();
+ RcsContactUceCapability updatedCapability = getContactUceCapability();
+ updatedCapList.add(updatedCapability);
+ doReturn(updatedCapList).when(mResponse).getUpdatedContactCapability();
+
+ coordinator.onRequestUpdated(mTaskId, REQUEST_UPDATE_CAPABILITY_UPDATE);
+
+ verify(mRequestMgrCallback).saveCapabilities(updatedCapList);
+ verify(mUceCallback).onCapabilitiesReceived(updatedCapList);
+ verify(mResponse).removeUpdatedCapabilities(updatedCapList);
+ }
+
+ @Test
+ @SmallTest
+ public void testResourceTerminated() throws Exception {
+ SubscribeRequestCoordinator coordinator = getSubscribeCoordinator();
+
+ final List<RcsContactUceCapability> updatedCapList = new ArrayList<>();
+ RcsContactUceCapability updatedCapability = getContactUceCapability();
+ updatedCapList.add(updatedCapability);
+ doReturn(updatedCapList).when(mResponse).getTerminatedResources();
+
+ coordinator.onRequestUpdated(mTaskId, REQUEST_UPDATE_RESOURCE_TERMINATED);
+
+ verify(mRequestMgrCallback).saveCapabilities(updatedCapList);
+ verify(mUceCallback).onCapabilitiesReceived(updatedCapList);
+ verify(mResponse).removeTerminatedResources(updatedCapList);
+ }
+
+ @Test
+ @SmallTest
+ public void testCachedCapabilityUpdated() throws Exception {
+ SubscribeRequestCoordinator coordinator = getSubscribeCoordinator();
+
+ final List<RcsContactUceCapability> updatedCapList = new ArrayList<>();
+ RcsContactUceCapability updatedCapability = getContactUceCapability();
+ updatedCapList.add(updatedCapability);
+ doReturn(updatedCapList).when(mResponse).getCachedContactCapability();
+
+ coordinator.onRequestUpdated(mTaskId, REQUEST_UPDATE_CACHED_CAPABILITY_UPDATE);
+
+ verify(mUceCallback).onCapabilitiesReceived(updatedCapList);
+ verify(mResponse).removeCachedContactCapabilities();
+ }
+
+ @Test
+ @SmallTest
+ public void testRequestTerminated() throws Exception {
+ SubscribeRequestCoordinator coordinator = getSubscribeCoordinator();
+
+ coordinator.onRequestUpdated(mTaskId, REQUEST_UPDATE_TERMINATED);
+
+ Collection<UceRequest> requestList = coordinator.getActivatedRequest();
+ Collection<RequestResult> resultList = coordinator.getFinishedRequest();
+ assertTrue(requestList.isEmpty());
+ assertEquals(1, resultList.size());
+ }
+
+ @Test
+ @SmallTest
+ public void testNoNeedRequestFromNetwork() throws Exception {
+ SubscribeRequestCoordinator coordinator = getSubscribeCoordinator();
+
+ coordinator.onRequestUpdated(mTaskId, REQUEST_UPDATE_NO_NEED_REQUEST_FROM_NETWORK);
+
+ Collection<UceRequest> requestList = coordinator.getActivatedRequest();
+ Collection<RequestResult> resultList = coordinator.getFinishedRequest();
+ assertTrue(requestList.isEmpty());
+ assertEquals(1, resultList.size());
+ }
+
+ private SubscribeRequestCoordinator getSubscribeCoordinator() {
+ SubscribeRequestCoordinator.Builder builder = new SubscribeRequestCoordinator.Builder(
+ mSubId, Collections.singletonList(mRequest), mRequestMgrCallback);
+ builder.setCapabilitiesCallback(mUceCallback);
+ return builder.build();
+ }
+
+ private RcsContactUceCapability getContactUceCapability() {
+ int requestResult = RcsContactUceCapability.REQUEST_RESULT_FOUND;
+ RcsContactUceCapability.PresenceBuilder builder =
+ new RcsContactUceCapability.PresenceBuilder(
+ mContact, RcsContactUceCapability.SOURCE_TYPE_NETWORK, requestResult);
+ return builder.build();
+ }
+}
diff --git a/tests/src/com/android/ims/rcs/uce/request/SubscribeRequestTest.java b/tests/src/com/android/ims/rcs/uce/request/SubscribeRequestTest.java
new file mode 100644
index 00000000..b4f9cca4
--- /dev/null
+++ b/tests/src/com/android/ims/rcs/uce/request/SubscribeRequestTest.java
@@ -0,0 +1,198 @@
+/*
+ * 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.ims.rcs.uce.request;
+
+import static android.telephony.ims.stub.RcsCapabilityExchangeImplBase.COMMAND_CODE_NOT_SUPPORTED;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import android.net.Uri;
+import android.telephony.ims.RcsContactTerminatedReason;
+import android.telephony.ims.RcsUceAdapter;
+import android.telephony.ims.aidl.ISubscribeResponseCallback;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.ims.ImsTestBase;
+import com.android.ims.rcs.uce.presence.subscribe.SubscribeController;
+import com.android.ims.rcs.uce.request.UceRequestManager.RequestManagerCallback;
+import com.android.ims.rcs.uce.util.NetworkSipCode;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+@RunWith(AndroidJUnit4.class)
+public class SubscribeRequestTest extends ImsTestBase {
+
+ @Mock SubscribeController mSubscribeController;
+ @Mock CapabilityRequestResponse mRequestResponse;
+ @Mock RequestManagerCallback mRequestManagerCallback;
+
+ private int mSubId = 1;
+ private long mCoordId = 1;
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ @Test
+ @SmallTest
+ public void testRequestCapabilities() throws Exception {
+ SubscribeRequest subscribeRequest = getSubscribeRequest();
+
+ List<Uri> uriList = new ArrayList<>();
+ subscribeRequest.requestCapabilities(uriList);
+
+ verify(mSubscribeController).requestCapabilities(eq(uriList), any());
+ }
+
+ @Test
+ @SmallTest
+ public void testRequestCapabilitiesWhenDestroy() throws Exception {
+ SubscribeRequest subscribeRequest = getSubscribeRequest();
+ subscribeRequest.onFinish();
+
+ List<Uri> uriList = new ArrayList<>();
+ subscribeRequest.requestCapabilities(uriList);
+
+ verify(mRequestResponse).setRequestInternalError(RcsUceAdapter.ERROR_GENERIC_FAILURE);
+ verify(mRequestManagerCallback).notifyRequestError(eq(mCoordId), anyLong());
+ verify(mSubscribeController, never()).requestCapabilities(any(), any());
+ }
+
+ @Test
+ @SmallTest
+ public void testCommandErrorCallback() throws Exception {
+ SubscribeRequest subscribeRequest = getSubscribeRequest();
+ ISubscribeResponseCallback callback = subscribeRequest.getResponseCallback();
+
+ callback.onCommandError(COMMAND_CODE_NOT_SUPPORTED);
+
+ verify(mRequestResponse).setCommandError(COMMAND_CODE_NOT_SUPPORTED);
+ verify(mRequestManagerCallback).notifyCommandError(eq(mCoordId), anyLong());
+ }
+
+ @Test
+ @SmallTest
+ public void testNetworkResponse() throws Exception {
+ SubscribeRequest subscribeRequest = getSubscribeRequest();
+
+ int sipCode = NetworkSipCode.SIP_CODE_FORBIDDEN;
+ String reason = "forbidden";
+ ISubscribeResponseCallback callback = subscribeRequest.getResponseCallback();
+ callback.onNetworkResponse(sipCode, reason);
+
+ verify(mRequestResponse).setNetworkResponseCode(sipCode, reason);
+ verify(mRequestManagerCallback).notifyNetworkResponse(eq(mCoordId), anyLong());
+ }
+
+ @Test
+ @SmallTest
+ public void testResourceTerminated() throws Exception {
+ SubscribeRequest subscribeRequest = getSubscribeRequest();
+ ISubscribeResponseCallback callback = subscribeRequest.getResponseCallback();
+
+ Uri contact = Uri.fromParts("sip", "test", null);
+ List<RcsContactTerminatedReason> list = new ArrayList<>();
+ list.add(new RcsContactTerminatedReason(contact, "terminated"));
+ callback.onResourceTerminated(list);
+
+ verify(mRequestResponse).addTerminatedResource(list);
+ verify(mRequestManagerCallback).notifyResourceTerminated(eq(mCoordId), anyLong());
+ }
+
+ @Test
+ @SmallTest
+ public void testCapabilitiesUpdate() throws Exception {
+ SubscribeRequest subscribeRequest = getSubscribeRequest();
+ ISubscribeResponseCallback callback = subscribeRequest.getResponseCallback();
+
+ List<String> pidfXml = new ArrayList<>();
+ pidfXml.add(getPidfData());
+ callback.onNotifyCapabilitiesUpdate(pidfXml);
+
+ verify(mRequestResponse).addUpdatedCapabilities(any());
+ verify(mRequestManagerCallback).notifyCapabilitiesUpdated(eq(mCoordId), anyLong());
+ }
+
+ @Test
+ @SmallTest
+ public void testTerminatedCallback() throws Exception {
+ SubscribeRequest subscribeRequest = getSubscribeRequest();
+ doReturn(true).when(mRequestResponse).isNetworkResponseOK();
+ ISubscribeResponseCallback callback = subscribeRequest.getResponseCallback();
+
+ String reason = "forbidden";
+ long retryAfterMillis = 10000L;
+ callback.onTerminated(reason, retryAfterMillis);
+
+ verify(mRequestResponse).setTerminated(reason, retryAfterMillis);
+ verify(mRequestManagerCallback).notifyTerminated(eq(mCoordId), anyLong());
+ }
+
+ private SubscribeRequest getSubscribeRequest() {
+ SubscribeRequest request = new SubscribeRequest(mSubId, UceRequest.REQUEST_TYPE_CAPABILITY,
+ mRequestManagerCallback, mSubscribeController, mRequestResponse);
+ request.setRequestCoordinatorId(mCoordId);
+ return request;
+ }
+
+ private String getPidfData() {
+ StringBuilder pidfBuilder = new StringBuilder();
+ pidfBuilder.append("<?xml version='1.0' encoding='utf-8' standalone='yes' ?>")
+ .append("<presence entity=\"sip:test\"")
+ .append(" xmlns=\"urn:ietf:params:xml:ns:pidf\"")
+ .append(" xmlns:op=\"urn:oma:xml:prs:pidf:oma-pres\"")
+ .append(" xmlns:op=\"urn:oma:xml:prs:pidf:oma-pres\"")
+ .append(" xmlns:caps=\"urn:ietf:params:xml:ns:pidf:caps\">")
+ // tuple 1
+ .append("<tuple id=\"tid0\"><status><basic>open</basic></status>")
+ .append("<op:service-description>")
+ .append("<op:service-id>service_id_01</op:service-id>")
+ .append("<op:version>1.0</op:version>")
+ .append("<op:description>description_test1</op:description>")
+ .append("</op:service-description>")
+ // support audio
+ .append("<caps:servcaps>")
+ .append("<caps:audio>true</caps:audio>")
+ // support video
+ .append("<caps:video>true</caps:video>")
+ .append("</caps:servcaps>")
+ .append("<contact>sip:test</contact>")
+ .append("</tuple>");
+
+ return pidfBuilder.toString();
+ }
+}
diff --git a/tests/src/com/android/ims/rcs/uce/request/UceRequestManagerTest.java b/tests/src/com/android/ims/rcs/uce/request/UceRequestManagerTest.java
new file mode 100644
index 00000000..4a99dd19
--- /dev/null
+++ b/tests/src/com/android/ims/rcs/uce/request/UceRequestManagerTest.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.ims.rcs.uce.request;
+
+import static android.telephony.ims.RcsContactUceCapability.CAPABILITY_MECHANISM_PRESENCE;
+
+import static com.android.ims.rcs.uce.request.UceRequestCoordinator.REQUEST_UPDATE_CACHED_CAPABILITY_UPDATE;
+import static com.android.ims.rcs.uce.request.UceRequestCoordinator.REQUEST_UPDATE_CAPABILITY_UPDATE;
+import static com.android.ims.rcs.uce.request.UceRequestCoordinator.REQUEST_UPDATE_COMMAND_ERROR;
+import static com.android.ims.rcs.uce.request.UceRequestCoordinator.REQUEST_UPDATE_ERROR;
+import static com.android.ims.rcs.uce.request.UceRequestCoordinator.REQUEST_UPDATE_NETWORK_RESPONSE;
+import static com.android.ims.rcs.uce.request.UceRequestCoordinator.REQUEST_UPDATE_NO_NEED_REQUEST_FROM_NETWORK;
+import static com.android.ims.rcs.uce.request.UceRequestCoordinator.REQUEST_UPDATE_REMOTE_REQUEST_DONE;
+import static com.android.ims.rcs.uce.request.UceRequestCoordinator.REQUEST_UPDATE_RESOURCE_TERMINATED;
+import static com.android.ims.rcs.uce.request.UceRequestCoordinator.REQUEST_UPDATE_TERMINATED;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import android.content.Context;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Looper;
+import android.telephony.ims.RcsContactUceCapability;
+import android.telephony.ims.RcsUceAdapter;
+import android.telephony.ims.aidl.IOptionsRequestCallback;
+import android.telephony.ims.aidl.IRcsUceControllerCallback;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.ims.ImsTestBase;
+import com.android.ims.rcs.uce.UceController;
+import com.android.ims.rcs.uce.UceController.UceControllerCallback;
+import com.android.ims.rcs.uce.request.UceRequestManager.RequestManagerCallback;
+import com.android.ims.rcs.uce.request.UceRequestManager.UceUtilsProxy;
+import com.android.ims.rcs.uce.util.FeatureTags;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+@RunWith(AndroidJUnit4.class)
+public class UceRequestManagerTest extends ImsTestBase {
+
+ @Mock UceRequest mUceRequest;
+ @Mock UceRequestCoordinator mCoordinator;
+ @Mock UceControllerCallback mCallback;
+ @Mock UceRequestRepository mRequestRepository;
+ @Mock IRcsUceControllerCallback mCapabilitiesCallback;
+ @Mock IOptionsRequestCallback mOptionsReqCallback;
+
+ private int mSubId = 1;
+ private long mTaskId = 1L;
+ private long mCoordId = 1L;
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ doReturn(mUceRequest).when(mRequestRepository).getUceRequest(anyLong());
+ doReturn(mCoordinator).when(mRequestRepository).getRequestCoordinator(anyLong());
+ doReturn(mCoordinator).when(mRequestRepository).removeRequestCoordinator(anyLong());
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ @Test
+ @SmallTest
+ public void testSendCapabilityRequest() throws Exception {
+ UceRequestManager requestManager = getUceRequestManager();
+ requestManager.setsUceUtilsProxy(getUceUtilsProxy(true, true, false, false, true, 10));
+
+ List<Uri> uriList = new ArrayList<>();
+ uriList.add(Uri.fromParts("sip", "test", null));
+ requestManager.sendCapabilityRequest(uriList, false, mCapabilitiesCallback);
+
+ verify(mRequestRepository).addRequestCoordinator(any());
+ }
+
+ @Test
+ @SmallTest
+ public void testSendAvailabilityRequest() throws Exception {
+ UceRequestManager requestManager = getUceRequestManager();
+ requestManager.setsUceUtilsProxy(getUceUtilsProxy(true, true, false, false, true, 10));
+
+ Uri uri = Uri.fromParts("sip", "test", null);
+ requestManager.sendAvailabilityRequest(uri, mCapabilitiesCallback);
+
+ verify(mRequestRepository).addRequestCoordinator(any());
+ }
+
+ @Test
+ @SmallTest
+ public void testRequestDestroyed() throws Exception {
+ UceRequestManager requestManager = getUceRequestManager();
+ requestManager.setsUceUtilsProxy(getUceUtilsProxy(true, true, true, false, true, 10));
+
+ requestManager.onDestroy();
+
+ List<Uri> uriList = new ArrayList<>();
+ requestManager.sendCapabilityRequest(uriList, false, mCapabilitiesCallback);
+
+ Handler handler = requestManager.getUceRequestHandler();
+ waitForHandlerAction(handler, 500L);
+
+ verify(mUceRequest, never()).executeRequest();
+ verify(mCapabilitiesCallback).onError(RcsUceAdapter.ERROR_GENERIC_FAILURE, 0L);
+ }
+
+ @Test
+ @SmallTest
+ public void testRequestManagerCallback() throws Exception {
+ UceRequestManager requestManager = getUceRequestManager();
+ requestManager.setsUceUtilsProxy(getUceUtilsProxy(true, true, true, false, true, 10));
+ RequestManagerCallback requestMgrCallback = requestManager.getRequestManagerCallback();
+ Handler handler = requestManager.getUceRequestHandler();
+
+ Uri contact = Uri.fromParts("sip", "test", null);
+ List<Uri> uriList = new ArrayList<>();
+ uriList.add(contact);
+
+ requestMgrCallback.notifySendingRequest(mCoordId, mTaskId, 0L);
+ waitForHandlerAction(handler, 400L);
+ verify(mUceRequest).executeRequest();
+
+ requestMgrCallback.getCapabilitiesFromCache(uriList);
+ verify(mCallback).getCapabilitiesFromCache(uriList);
+
+ requestMgrCallback.getAvailabilityFromCache(contact);
+ verify(mCallback).getAvailabilityFromCache(contact);
+
+ List<RcsContactUceCapability> capabilityList = new ArrayList<>();
+ requestMgrCallback.saveCapabilities(capabilityList);
+ verify(mCallback).saveCapabilities(capabilityList);
+
+ requestMgrCallback.getDeviceCapabilities(CAPABILITY_MECHANISM_PRESENCE);
+ verify(mCallback).getDeviceCapabilities(CAPABILITY_MECHANISM_PRESENCE);
+
+ requestMgrCallback.getDeviceState();
+ verify(mCallback).getDeviceState();
+
+ requestMgrCallback.refreshDeviceState(200, "OK");
+ verify(mCallback).refreshDeviceState(200, "OK", UceController.REQUEST_TYPE_CAPABILITY);
+
+ requestMgrCallback.notifyRequestError(mCoordId, mTaskId);
+ waitForHandlerAction(handler, 400L);
+ verify(mCoordinator).onRequestUpdated(mTaskId, REQUEST_UPDATE_ERROR);
+
+ requestMgrCallback.notifyCommandError(mCoordId, mTaskId);
+ waitForHandlerAction(handler, 400L);
+ verify(mCoordinator).onRequestUpdated(mTaskId, REQUEST_UPDATE_COMMAND_ERROR);
+
+ requestMgrCallback.notifyNetworkResponse(mCoordId, mTaskId);
+ waitForHandlerAction(handler, 400L);
+ verify(mCoordinator).onRequestUpdated(mTaskId, REQUEST_UPDATE_NETWORK_RESPONSE);
+
+ requestMgrCallback.notifyTerminated(mCoordId, mTaskId);
+ waitForHandlerAction(handler, 400L);
+ verify(mCoordinator).onRequestUpdated(mTaskId, REQUEST_UPDATE_TERMINATED);
+
+ requestMgrCallback.notifyResourceTerminated(mCoordId, mTaskId);
+ waitForHandlerAction(handler, 400L);
+ verify(mCoordinator).onRequestUpdated(mTaskId, REQUEST_UPDATE_RESOURCE_TERMINATED);
+
+ requestMgrCallback.notifyCapabilitiesUpdated(mCoordId, mTaskId);
+ waitForHandlerAction(handler, 400L);
+ verify(mCoordinator).onRequestUpdated(mTaskId, REQUEST_UPDATE_CAPABILITY_UPDATE);
+
+ requestMgrCallback.notifyCachedCapabilitiesUpdated(mCoordId, mTaskId);
+ waitForHandlerAction(handler, 400L);
+ verify(mCoordinator).onRequestUpdated(mTaskId, REQUEST_UPDATE_CACHED_CAPABILITY_UPDATE);
+
+ requestMgrCallback.notifyNoNeedRequestFromNetwork(mCoordId, mTaskId);
+ waitForHandlerAction(handler, 400L);
+ verify(mCoordinator).onRequestUpdated(mTaskId, REQUEST_UPDATE_NO_NEED_REQUEST_FROM_NETWORK);
+
+ requestMgrCallback.notifyRemoteRequestDone(mCoordId, mTaskId);
+ waitForHandlerAction(handler, 400L);
+ verify(mCoordinator).onRequestUpdated(mTaskId, REQUEST_UPDATE_REMOTE_REQUEST_DONE);
+
+ requestMgrCallback.notifyUceRequestFinished(mCoordId, mTaskId);
+ waitForHandlerAction(handler, 400L);
+ verify(mRequestRepository).notifyRequestFinished(mTaskId);
+
+ requestMgrCallback.notifyRequestCoordinatorFinished(mCoordId);
+ waitForHandlerAction(handler, 400L);
+ verify(mCoordinator).onFinish();
+ }
+
+ @Test
+ @SmallTest
+ public void testRetrieveCapForRemote() throws Exception {
+ UceRequestManager requestManager = getUceRequestManager();
+ requestManager.setsUceUtilsProxy(getUceUtilsProxy(true, true, true, false, true, 10));
+
+ Uri contact = Uri.fromParts("sip", "test", null);
+ List<String> remoteCapList = new ArrayList<>();
+ remoteCapList.add(FeatureTags.FEATURE_TAG_CHAT_IM);
+ remoteCapList.add(FeatureTags.FEATURE_TAG_FILE_TRANSFER);
+ requestManager.retrieveCapabilitiesForRemote(contact, remoteCapList, mOptionsReqCallback);
+
+ verify(mRequestRepository).addRequestCoordinator(any());
+ }
+
+ private UceRequestManager getUceRequestManager() {
+ UceRequestManager manager = new UceRequestManager(mContext, mSubId, Looper.getMainLooper(),
+ mCallback, mRequestRepository);
+ return manager;
+ }
+
+ private UceUtilsProxy getUceUtilsProxy(boolean presenceCapEnabled, boolean supportPresence,
+ boolean supportOptions, boolean isBlocked, boolean groupSubscribe, int rclMaximum) {
+ return new UceUtilsProxy() {
+ @Override
+ public boolean isPresenceCapExchangeEnabled(Context context, int subId) {
+ return presenceCapEnabled;
+ }
+
+ @Override
+ public boolean isPresenceSupported(Context context, int subId) {
+ return supportPresence;
+ }
+
+ @Override
+ public boolean isSipOptionsSupported(Context context, int subId) {
+ return supportOptions;
+ }
+
+ @Override
+ public boolean isPresenceGroupSubscribeEnabled(Context context, int subId) {
+ return groupSubscribe;
+ }
+
+ @Override
+ public int getRclMaxNumberEntries(int subId) {
+ return rclMaximum;
+ }
+
+ @Override
+ public boolean isNumberBlocked(Context context, String phoneNumber) {
+ return isBlocked;
+ }
+ };
+ }
+}