Collect connectivity diagnostics related information from DSRS

1. Collect validation and network probe results
2. Sending notification when data stall recovered

Bug: 319601607
Test: Successfully test on Pixel device with simulate data stall environment
Change-Id: Ifbbf20ac5f273fb5cd452f79a055b567bbfe04c6
diff --git a/flags/data.aconfig b/flags/data.aconfig
index 3beb016..a265260 100644
--- a/flags/data.aconfig
+++ b/flags/data.aconfig
@@ -117,4 +117,11 @@
   namespace: "telephony"
   description: "This flag is for internal implementation to handle reconnect request from QNS in telephony FWK."
   bug: "319520561"
+}
+
+flag {
+  name: "dsrs_diagnostics_enabled"
+  namespace: "telephony"
+  description: "Enable DSRS diagnostics."
+  bug: "319601607"
 }
\ No newline at end of file
diff --git a/src/java/com/android/internal/telephony/data/DataStallRecoveryManager.java b/src/java/com/android/internal/telephony/data/DataStallRecoveryManager.java
index 893509c..e9c00d9 100644
--- a/src/java/com/android/internal/telephony/data/DataStallRecoveryManager.java
+++ b/src/java/com/android/internal/telephony/data/DataStallRecoveryManager.java
@@ -48,6 +48,8 @@
 import com.android.internal.telephony.data.DataConfigManager.DataConfigManagerCallback;
 import com.android.internal.telephony.data.DataNetworkController.DataNetworkControllerCallback;
 import com.android.internal.telephony.data.DataSettingsManager.DataSettingsManagerCallback;
+import com.android.internal.telephony.flags.FeatureFlags;
+import com.android.internal.telephony.flags.FeatureFlagsImpl;
 import com.android.internal.telephony.metrics.DataStallRecoveryStats;
 import com.android.internal.telephony.metrics.TelephonyMetrics;
 import com.android.telephony.Rlog;
@@ -153,6 +155,7 @@
     private final @NonNull Phone mPhone;
     private final @NonNull String mLogTag;
     private final @NonNull LocalLog mLocalLog = new LocalLog(128);
+    private final @NonNull FeatureFlags mFeatureFlags = new FeatureFlagsImpl();
 
     /** Data network controller */
     private final @NonNull DataNetworkController mDataNetworkController;
@@ -196,7 +199,10 @@
     private boolean mIsInternetNetworkConnected;
     /** The durations for current recovery action */
     private @ElapsedRealtimeLong long mTimeElapsedOfCurrentAction;
-
+    /** Tracks the total number of validation duration a data stall */
+    private int mValidationCount;
+    /** Tracks the number of validation for current action during a data stall */
+    private int mActionValidationCount;
     /** The array for the timers between recovery actions. */
     private @NonNull long[] mDataStallRecoveryDelayMillisArray;
     /** The boolean array for the flags. They are used to skip the recovery actions if needed. */
@@ -546,6 +552,8 @@
         mTimeLastRecoveryStartMs = 0;
         mLastAction = RECOVERY_ACTION_GET_DATA_CALL_LIST;
         mRecoveryAction = RECOVERY_ACTION_GET_DATA_CALL_LIST;
+        mValidationCount = 0;
+        mActionValidationCount = 0;
     }
 
     /**
@@ -556,8 +564,16 @@
     private void onInternetValidationStatusChanged(@ValidationStatus int status) {
         logl("onInternetValidationStatusChanged: " + DataUtils.validationStatusToString(status));
         final boolean isValid = status == NetworkAgent.VALIDATION_STATUS_VALID;
+        if (mFeatureFlags.dsrsDiagnosticsEnabled()) {
+            mValidationCount += 1;
+            mActionValidationCount += 1;
+        }
         setNetworkValidationState(isValid);
         if (isValid) {
+            if (mFeatureFlags.dsrsDiagnosticsEnabled()) {
+                // Broadcast intent that data stall recovered.
+                broadcastDataStallDetected(getRecoveryAction());
+            }
             reset();
         } else if (isRecoveryNeeded(true)) {
             // Set the network as invalid, because recovery is needed
@@ -596,6 +612,10 @@
      */
     @VisibleForTesting
     public void setRecoveryAction(@RecoveryAction int action) {
+        // Reset the validation count for action change
+        if (mFeatureFlags.dsrsDiagnosticsEnabled() && mRecoveryAction != action) {
+            mActionValidationCount = 0;
+        }
         mRecoveryAction = action;
 
         // Check if the mobile data enabled is TRUE, it means that the mobile data setting changed
@@ -674,13 +694,16 @@
         final boolean isRecovered = !mDataStalled;
         final int duration = (int) (SystemClock.elapsedRealtime() - mDataStallStartMs);
         final @RecoveredReason int reason = getRecoveredReason(mIsValidNetwork);
-        final boolean isFirstValidationOfAction = false;
         final int durationOfAction = (int) getDurationOfCurrentRecoveryMs();
+        if (mFeatureFlags.dsrsDiagnosticsEnabled()) {
+            log("mValidationCount=" + mValidationCount
+                    + ", mActionValidationCount=" + mActionValidationCount);
+        }
 
         // Get the bundled DSRS stats.
         Bundle bundle = mStats.getDataStallRecoveryMetricsData(
-                recoveryAction, isRecovered, duration, reason, isFirstValidationOfAction,
-                durationOfAction);
+                recoveryAction, isRecovered, duration, reason, mValidationCount,
+                mActionValidationCount, durationOfAction);
 
         // Put the bundled stats extras on the intent.
         intent.putExtra("EXTRA_DSRS_STATS_BUNDLE", bundle);
diff --git a/src/java/com/android/internal/telephony/metrics/DataStallRecoveryStats.java b/src/java/com/android/internal/telephony/metrics/DataStallRecoveryStats.java
index 387495e..c34c559 100644
--- a/src/java/com/android/internal/telephony/metrics/DataStallRecoveryStats.java
+++ b/src/java/com/android/internal/telephony/metrics/DataStallRecoveryStats.java
@@ -16,12 +16,27 @@
 
 package com.android.internal.telephony.metrics;
 
+import static android.net.ConnectivityDiagnosticsManager.ConnectivityReport.KEY_NETWORK_PROBES_ATTEMPTED_BITMASK;
+import static android.net.ConnectivityDiagnosticsManager.ConnectivityReport.KEY_NETWORK_PROBES_SUCCEEDED_BITMASK;
+import static android.net.ConnectivityDiagnosticsManager.ConnectivityReport.KEY_NETWORK_VALIDATION_RESULT;
+import static android.net.ConnectivityDiagnosticsManager.DataStallReport.KEY_TCP_METRICS_COLLECTION_PERIOD_MILLIS;
+import static android.net.ConnectivityDiagnosticsManager.DataStallReport.KEY_TCP_PACKET_FAIL_RATE;
+import static android.net.ConnectivityDiagnosticsManager.DataStallReport.KEY_DNS_CONSECUTIVE_TIMEOUTS;
+
+import android.annotation.ElapsedRealtimeLong;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.net.ConnectivityDiagnosticsManager;
+import android.net.ConnectivityDiagnosticsManager.ConnectivityDiagnosticsCallback;
+import android.net.ConnectivityDiagnosticsManager.ConnectivityReport;
+import android.net.ConnectivityDiagnosticsManager.DataStallReport;
 import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.HandlerThread;
+import android.os.PersistableBundle;
+import android.os.SystemClock;
 import android.telephony.AccessNetworkConstants;
 import android.telephony.Annotation.NetworkType;
 import android.telephony.CellSignalStrength;
@@ -30,6 +45,7 @@
 import android.telephony.TelephonyManager;
 import android.telephony.data.DataCallResponse;
 import android.telephony.data.DataCallResponse.LinkStatus;
+import android.text.TextUtils;
 
 import com.android.internal.telephony.Phone;
 import com.android.internal.telephony.PhoneFactory;
@@ -38,11 +54,15 @@
 import com.android.internal.telephony.data.DataNetworkController;
 import com.android.internal.telephony.data.DataNetworkController.DataNetworkControllerCallback;
 import com.android.internal.telephony.data.DataStallRecoveryManager;
+import com.android.internal.telephony.flags.FeatureFlags;
+import com.android.internal.telephony.flags.FeatureFlagsImpl;
 import com.android.internal.telephony.subscription.SubscriptionInfoInternal;
 import com.android.internal.telephony.subscription.SubscriptionManagerService;
 import com.android.telephony.Rlog;
 
 import java.util.Set;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
 
 /**
  * Generates metrics related to data stall recovery events per phone ID for the pushed atom.
@@ -57,18 +77,31 @@
 
     private static final String TAG = "DSRS-";
 
+    private static final int UNSET_DIAGNOSTIC_STATE = -1;
+
+    private static final long REFRESH_DURATION_IN_MILLIS = TimeUnit.MINUTES.toMillis(3);
+
     // Handler to upload metrics.
     private final @NonNull Handler mHandler;
 
     private final @NonNull String mTag;
     private final @NonNull Phone mPhone;
+    private final @NonNull TelephonyManager mTelephonyManager;
+    private final @NonNull FeatureFlags mFeatureFlags = new FeatureFlagsImpl();
+
+    // Flag to control the DSRS diagnostics
+    private final boolean mIsDsrsDiagnosticsEnabled;
 
     // The interface name of the internet network.
     private @Nullable String mIfaceName = null;
 
     /* Metrics and stats data variables */
+    // Record metrics refresh time in milliseconds to decide whether to refresh data again
+    @ElapsedRealtimeLong
+    private long mMetricsReflashTime = 0L;
     private int mPhoneId = 0;
     private int mCarrierId = TelephonyManager.UNKNOWN_CARRIER_ID;
+    private int mConvertedMccMnc = -1;
     private int mSignalStrength = CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN;
     private int mBand = 0;
     // The RAT used for data (including IWLAN).
@@ -88,6 +121,18 @@
     private int mLinkDownBandwidthKbps = LINK_BANDWIDTH_UNSPECIFIED;
     private int mLinkUpBandwidthKbps = LINK_BANDWIDTH_UNSPECIFIED;
 
+    // Connectivity diagnostics states
+    private int mNetworkProbesResult = UNSET_DIAGNOSTIC_STATE;
+    private int mNetworkProbesType = UNSET_DIAGNOSTIC_STATE;
+    private int mNetworkValidationResult = UNSET_DIAGNOSTIC_STATE;
+    private int mTcpMetricsCollectionPeriodMillis = UNSET_DIAGNOSTIC_STATE;
+    private int mTcpPacketFailRate = UNSET_DIAGNOSTIC_STATE;
+    private int mDnsConsecutiveTimeouts = UNSET_DIAGNOSTIC_STATE;
+
+    private ConnectivityDiagnosticsManager mConnectivityDiagnosticsManager = null;
+    private ConnectivityDiagnosticsCallback mConnectivityDiagnosticsCallback = null;
+    private static final Executor INLINE_EXECUTOR = x -> x.run();
+
     /**
      * Constructs a new instance of {@link DataStallRecoveryStats}.
      */
@@ -99,6 +144,7 @@
         HandlerThread handlerThread = new HandlerThread(mTag + "-thread");
         handlerThread.start();
         mHandler = new Handler(handlerThread.getLooper());
+        mTelephonyManager = mPhone.getContext().getSystemService(TelephonyManager.class);
 
         dataNetworkController.registerDataNetworkControllerCallback(
                 new DataNetworkControllerCallback(mHandler::post) {
@@ -117,6 +163,45 @@
                     mInternetLinkStatus = status;
                 }
             });
+
+        mIsDsrsDiagnosticsEnabled = mFeatureFlags.dsrsDiagnosticsEnabled();
+        if (mIsDsrsDiagnosticsEnabled) {
+            try {
+                // Register ConnectivityDiagnosticsCallback to get diagnostics states
+                mConnectivityDiagnosticsManager =
+                    mPhone.getContext().getSystemService(ConnectivityDiagnosticsManager.class);
+                mConnectivityDiagnosticsCallback = new ConnectivityDiagnosticsCallback() {
+                    @Override
+                    public void onConnectivityReportAvailable(@NonNull ConnectivityReport report) {
+                        PersistableBundle bundle = report.getAdditionalInfo();
+                        mNetworkProbesResult = bundle.getInt(KEY_NETWORK_PROBES_SUCCEEDED_BITMASK);
+                        mNetworkProbesType = bundle.getInt(KEY_NETWORK_PROBES_ATTEMPTED_BITMASK);
+                        mNetworkValidationResult = bundle.getInt(KEY_NETWORK_VALIDATION_RESULT);
+                    }
+
+                    @Override
+                    public void onDataStallSuspected(@NonNull DataStallReport report) {
+                        PersistableBundle bundle = report.getStallDetails();
+                        mTcpMetricsCollectionPeriodMillis =
+                            bundle.getInt(KEY_TCP_METRICS_COLLECTION_PERIOD_MILLIS);
+                        mTcpPacketFailRate = bundle.getInt(KEY_TCP_PACKET_FAIL_RATE);
+                        mDnsConsecutiveTimeouts = bundle.getInt(KEY_DNS_CONSECUTIVE_TIMEOUTS);
+                    }
+                };
+                mConnectivityDiagnosticsManager.registerConnectivityDiagnosticsCallback(
+                    new NetworkRequest.Builder()
+                        .clearCapabilities()
+                        .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+                        .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
+                        .build(),
+                        INLINE_EXECUTOR,
+                        mConnectivityDiagnosticsCallback
+                );
+            } catch (Exception e) {
+                mConnectivityDiagnosticsManager = null;
+                mConnectivityDiagnosticsCallback = null;
+            }
+        }
     }
 
     /**
@@ -194,10 +279,26 @@
      */
     private void refreshMetricsData() {
         logd("Refreshes the metrics data.");
+        // Update the metrics reflash time
+        mMetricsReflashTime = SystemClock.elapsedRealtime();
         // Update phone id/carrier id and signal strength
         mPhoneId = mPhone.getPhoneId() + 1;
         mCarrierId = mPhone.getCarrierId();
         mSignalStrength = mPhone.getSignalStrength().getLevel();
+        if (mIsDsrsDiagnosticsEnabled) {
+            // Get the MCCMNC and convert it to an int
+            String networkOperator = mTelephonyManager.getNetworkOperator();
+            if (!TextUtils.isEmpty(networkOperator)) {
+                try {
+                    mConvertedMccMnc = Integer.parseInt(networkOperator);
+                } catch (NumberFormatException e) {
+                    loge("Invalid MCCMNC format: " + networkOperator);
+                    mConvertedMccMnc = -1;
+                }
+            } else {
+                mConvertedMccMnc = -1;
+            }
+        }
 
         // Update the bandwidth.
         updateBandwidths();
@@ -308,7 +409,8 @@
      * @param isRecovered Whether the data stall has been recovered.
      * @param duration The duration from data stall occurred in milliseconds.
      * @param reason The reason for the recovery.
-     * @param isFirstValidation Whether this is the first validation after recovery.
+     * @param validationCount The total number of validation duration a data stall.
+     * @param actionValidationCount The number of validation for current action during a data stall
      * @param durationOfAction The duration of the current action in milliseconds.
      */
     public Bundle getDataStallRecoveryMetricsData(
@@ -316,28 +418,75 @@
             boolean isRecovered,
             int duration,
             @DataStallRecoveryManager.RecoveredReason int reason,
-            boolean isFirstValidation,
+            int validationCount,
+            int actionValidationCount,
             int durationOfAction) {
+
+        if (mIsDsrsDiagnosticsEnabled) {
+            // Refresh data if the data has not been updated within 3 minutes
+            final long refreshDuration = SystemClock.elapsedRealtime() - mMetricsReflashTime;
+            if (refreshDuration > REFRESH_DURATION_IN_MILLIS) {
+                // Refreshes the metrics data.
+                try {
+                    refreshMetricsData();
+                } catch (Exception e) {
+                    loge("The metrics data cannot be refreshed.", e);
+                }
+            }
+        }
+
         Bundle bundle = new Bundle();
-        bundle.putInt("Action", action);
-        bundle.putBoolean("IsRecovered", isRecovered);
-        bundle.putInt("Duration", duration);
-        bundle.putInt("Reason", reason);
-        bundle.putBoolean("IsFirstValidation", isFirstValidation);
-        bundle.putInt("DurationOfAction", durationOfAction);
-        bundle.putInt("PhoneId", mPhoneId);
-        bundle.putInt("CarrierId", mCarrierId);
-        bundle.putInt("SignalStrength", mSignalStrength);
-        bundle.putInt("Band", mBand);
-        bundle.putInt("Rat", mRat);
-        bundle.putBoolean("IsOpportunistic", mIsOpportunistic);
-        bundle.putBoolean("IsMultiSim", mIsMultiSim);
-        bundle.putInt("NetworkRegState", mNetworkRegState);
-        bundle.putInt("OtherSignalStrength", mOtherSignalStrength);
-        bundle.putInt("OtherNetworkRegState", mOtherNetworkRegState);
-        bundle.putInt("InternetLinkStatus", mInternetLinkStatus);
-        bundle.putInt("LinkDownBandwidthKbps", mLinkDownBandwidthKbps);
-        bundle.putInt("LinkUpBandwidthKbps", mLinkUpBandwidthKbps);
+
+        if (mIsDsrsDiagnosticsEnabled) {
+            bundle.putInt("Action", action);
+            bundle.putInt("IsRecovered", isRecovered ? 1 : 0);
+            bundle.putInt("Duration", duration);
+            bundle.putInt("Reason", reason);
+            bundle.putInt("DurationOfAction", durationOfAction);
+            bundle.putInt("ValidationCount", validationCount);
+            bundle.putInt("ActionValidationCount", actionValidationCount);
+            bundle.putInt("PhoneId", mPhoneId);
+            bundle.putInt("CarrierId", mCarrierId);
+            bundle.putInt("MccMnc", mConvertedMccMnc);
+            bundle.putInt("SignalStrength", mSignalStrength);
+            bundle.putInt("Band", mBand);
+            bundle.putInt("Rat", mRat);
+            bundle.putInt("IsOpportunistic", mIsOpportunistic ? 1 : 0);
+            bundle.putInt("IsMultiSim", mIsMultiSim ? 1 : 0);
+            bundle.putInt("NetworkRegState", mNetworkRegState);
+            bundle.putInt("OtherSignalStrength", mOtherSignalStrength);
+            bundle.putInt("OtherNetworkRegState", mOtherNetworkRegState);
+            bundle.putInt("InternetLinkStatus", mInternetLinkStatus);
+            bundle.putInt("LinkDownBandwidthKbps", mLinkDownBandwidthKbps);
+            bundle.putInt("LinkUpBandwidthKbps", mLinkUpBandwidthKbps);
+            bundle.putInt("NetworkProbesResult", mNetworkProbesResult);
+            bundle.putInt("NetworkProbesType", mNetworkProbesType);
+            bundle.putInt("NetworkValidationResult", mNetworkValidationResult);
+            bundle.putInt("TcpMetricsCollectionPeriodMillis", mTcpMetricsCollectionPeriodMillis);
+            bundle.putInt("TcpPacketFailRate", mTcpPacketFailRate);
+            bundle.putInt("DnsConsecutiveTimeouts", mDnsConsecutiveTimeouts);
+        } else {
+            bundle.putInt("Action", action);
+            bundle.putBoolean("IsRecovered", isRecovered);
+            bundle.putInt("Duration", duration);
+            bundle.putInt("Reason", reason);
+            bundle.putBoolean("IsFirstValidation", validationCount == 1);
+            bundle.putInt("DurationOfAction", durationOfAction);
+            bundle.putInt("PhoneId", mPhoneId);
+            bundle.putInt("CarrierId", mCarrierId);
+            bundle.putInt("SignalStrength", mSignalStrength);
+            bundle.putInt("Band", mBand);
+            bundle.putInt("Rat", mRat);
+            bundle.putBoolean("IsOpportunistic", mIsOpportunistic);
+            bundle.putBoolean("IsMultiSim", mIsMultiSim);
+            bundle.putInt("NetworkRegState", mNetworkRegState);
+            bundle.putInt("OtherSignalStrength", mOtherSignalStrength);
+            bundle.putInt("OtherNetworkRegState", mOtherNetworkRegState);
+            bundle.putInt("InternetLinkStatus", mInternetLinkStatus);
+            bundle.putInt("LinkDownBandwidthKbps", mLinkDownBandwidthKbps);
+            bundle.putInt("LinkUpBandwidthKbps", mLinkUpBandwidthKbps);
+        }
+
         return bundle;
     }
 
diff --git a/tests/telephonytests/src/com/android/internal/telephony/data/DataStallRecoveryManagerTest.java b/tests/telephonytests/src/com/android/internal/telephony/data/DataStallRecoveryManagerTest.java
index e508e5b..18efce5 100644
--- a/tests/telephonytests/src/com/android/internal/telephony/data/DataStallRecoveryManagerTest.java
+++ b/tests/telephonytests/src/com/android/internal/telephony/data/DataStallRecoveryManagerTest.java
@@ -58,6 +58,8 @@
 @RunWith(AndroidTestingRunner.class)
 @TestableLooper.RunWithLooper
 public class DataStallRecoveryManagerTest extends TelephonyTest {
+    private static final String KEY_IS_DSRS_DIAGNOSTICS_ENABLED =
+            "is_dsrs_diagnostics_enabled";
     private FakeContentResolver mFakeContentResolver;
 
     // Mocked classes
@@ -429,6 +431,7 @@
     @Test
     public void testSendDSRMData() throws Exception {
         ArgumentCaptor<Intent> captorIntent = ArgumentCaptor.forClass(Intent.class);
+        boolean isDsrsDiagnosticsEnabled = mFeatureFlags.dsrsDiagnosticsEnabled();
 
         logd("Set phone status to normal status.");
         sendOnInternetDataNetworkCallback(true);
@@ -460,8 +463,13 @@
             logd(bundle.toString());
             int size = bundle.size();
             logd("bundle size is " + size);
-            // Check if bundle size is 19
-            assertThat(size).isEqualTo(19);
+            if (isDsrsDiagnosticsEnabled) {
+                // Check if bundle size is 27
+                assertThat(size).isEqualTo(27);
+            } else {
+                // Check if bundle size is 19
+                assertThat(size).isEqualTo(19);
+            }
         }
     }