AOSP API support for Sip Dialog status callback

All active RCS services must be maintained from WiFi to RAN or from RAN to WiFi.
According to the carrier specification, QNS applied this API because it needs to know if the MSRP service is activated during handover.

Test: atest SipSessionTrackerTest
Test: cts SipDelegateManagerTest

Bug: b/244429207

Change-Id: Id97d17364a11ed0ce50ade8d5469a7dc07eb6489
diff --git a/src/com/android/phone/ImsRcsController.java b/src/com/android/phone/ImsRcsController.java
index d4a0f1e..3f35454 100644
--- a/src/com/android/phone/ImsRcsController.java
+++ b/src/com/android/phone/ImsRcsController.java
@@ -50,6 +50,7 @@
 import com.android.ims.ImsManager;
 import com.android.ims.internal.IImsServiceFeatureCallback;
 import com.android.internal.telephony.IIntegerConsumer;
+import com.android.internal.telephony.ISipDialogStateCallback;
 import com.android.internal.telephony.Phone;
 import com.android.internal.telephony.TelephonyPermissions;
 import com.android.internal.telephony.ims.ImsResolver;
@@ -667,6 +668,60 @@
     }
 
     /**
+     * Register a state of Sip Dialog callback
+     */
+    @Override
+    public void registerSipDialogStateCallback(int subId, ISipDialogStateCallback cb) {
+        enforceReadPrivilegedPermission("registerSipDialogStateCallback");
+        if (cb == null) {
+            throw new IllegalArgumentException("SipDialogStateCallback is null");
+        }
+        final long identity = Binder.clearCallingIdentity();
+        if (!SubscriptionManager.isValidSubscriptionId(subId)) {
+            throw new IllegalArgumentException("Invalid Subscription ID: " + subId);
+        }
+        try {
+            SipTransportController transport = getRcsFeatureController(subId).getFeature(
+                    SipTransportController.class);
+            if (transport == null) {
+                throw new ServiceSpecificException(ImsException.CODE_ERROR_SERVICE_UNAVAILABLE,
+                        "This transport does not support the registerSipDialogStateCallback"
+                                + " of SIP delegates");
+            }
+            transport.addCallbackForSipDialogState(subId, cb);
+        } finally {
+            Binder.restoreCallingIdentity(identity);
+        }
+    }
+
+    /**
+     * Unregister a state of Sip Dialog callback
+     */
+    @Override
+    public  void unregisterSipDialogStateCallback(int subId, ISipDialogStateCallback cb) {
+        enforceReadPrivilegedPermission("unregisterSipDialogStateCallback");
+        if (cb == null) {
+            throw new IllegalArgumentException("SipDialogStateCallback is null");
+        }
+        final long identity = Binder.clearCallingIdentity();
+        if (!SubscriptionManager.isValidSubscriptionId(subId)) {
+            throw new IllegalArgumentException("Invalid Subscription ID: " + subId);
+        }
+        try {
+            SipTransportController transport = getRcsFeatureController(subId).getFeature(
+                    SipTransportController.class);
+            if (transport == null) {
+                throw new ServiceSpecificException(ImsException.CODE_ERROR_SERVICE_UNAVAILABLE,
+                        "This transport does not support the unregisterSipDialogStateCallback"
+                                + " of SIP delegates");
+            }
+            transport.removeCallbackForSipDialogState(subId, cb);
+        } finally {
+            Binder.restoreCallingIdentity(identity);
+        }
+    }
+
+    /**
      * Registers for updates to the RcsFeature connection through the IImsServiceFeatureCallback
      * callback.
      */
diff --git a/src/com/android/services/telephony/rcs/MessageTransportWrapper.java b/src/com/android/services/telephony/rcs/MessageTransportWrapper.java
index 45f7d95..d992883 100644
--- a/src/com/android/services/telephony/rcs/MessageTransportWrapper.java
+++ b/src/com/android/services/telephony/rcs/MessageTransportWrapper.java
@@ -481,6 +481,16 @@
         }
     }
 
+    /**
+     * This is a listener to handle SipDialog state of delegate
+     * @param listener {@link SipDialogsStateListener}
+     * @param isNeedNotify It indicates whether the current dialogs state should be notified.
+     */
+    public void setSipDialogsListener(SipDialogsStateListener listener,
+            boolean isNeedNotify) {
+        mSipSessionTracker.setSipDialogsListener(listener, isNeedNotify);
+    }
+
     private void logi(String log) {
         Log.i(SipTransportController.LOG_TAG, TAG + "[" + mSubId + "] " + log);
         mLocalLog.log("[I] " + log);
diff --git a/src/com/android/services/telephony/rcs/SipDelegateController.java b/src/com/android/services/telephony/rcs/SipDelegateController.java
index 860a6d9..f7fc359 100644
--- a/src/com/android/services/telephony/rcs/SipDelegateController.java
+++ b/src/com/android/services/telephony/rcs/SipDelegateController.java
@@ -391,6 +391,16 @@
     }
 
     /**
+     * This is a listener to handle SipDialog state of delegate
+     * @param listener {@link SipDialogsStateListener}
+     * @param isNeedNotify It indicates whether the current dialogs state should be notified.
+     */
+    public void setSipDialogsListener(SipDialogsStateListener listener,
+            boolean isNeedNotify) {
+        mMessageTransportWrapper.setSipDialogsListener(listener, isNeedNotify);
+    }
+
+    /**
      * Write the current state of this controller in String format using the PrintWriter provided
      * for dumpsys.
      */
diff --git a/src/com/android/services/telephony/rcs/SipDialogsStateListener.java b/src/com/android/services/telephony/rcs/SipDialogsStateListener.java
new file mode 100644
index 0000000..bbd3bd1
--- /dev/null
+++ b/src/com/android/services/telephony/rcs/SipDialogsStateListener.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.services.telephony.rcs;
+
+import android.telephony.ims.SipDialogState;
+
+import java.util.List;
+
+/**
+ * The listener interface for notifying the state of sip dialogs to SipDialogsStateHandle.
+ * refer to {@link SipTransportController}
+ */
+public interface SipDialogsStateListener {
+    /**
+     * To map dialog state information of available delegates
+     * @param key This is an ID of SipSessionTracker for distinguishing whose delegate is
+     *               during dialog mapping.
+     * @param dialogStates This is dialog state information of delegate
+     */
+    void reMappingSipDelegateState(String key, List<SipDialogState> dialogStates);
+
+    /**
+     * Notify SipDialogState information with
+     * {@link com.android.internal.telephony.ISipDialogStateCallback}
+     */
+    void notifySipDialogState();
+}
diff --git a/src/com/android/services/telephony/rcs/SipSessionTracker.java b/src/com/android/services/telephony/rcs/SipSessionTracker.java
index 68e3065..2d48a9f 100644
--- a/src/com/android/services/telephony/rcs/SipSessionTracker.java
+++ b/src/com/android/services/telephony/rcs/SipSessionTracker.java
@@ -16,6 +16,7 @@
 
 package com.android.services.telephony.rcs;
 
+import android.telephony.ims.SipDialogState;
 import android.telephony.ims.SipMessage;
 import android.util.ArrayMap;
 import android.util.ArraySet;
@@ -33,6 +34,7 @@
 import java.util.Collections;
 import java.util.List;
 import java.util.Set;
+import java.util.UUID;
 import java.util.stream.Collectors;
 
 /**
@@ -69,10 +71,13 @@
 
     private final RcsStats mRcsStats;
     int mSubId;
+    private SipDialogsStateListener mSipDialogsListener;
+    private String mDelegateKey;
 
     public SipSessionTracker(int subId, RcsStats rcsStats) {
         mSubId = subId;
         mRcsStats = rcsStats;
+        mDelegateKey = String.valueOf(UUID.randomUUID());
     }
 
     /**
@@ -152,6 +157,7 @@
             logi("Dialog closed: " + d);
         }
         mTrackedDialogs.removeAll(dialogsToCleanup);
+        notifySipDialogState();
     }
 
     /**
@@ -213,6 +219,7 @@
         }
         mTrackedDialogs.clear();
         mPendingAck.clear();
+        notifySipDialogState();
     }
 
     /**
@@ -308,6 +315,7 @@
                 d.close();
                 logi("Dialog closed: " + d);
             }
+            notifySipDialogState();
         };
     }
 
@@ -365,16 +373,47 @@
         if (statusCode >= 300) {
             mRcsStats.onSipTransportSessionClosed(mSubId, m.getCallIdParameter(), statusCode, true);
             d.close();
+            notifySipDialogState();
             return;
         }
         if (toTag == null) logw("updateSipDialogState: No to tag for message: " + m);
         if (statusCode >= 200) {
             mRcsStats.confirmedSipTransportSession(m.getCallIdParameter(), statusCode);
             d.confirm(toTag);
+            notifySipDialogState();
             return;
         }
         // 1XX responses still require updates to dialogs.
         d.earlyResponse(toTag);
+        notifySipDialogState();
+    }
+
+    /**
+     * This is a listener to handle SipDialog state of delegate
+     * @param listener {@link SipDialogsStateListener}
+     * @param isNeedNotify It indicates whether the current dialogs state should be notified.
+     */
+    public void setSipDialogsListener(SipDialogsStateListener listener,
+            boolean isNeedNotify) {
+        mSipDialogsListener = listener;
+        if (listener == null) {
+            return;
+        }
+        if (isNeedNotify) {
+            notifySipDialogState();
+        }
+    }
+
+    private void notifySipDialogState() {
+        if (mSipDialogsListener == null) {
+            return;
+        }
+        List<SipDialogState> dialogStates = new ArrayList<>();
+        for (SipDialog d : mTrackedDialogs) {
+            SipDialogState dialog = new SipDialogState.Builder(d.getState()).build();
+            dialogStates.add(dialog);
+        }
+        mSipDialogsListener.reMappingSipDelegateState(mDelegateKey, dialogStates);
     }
 
     private void logi(String log) {
diff --git a/src/com/android/services/telephony/rcs/SipTransportController.java b/src/com/android/services/telephony/rcs/SipTransportController.java
index 1fc1349..2f090d5 100644
--- a/src/com/android/services/telephony/rcs/SipTransportController.java
+++ b/src/com/android/services/telephony/rcs/SipTransportController.java
@@ -29,6 +29,7 @@
 import android.telephony.ims.ImsException;
 import android.telephony.ims.ImsService;
 import android.telephony.ims.SipDelegateManager;
+import android.telephony.ims.SipDialogState;
 import android.telephony.ims.aidl.IImsRegistration;
 import android.telephony.ims.aidl.ISipDelegate;
 import android.telephony.ims.aidl.ISipDelegateConnectionStateCallback;
@@ -46,6 +47,8 @@
 
 import com.android.ims.RcsFeatureManager;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.telephony.ISipDialogStateCallback;
+import com.android.internal.telephony.util.RemoteCallbackListExt;
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.phone.RcsProvisioningMonitor;
 
@@ -54,9 +57,11 @@
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Locale;
+import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.Callable;
 import java.util.concurrent.CompletableFuture;
@@ -239,6 +244,65 @@
     }
 
     /**
+     * This is to handle with dialogs of all available delegates that have dialogs.
+     */
+    private final class SipDialogsStateHandle implements SipDialogsStateListener {
+
+        private Executor mExecutor = Runnable::run;
+        Map<String, List<SipDialogState>> mMapDialogState = new HashMap<>();
+
+        /**
+         * This will be called using the {@link SipDialogsStateListener}
+         * @param key This is the ID of the SipSessionTracker for handling the dialogs of
+         *               each created delegates.
+         * @param dialogStates This is a list of dialog states tracked in SipSessionTracker.
+         */
+        @Override
+        public void reMappingSipDelegateState(String key,
+                List<SipDialogState> dialogStates) {
+            mExecutor.execute(()->processReMappingSipDelegateState(key, dialogStates));
+        }
+
+        /**
+         * Notify SipDialogState information with
+         * {@link com.android.internal.telephony.ISipDialogStateCallback}
+         */
+        @Override
+        public void notifySipDialogState() {
+            mExecutor.execute(()->processNotifySipDialogState());
+        }
+
+        private void processReMappingSipDelegateState(String key,
+                List<SipDialogState> dialogStates) {
+            if (dialogStates.isEmpty()) {
+                mMapDialogState.remove(key);
+            } else {
+                mMapDialogState.put(key, dialogStates);
+            }
+            notifySipDialogState();
+        }
+
+        private void processNotifySipDialogState() {
+            List<SipDialogState> finalDialogStates = new ArrayList<>();
+            for (List<SipDialogState> d : mMapDialogState.values()) {
+                finalDialogStates.addAll(d);
+            }
+
+            if (mSipDialogStateCallbacks.getRegisteredCallbackCount() == 0) {
+                return;
+            }
+            mSipDialogStateCallbacks.broadcastAction((c) -> {
+                try {
+                    c.onActiveSipDialogsChanged(finalDialogStates);
+                } catch (RemoteException e) {
+                    Log.e(LOG_TAG,
+                            "onActiveSipDialogsChanged() - Skipping callback." + e);
+                }
+            });
+        }
+    }
+
+    /**
      * Allow the ability for tests to easily mock out the SipDelegateController for testing.
      */
     @VisibleForTesting
@@ -266,6 +330,12 @@
     private final List<SipDelegateController> mDelegatePendingCreate = new ArrayList<>();
     // SipDelegateControllers that are pending to be destroyed.
     private final List<DestroyRequest> mDelegatePendingDestroy = new ArrayList<>();
+    // SipDialogStateCallback that are adding to use callback.
+    private final RemoteCallbackListExt<ISipDialogStateCallback> mSipDialogStateCallbacks =
+            new RemoteCallbackListExt<>();
+    // To listen the state information if the dialog status is changed from the SipSessionTracker.
+    private final SipDialogsStateListener mSipDialogsListener = new SipDialogsStateHandle();
+
     // Cache of Binders to remote IMS applications for tracking their potential death
     private final TrackedAppBinders mActiveAppBinders = new TrackedAppBinders();
 
@@ -458,6 +528,10 @@
         logi("createSipDelegateInternal: request= " + request + ", packageName= " + packageName
                 + ", controller created: " + c);
         addPendingCreateAndEvaluate(c);
+        // If SipDialogStateCallback is registered, listener will be set.
+        if (mSipDialogStateCallbacks.getRegisteredCallbackCount() > 0) {
+            c.setSipDialogsListener(mSipDialogsListener, false);
+        }
     }
 
     private void destroySipDelegateInternal(int subId, ISipDelegate connection, int reason) {
@@ -1057,6 +1131,56 @@
         }
     }
 
+    /**
+     * Adds a callback that gets called when SipDialog status has changed.
+     * @param subId The subId associated with the request.
+     * @param cb A {@link android.telephony.ims.SipDialogStateCallback} that will notify the caller
+     *          when Dialog status has changed.
+     */
+    public void addCallbackForSipDialogState(int subId, ISipDialogStateCallback cb) {
+        if (subId != mSubId) {
+            logw("addCallbackForSipDialogState the subId is not supported");
+            return;
+        }
+        // callback register and no delegate : register this callback / notify (empty state)
+        // callback register and delegates : register this callback / release listener / notify
+        mSipDialogStateCallbacks.register(cb);
+        if (!mDelegatePriorityQueue.isEmpty()) {
+            for (SipDelegateController dc : mDelegatePriorityQueue) {
+                dc.setSipDialogsListener(mSipDialogsListener, true);
+            }
+        } else {
+            mSipDialogsListener.notifySipDialogState();
+        }
+    }
+
+    /**
+     * Unregister previously registered callback
+     * @param subId The subId associated with the request.
+     * @param cb A {@link android.telephony.ims.SipDialogStateCallback} that will be unregistering.
+     */
+    public void removeCallbackForSipDialogState(int subId, ISipDialogStateCallback cb) {
+        if (subId != mSubId) {
+            logw("addCallbackForSipDialogState the subId is not supported");
+            return;
+        }
+        if (cb == null) {
+            throw new IllegalArgumentException("callback is null");
+        }
+
+        // remove callback register and no delegate : only unregister this callback
+        // remove callback register and delegates :
+        // unregister this callback and setListener(null)
+        mSipDialogStateCallbacks.unregister(cb);
+        if (mSipDialogStateCallbacks.getRegisteredCallbackCount() == 0) {
+            if (!mDelegatePriorityQueue.isEmpty()) {
+                for (SipDelegateController dc : mDelegatePriorityQueue) {
+                    dc.setSipDialogsListener(null, false);
+                }
+            }
+        }
+    }
+
     @Override
     public void dump(PrintWriter printWriter) {
         IndentingPrintWriter pw = new IndentingPrintWriter(printWriter, "  ");
diff --git a/src/com/android/services/telephony/rcs/TransportSipMessageValidator.java b/src/com/android/services/telephony/rcs/TransportSipMessageValidator.java
index 3b1b26d..b15992e 100644
--- a/src/com/android/services/telephony/rcs/TransportSipMessageValidator.java
+++ b/src/com/android/services/telephony/rcs/TransportSipMessageValidator.java
@@ -577,6 +577,16 @@
         }
     }
 
+    /**
+     * This is a listener to handle SipDialog state of delegate
+     * @param listener {@link SipDialogsStateListener}
+     * @param isNeedNotify It indicates whether the current dialogs state should be notified.
+     */
+    public void setSipDialogsListener(SipDialogsStateListener listener,
+            boolean isNeedNotify) {
+        mSipSessionTracker.setSipDialogsListener(listener, isNeedNotify);
+    }
+
     private void logi(String log) {
         Log.i(SipTransportController.LOG_TAG, LOG_TAG + "[" + mSubId + "] " + log);
         mLocalLog.log("[I] " + log);