Collect and process provisioning items to call updateImsServiceConfig

Provisioning items are notified one by one, updateImsServiceConfig is
called multiple times redundantly. Improvements have been added to
process reevaluateCapability at once as much as possible.

Bug: 302281114
Test: atest ImsPhoneCallTrackerTest
Change-Id: Ib8e35995623a5149d533d9457f7f72c2cbf7fc86
diff --git a/flags/ims.aconfig b/flags/ims.aconfig
index 482802a..0fa1f1e 100644
--- a/flags/ims.aconfig
+++ b/flags/ims.aconfig
@@ -20,3 +20,10 @@
     description: "This flag clears cached IMS phone number when device lost IMS registration"
     bug:"288002989"
 }
+
+flag {
+    name: "update_ims_service_by_gathering_provisioning_changes"
+    namespace: "telephony"
+    description: "This flag is created to prevent unnecessary updates when multiple provisioning items to update ims service are changed."
+    bug:"302281114"
+}
diff --git a/src/java/com/android/internal/telephony/TelephonyComponentFactory.java b/src/java/com/android/internal/telephony/TelephonyComponentFactory.java
index 8b832dd..6e3e911 100644
--- a/src/java/com/android/internal/telephony/TelephonyComponentFactory.java
+++ b/src/java/com/android/internal/telephony/TelephonyComponentFactory.java
@@ -41,6 +41,7 @@
 import com.android.internal.telephony.data.PhoneSwitcher;
 import com.android.internal.telephony.emergency.EmergencyNumberTracker;
 import com.android.internal.telephony.flags.FeatureFlags;
+import com.android.internal.telephony.flags.FeatureFlagsImpl;
 import com.android.internal.telephony.imsphone.ImsExternalCallTracker;
 import com.android.internal.telephony.imsphone.ImsNrSaModeHandler;
 import com.android.internal.telephony.imsphone.ImsPhone;
@@ -382,8 +383,27 @@
         return new InboundSmsTracker(context, cursor, isCurrentFormat3gpp2);
     }
 
+    /**
+     * Create an ImsPhoneCallTracker.
+     *
+     * @param imsPhone imsphone
+     * @return ImsPhoneCallTracker newly created ImsPhoneCallTracker
+     * @deprecated Use {@link #makeImsPhoneCallTracker(ImsPhone, FeatureFlags)} instead
+     */
     public ImsPhoneCallTracker makeImsPhoneCallTracker(ImsPhone imsPhone) {
-        return new ImsPhoneCallTracker(imsPhone, ImsManager::getConnector);
+        return makeImsPhoneCallTracker(imsPhone, new FeatureFlagsImpl());
+    }
+
+    /**
+     * Create a ims phone call tracker.
+     *
+     * @param imsPhone imsphone
+     * @param featureFlags feature flags
+     * @return ImsPhoneCallTracker newly created ImsPhoneCallTracker
+     */
+    public ImsPhoneCallTracker makeImsPhoneCallTracker(ImsPhone imsPhone,
+                                                       @NonNull FeatureFlags featureFlags) {
+        return new ImsPhoneCallTracker(imsPhone, ImsManager::getConnector, featureFlags);
     }
 
     public ImsExternalCallTracker makeImsExternalCallTracker(ImsPhone imsPhone) {
diff --git a/src/java/com/android/internal/telephony/imsphone/ImsPhone.java b/src/java/com/android/internal/telephony/imsphone/ImsPhone.java
index 2912935..1ee8447 100644
--- a/src/java/com/android/internal/telephony/imsphone/ImsPhone.java
+++ b/src/java/com/android/internal/telephony/imsphone/ImsPhone.java
@@ -484,7 +484,7 @@
                         .inject(ImsNrSaModeHandler.class.getName())
                         .makeImsNrSaModeHandler(this);
         mCT = TelephonyComponentFactory.getInstance().inject(ImsPhoneCallTracker.class.getName())
-                .makeImsPhoneCallTracker(this);
+                .makeImsPhoneCallTracker(this, featureFlags);
         mCT.registerPhoneStateListener(mExternalCallTracker);
         mExternalCallTracker.setCallPuller(mCT);
 
diff --git a/src/java/com/android/internal/telephony/imsphone/ImsPhoneCallTracker.java b/src/java/com/android/internal/telephony/imsphone/ImsPhoneCallTracker.java
index c3ee0f6..4f9b69d 100644
--- a/src/java/com/android/internal/telephony/imsphone/ImsPhoneCallTracker.java
+++ b/src/java/com/android/internal/telephony/imsphone/ImsPhoneCallTracker.java
@@ -148,6 +148,7 @@
 import com.android.internal.telephony.domainselection.DomainSelectionResolver;
 import com.android.internal.telephony.emergency.EmergencyNumberTracker;
 import com.android.internal.telephony.emergency.EmergencyStateTracker;
+import com.android.internal.telephony.flags.FeatureFlags;
 import com.android.internal.telephony.gsm.SuppServiceNotification;
 import com.android.internal.telephony.imsphone.ImsPhone.ImsDialArgs;
 import com.android.internal.telephony.metrics.CallQualityMetrics;
@@ -664,6 +665,7 @@
     private static final int EVENT_START_IMS_TRAFFIC_DONE = 33;
     private static final int EVENT_CONNECTION_SETUP_FAILURE = 34;
     private static final int EVENT_NEW_ACTIVE_CALL_STARTED = 35;
+    private static final int EVENT_PROVISIONING_CHANGED = 36;
 
     private static final int TIMEOUT_HANGUP_PENDINGMO = 500;
 
@@ -673,6 +675,8 @@
 
     private static final int TIMEOUT_PARTICIPANT_CONNECT_TIME_CACHE_MS = 60000; //ms
 
+    private static final int DELAY_STACKING_PROVISIONING_CHANGES_MILLIS = 50; //ms
+
     // Following values are for mHoldSwitchingState
     private enum HoldSwapState {
         // Not in the middle of a hold/swap operation
@@ -1235,17 +1239,39 @@
         }
     }
 
+    private final ConcurrentLinkedQueue<ProvisioningItem> mProvisioningItemQueue =
+            new ConcurrentLinkedQueue<>();
+
+    private static class ProvisioningItem {
+        final int mItem;
+        final Object mValue;
+        ProvisioningItem(int item, int value) {
+            this.mItem = item;
+            this.mValue = Integer.valueOf(value);
+        }
+
+        ProvisioningItem(int item, String value) {
+            this.mItem = item;
+            this.mValue = value;
+        }
+    }
+
+    private @NonNull final FeatureFlags mFeatureFlags;
+
     //***** Events
 
 
     //***** Constructors
-    public ImsPhoneCallTracker(ImsPhone phone, ConnectorFactory factory) {
-        this(phone, factory, phone.getContext().getMainExecutor());
+    public ImsPhoneCallTracker(ImsPhone phone, ConnectorFactory factory,
+            FeatureFlags featureFlags) {
+        this(phone, factory, phone.getContext().getMainExecutor(), featureFlags);
     }
 
     @VisibleForTesting
-    public ImsPhoneCallTracker(ImsPhone phone, ConnectorFactory factory, Executor executor) {
+    public ImsPhoneCallTracker(ImsPhone phone, ConnectorFactory factory, Executor executor,
+            FeatureFlags featureFlags) {
         this.mPhone = phone;
+        mFeatureFlags = featureFlags;
         mConnectorFactory = factory;
         if (executor != null) {
             mExecutor = executor;
@@ -4590,37 +4616,67 @@
 
     private final ProvisioningManager.Callback mConfigCallback =
             new ProvisioningManager.Callback() {
-        @Override
-        public void onProvisioningIntChanged(int item, int value) {
-            sendConfigChangedIntent(item, Integer.toString(value));
-            if ((mImsManager != null)
-                    && (item == ImsConfig.ConfigConstants.VOICE_OVER_WIFI_SETTING_ENABLED
-                    || item == ImsConfig.ConfigConstants.VLT_SETTING_ENABLED
-                    || item == ImsConfig.ConfigConstants.LVC_SETTING_ENABLED)) {
-                // Update Ims Service state to make sure updated provisioning values take effect
-                // immediately.
-                updateImsServiceConfig();
-            }
-        }
+                @Override
+                public void onProvisioningIntChanged(int item, int value) {
+                    // if updateImsServiceByGatheringProvisioningChanges feature is enabled,
+                    // Provisioning items are processed all at once by queuing and sending message.
+                    if (mFeatureFlags.updateImsServiceByGatheringProvisioningChanges()) {
+                        queueAndSendProvisioningChanged(new ProvisioningItem(item, value));
+                        return;
+                    }
+                    // run belows when updateImsServiceByGatheringProvisioningChanges feature is
+                    // disabled only
 
-        @Override
-        public void onProvisioningStringChanged(int item, String value) {
-            sendConfigChangedIntent(item, value);
-        }
+                    sendConfigChangedIntent(item, Integer.toString(value));
+                    if ((mImsManager != null)
+                            && (item == ImsConfig.ConfigConstants.VOICE_OVER_WIFI_SETTING_ENABLED
+                            || item == ImsConfig.ConfigConstants.VLT_SETTING_ENABLED
+                            || item == ImsConfig.ConfigConstants.LVC_SETTING_ENABLED)) {
+                        // Update Ims Service state to make sure updated provisioning values take
+                        // effect immediately.
+                        updateImsServiceConfig();
+                    }
+                }
 
-        // send IMS_CONFIG_CHANGED intent for older services that do not implement the new callback
-        // interface.
-        private void sendConfigChangedIntent(int item, String value) {
-            log("sendConfigChangedIntent - [" + item + ", " + value + "]");
-            Intent configChangedIntent = new Intent(ImsConfig.ACTION_IMS_CONFIG_CHANGED);
-            configChangedIntent.putExtra(ImsConfig.EXTRA_CHANGED_ITEM, item);
-            configChangedIntent.putExtra(ImsConfig.EXTRA_NEW_VALUE, value);
-            if (mPhone != null && mPhone.getContext() != null) {
-                mPhone.getContext().sendBroadcast(
-                        configChangedIntent, Manifest.permission.READ_PRIVILEGED_PHONE_STATE);
-            }
-        }
-    };
+                @Override
+                public void onProvisioningStringChanged(int item, String value) {
+                    if (mFeatureFlags.updateImsServiceByGatheringProvisioningChanges()) {
+                        queueAndSendProvisioningChanged(new ProvisioningItem(item, value));
+                        return;
+                    }
+                    // run belows when updateImsServiceByGatheringProvisioningChanges feature is
+                    // disabled only
+
+                    sendConfigChangedIntent(item, value);
+                }
+
+                // send IMS_CONFIG_CHANGED intent for older services that do not implement the new
+                // callback interface.
+                private void sendConfigChangedIntent(int item, String value) {
+                    log("sendConfigChangedIntent - [" + item + ", " + value + "]");
+                    Intent configChangedIntent = new Intent(ImsConfig.ACTION_IMS_CONFIG_CHANGED);
+                    configChangedIntent.putExtra(ImsConfig.EXTRA_CHANGED_ITEM, item);
+                    configChangedIntent.putExtra(ImsConfig.EXTRA_NEW_VALUE, value);
+                    if (mPhone != null && mPhone.getContext() != null) {
+                        mPhone.getContext().sendBroadcast(configChangedIntent,
+                                Manifest.permission.READ_PRIVILEGED_PHONE_STATE);
+                    }
+                }
+
+                private void queueAndSendProvisioningChanged(ProvisioningItem provisioningItem) {
+                    if (!mFeatureFlags.updateImsServiceByGatheringProvisioningChanges()) {
+                        return;
+                    }
+
+                    boolean bQueueOffer = mProvisioningItemQueue.offer(provisioningItem);
+                    // Checks the Handler Message Queue and schedules a new message with small delay
+                    // to avoid stacking multiple redundant event only if it doesn't exist.
+                    if (bQueueOffer && !hasMessages(EVENT_PROVISIONING_CHANGED)) {
+                        sendMessageDelayed(obtainMessage(EVENT_PROVISIONING_CHANGED),
+                                DELAY_STACKING_PROVISIONING_CHANGES_MILLIS);
+                    }
+                }
+            };
 
     public void sendCallStartFailedDisconnect(ImsCall imsCall, ImsReasonInfo reasonInfo) {
         mPendingMO = null;
@@ -5011,6 +5067,11 @@
                 }
                 break;
             }
+
+            case EVENT_PROVISIONING_CHANGED: {
+                handleProvisioningChanged();
+                break;
+            }
         }
     }
 
@@ -6210,4 +6271,47 @@
         mImsTrafficSessions.forEachKey(1, token -> mPhone.stopImsTraffic(token, null));
         mImsTrafficSessions.clear();
     }
+
+    /**
+     * Process provisioning changes all at once.
+     */
+    private void handleProvisioningChanged() {
+        boolean bNeedUpdateImsServiceConfig = false;
+        ProvisioningItem provisioningItem;
+        while ((provisioningItem = mProvisioningItemQueue.poll()) != null) {
+            int item = provisioningItem.mItem;
+            if (provisioningItem.mValue instanceof Integer) {
+                sendConfigChangedIntent(item, provisioningItem.mValue.toString());
+                if (item == ImsConfig.ConfigConstants.VOICE_OVER_WIFI_SETTING_ENABLED
+                        || item == ImsConfig.ConfigConstants.VLT_SETTING_ENABLED
+                        || item == ImsConfig.ConfigConstants.LVC_SETTING_ENABLED) {
+                    bNeedUpdateImsServiceConfig = true;
+                }
+            } else if (provisioningItem.mValue instanceof String) {
+                sendConfigChangedIntent(item, provisioningItem.mValue.toString());
+            }
+        }
+        if (bNeedUpdateImsServiceConfig) {
+            // Update Ims Service state to make sure updated provisioning values take effect.
+            updateImsServiceConfig();
+        }
+    }
+
+    /**
+     * send IMS_CONFIG_CHANGED intent for older services that do not implement the new callback
+     * interface
+     *
+     * @param item provisioning item
+     * @param value provisioning value
+     */
+    private void sendConfigChangedIntent(int item, String value) {
+        log("sendConfigChangedIntent - [" + item + ", " + value + "]");
+        Intent configChangedIntent = new Intent(ImsConfig.ACTION_IMS_CONFIG_CHANGED);
+        configChangedIntent.putExtra(ImsConfig.EXTRA_CHANGED_ITEM, item);
+        configChangedIntent.putExtra(ImsConfig.EXTRA_NEW_VALUE, value);
+        if (mPhone != null && mPhone.getContext() != null) {
+            mPhone.getContext().sendBroadcast(
+                    configChangedIntent, Manifest.permission.READ_PRIVILEGED_PHONE_STATE);
+        }
+    }
 }
diff --git a/tests/telephonytests/src/com/android/internal/telephony/TelephonyTest.java b/tests/telephonytests/src/com/android/internal/telephony/TelephonyTest.java
index 435763b..c613155 100644
--- a/tests/telephonytests/src/com/android/internal/telephony/TelephonyTest.java
+++ b/tests/telephonytests/src/com/android/internal/telephony/TelephonyTest.java
@@ -597,7 +597,7 @@
         doReturn(mWspTypeDecoder).when(mTelephonyComponentFactory)
                 .makeWspTypeDecoder(nullable(byte[].class));
         doReturn(mImsCT).when(mTelephonyComponentFactory)
-                .makeImsPhoneCallTracker(nullable(ImsPhone.class));
+                .makeImsPhoneCallTracker(nullable(ImsPhone.class), any(FeatureFlags.class));
         doReturn(mCdmaSSM).when(mTelephonyComponentFactory)
                 .getCdmaSubscriptionSourceManagerInstance(nullable(Context.class),
                         nullable(CommandsInterface.class), nullable(Handler.class),
diff --git a/tests/telephonytests/src/com/android/internal/telephony/imsphone/ImsPhoneCallTrackerTest.java b/tests/telephonytests/src/com/android/internal/telephony/imsphone/ImsPhoneCallTrackerTest.java
index d0a2094..04ae9d0 100644
--- a/tests/telephonytests/src/com/android/internal/telephony/imsphone/ImsPhoneCallTrackerTest.java
+++ b/tests/telephonytests/src/com/android/internal/telephony/imsphone/ImsPhoneCallTrackerTest.java
@@ -94,6 +94,7 @@
 import android.telephony.ims.ImsMmTelManager;
 import android.telephony.ims.ImsReasonInfo;
 import android.telephony.ims.ImsStreamMediaProfile;
+import android.telephony.ims.ProvisioningManager;
 import android.telephony.ims.RtpHeaderExtensionType;
 import android.telephony.ims.SrvccCall;
 import android.telephony.ims.aidl.IImsTrafficSessionCallback;
@@ -160,6 +161,7 @@
     private Bundle mBundle = new Bundle();
     private static final int SUB_0 = 0;
     @Nullable private VtDataUsageProvider mVtDataUsageProvider;
+    private ProvisioningManager.Callback mConfigCallback;
 
     // Mocked classes
     private ArgumentCaptor<Set<RtpHeaderExtensionType>> mRtpHeaderExtensionTypeCaptor;
@@ -282,10 +284,14 @@
         DomainSelectionResolver.setDomainSelectionResolver(mDomainSelectionResolver);
         doReturn(false).when(mDomainSelectionResolver).isDomainSelectionSupported();
 
+        doReturn(false)
+                .when(mFeatureFlags).updateImsServiceByGatheringProvisioningChanges();
+
         // Capture CarrierConfigChangeListener to emulate the carrier config change notification
         ArgumentCaptor<CarrierConfigManager.CarrierConfigChangeListener> listenerArgumentCaptor =
                 ArgumentCaptor.forClass(CarrierConfigManager.CarrierConfigChangeListener.class);
-        mCTUT = new ImsPhoneCallTracker(mImsPhone, mConnectorFactory, Runnable::run);
+        mCTUT = new ImsPhoneCallTracker(mImsPhone, mConnectorFactory, Runnable::run,
+                mFeatureFlags);
         verify(mCarrierConfigManager).registerCarrierConfigChangeListener(any(),
                 listenerArgumentCaptor.capture());
         mCarrierConfigChangeListener = listenerArgumentCaptor.getAllValues().get(0);
@@ -309,6 +315,12 @@
 
         verify(mMockConnector).connect();
         mConnectorListener.connectionReady(mImsManager, SUB_0);
+
+        final ArgumentCaptor<ProvisioningManager.Callback> configCallbackCaptor =
+                ArgumentCaptor.forClass(ProvisioningManager.Callback.class);
+        verify(mImsConfig).addConfigCallback(configCallbackCaptor.capture());
+        mConfigCallback = configCallbackCaptor.getValue();
+        assertNotNull(mConfigCallback);
     }
 
     @After
@@ -2641,6 +2653,73 @@
                 eq(IMS_TRAFFIC_DIRECTION_OUTGOING), any());
     }
 
+    @Test
+    public void testProvisioningItemAndUpdateImsServiceConfigWithFeatureEnabled() {
+        doReturn(true)
+                .when(mFeatureFlags).updateImsServiceByGatheringProvisioningChanges();
+
+        // Receive a subscription loaded and IMS connection ready indication.
+        mContextFixture.getCarrierConfigBundle().putBoolean(
+                CarrierConfigManager.KEY_CARRIER_CONFIG_APPLIED_BOOL, true);
+        sendCarrierConfigChanged();
+        processAllMessages();
+        verify(mImsManager, times(1)).updateImsServiceConfig();
+
+        logd("deliver provisioning items");
+        mConfigCallback.onProvisioningIntChanged(27, 2);
+        mConfigCallback.onProvisioningIntChanged(28, 1);
+        mConfigCallback.onProvisioningIntChanged(10, 1);
+        mConfigCallback.onProvisioningIntChanged(11, 1);
+        mConfigCallback.onProvisioningStringChanged(12, "msg.pc.t-mobile.com");
+        mConfigCallback.onProvisioningIntChanged(26, 0);
+        mConfigCallback.onProvisioningIntChanged(66, 0);
+
+        logd("proc provisioning items");
+        processAllFutureMessages();
+
+        // updateImsServiceConfig is called with below 2 events.
+        // 1. CarrierConfig
+        // 2. ProvisioningManager.KEY_VOICE_OVER_WIFI_ENABLED_OVERRIDE(28), ProvisioningManager
+        // .KEY_VOLTE_PROVISIONING_STATUS(10) and ProvisioningManager.KEY_VT_PROVISIONING_STATUS(11)
+        verify(mImsManager, times(2)).updateImsServiceConfig();
+    }
+
+
+    @Test
+    public void testProvisioningItemAndUpdateImsServiceConfigWithFeatureDisabled() {
+        doReturn(false)
+                .when(mFeatureFlags).updateImsServiceByGatheringProvisioningChanges();
+
+        // Receive a subscription loaded and IMS connection ready indication.
+        mContextFixture.getCarrierConfigBundle().putBoolean(
+                CarrierConfigManager.KEY_CARRIER_CONFIG_APPLIED_BOOL, true);
+        sendCarrierConfigChanged();
+        processAllMessages();
+        verify(mImsManager, times(1)).updateImsServiceConfig();
+
+        logd("deliver provisioning items");
+        mConfigCallback.onProvisioningIntChanged(27, 2);
+        //ProvisioningManager.KEY_VOICE_OVER_WIFI_ENABLED_OVERRIDE(28) call updateImsServiceConfig.
+        mConfigCallback.onProvisioningIntChanged(28, 1);
+        //ProvisioningManager.KEY_VOLTE_PROVISIONING_STATUS(10) call updateImsServiceConfig.
+        mConfigCallback.onProvisioningIntChanged(10, 1);
+        //ProvisioningManager.KEY_VT_PROVISIONING_STATUS(11) call updateImsServiceConfig.
+        mConfigCallback.onProvisioningIntChanged(11, 1);
+        mConfigCallback.onProvisioningStringChanged(12, "msg.pc.t-mobile.com");
+        mConfigCallback.onProvisioningIntChanged(26, 0);
+        mConfigCallback.onProvisioningIntChanged(66, 0);
+
+        logd("proc provisioning items");
+        processAllFutureMessages();
+
+        // updateImsServiceConfig is called with below 4 events.
+        // 1. CarrierConfig
+        // 2. ProvisioningManager.KEY_VOICE_OVER_WIFI_ENABLED_OVERRIDE(28)
+        // 3. ProvisioningManager.KEY_VOLTE_PROVISIONING_STATUS(10)
+        // 4. ProvisioningManager.KEY_VT_PROVISIONING_STATUS(11)
+        verify(mImsManager, times(4)).updateImsServiceConfig();
+    }
+
     private void sendCarrierConfigChanged() {
         mCarrierConfigChangeListener.onCarrierConfigChanged(mPhone.getPhoneId(), mPhone.getSubId(),
                 TelephonyManager.UNKNOWN_CARRIER_ID, TelephonyManager.UNKNOWN_CARRIER_ID);