diff options
129 files changed, 28430 insertions, 1451 deletions
@@ -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, @@ -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; + } + }; + } +} |