Add count and logic for sms metrics.

Add count field to SmsOutgoing and SmsIncoming.
Change logic when over 25 sms:
If there is a similar sms then update count
else replace the lowest count sms with the new one.

Bug: 233264472
Test: make, manual --Checking if after 25 outgoing and incoming sms, the count is updated

Change-Id: I323e2b593698af7945ad2db4f1d9ba030e1b5ea9
diff --git a/proto/src/persist_atoms.proto b/proto/src/persist_atoms.proto
index f8b5f20..bf55d0d 100644
--- a/proto/src/persist_atoms.proto
+++ b/proto/src/persist_atoms.proto
@@ -248,6 +248,10 @@
     optional bool is_esim = 12;
     optional int32 carrier_id = 13;
     optional int64 message_id = 14;
+    optional int32 count = 15;
+
+    // Internal use only
+    optional int32 hashCode = 10001;
 }
 
 message OutgoingSms {
@@ -265,6 +269,10 @@
     optional int64 message_id = 12;
     optional int32 retry_id = 13;
     optional int64 interval_millis = 14;
+    optional int32 count = 15;
+
+    // Internal use only
+    optional int32 hashCode = 10001;
 }
 
 message CarrierIdMismatch {
diff --git a/src/java/com/android/internal/telephony/metrics/MetricsCollector.java b/src/java/com/android/internal/telephony/metrics/MetricsCollector.java
index b660617..9da1d2b 100644
--- a/src/java/com/android/internal/telephony/metrics/MetricsCollector.java
+++ b/src/java/com/android/internal/telephony/metrics/MetricsCollector.java
@@ -787,7 +787,8 @@
                 sms.isMultiSim,
                 sms.isEsim,
                 sms.carrierId,
-                sms.messageId);
+                sms.messageId,
+                sms.count);
     }
 
     private static StatsEvent buildStatsEvent(OutgoingSms sms) {
@@ -806,7 +807,8 @@
                 sms.carrierId,
                 sms.messageId,
                 sms.retryId,
-                sms.intervalMillis);
+                sms.intervalMillis,
+                sms.count);
     }
 
     private static StatsEvent buildStatsEvent(DataCallSession dataCallSession) {
diff --git a/src/java/com/android/internal/telephony/metrics/PersistAtomsStorage.java b/src/java/com/android/internal/telephony/metrics/PersistAtomsStorage.java
index 9a6263a..000d1e6 100644
--- a/src/java/com/android/internal/telephony/metrics/PersistAtomsStorage.java
+++ b/src/java/com/android/internal/telephony/metrics/PersistAtomsStorage.java
@@ -26,6 +26,7 @@
 import android.os.HandlerThread;
 import android.telephony.TelephonyManager;
 import android.telephony.TelephonyManager.NetworkTypeBitMask;
+import android.util.SparseIntArray;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.telephony.nano.PersistAtomsProto.CarrierIdMismatch;
@@ -256,6 +257,7 @@
 
     /** Adds an incoming SMS to the storage. */
     public synchronized void addIncomingSms(IncomingSms sms) {
+        sms.hashCode = SmsStats.getSmsHashCode(sms);
         mAtoms.incomingSms = insertAtRandomPlace(mAtoms.incomingSms, sms, mMaxNumSms);
         saveAtomsToFile(SAVE_TO_FILE_DELAY_FOR_UPDATE_MILLIS);
 
@@ -265,6 +267,7 @@
 
     /** Adds an outgoing SMS to the storage. */
     public synchronized void addOutgoingSms(OutgoingSms sms) {
+        sms.hashCode = SmsStats.getSmsHashCode(sms);
         // Update the retry id, if needed, so that it's unique and larger than all
         // previous ones. (this algorithm ignores the fact that some SMS atoms might
         // be dropped due to limit in size of the array).
@@ -1649,8 +1652,9 @@
     }
 
     /**
-     * Inserts a new element in a random position in an array with a maximum size, replacing the
-     * least recent item if possible.
+     * Inserts a new element in a random position in an array with a maximum size.
+     *
+     * <p>If the array is full, merge with existing item if possible or replace one item randomly.
      */
     private static <T> T[] insertAtRandomPlace(T[] storage, T instance, int maxLength) {
         final int newLength = storage.length + 1;
@@ -1659,7 +1663,11 @@
         if (newLength == 1) {
             result[0] = instance;
         } else if (arrayFull) {
-            result[findItemToEvict(storage)] = instance;
+            if (instance instanceof OutgoingSms || instance instanceof IncomingSms) {
+                mergeSmsOrEvictInFullStorage(result, instance);
+            } else {
+                result[findItemToEvict(storage)] = instance;
+            }
         } else {
             // insert at random place (by moving the item at the random place to the end)
             int insertAt = sRandom.nextInt(newLength);
@@ -1669,6 +1677,90 @@
         return result;
     }
 
+    /**
+     * Merge new sms in a full storage.
+     *
+     * <p>If new sms is similar to old sms, merge them.
+     * If not, merge 2 old similar sms and add the new sms.
+     * If not, replace old sms with the lowest count.
+     */
+    private static <T> void mergeSmsOrEvictInFullStorage(T[] storage, T instance) {
+        // key: hashCode, value: smsIndex
+        SparseIntArray map = new SparseIntArray();
+        int smsIndex1 = -1;
+        int smsIndex2 = -1;
+        int indexLowestCount = -1;
+        int minCount = Integer.MAX_VALUE;
+
+        for (int i = 0; i < storage.length; i++) {
+            // If the new SMS can be merged to an existing item, merge it and return immediately.
+            if (areSmsMergeable(storage[i], instance)) {
+                storage[i] = mergeSms(storage[i], instance);
+                return;
+            }
+
+            // Keep sms index with lowest count to evict, in case we cannot merge any 2 messages.
+            int smsCount = getSmsCount(storage[i]);
+            if (smsCount < minCount) {
+                indexLowestCount = i;
+                minCount = smsCount;
+            }
+
+            // Find any 2 messages in the storage that can be merged together.
+            if (smsIndex1 != -1) {
+                int smsHashCode = getSmsHashCode(storage[i]);
+                if (map.indexOfKey(smsHashCode) < 0) {
+                    map.append(smsHashCode, i);
+                } else {
+                    smsIndex1 = map.get(smsHashCode);
+                    smsIndex2 = i;
+                }
+            }
+        }
+
+        // Merge 2 similar old sms and add the new sms
+        if (smsIndex1 != -1) {
+            storage[smsIndex1] = mergeSms(storage[smsIndex1], storage[smsIndex2]);
+            storage[smsIndex2] = instance;
+            return;
+        }
+
+        // Or replace old sms that has the lowest count
+        storage[indexLowestCount] = instance;
+        return;
+    }
+
+    private static <T> int getSmsHashCode(T sms) {
+        return sms instanceof OutgoingSms
+                ? ((OutgoingSms) sms).hashCode : ((IncomingSms) sms).hashCode;
+    }
+
+    private static <T> int getSmsCount(T sms) {
+        return sms instanceof OutgoingSms
+                ? ((OutgoingSms) sms).count : ((IncomingSms) sms).count;
+    }
+
+    /** Compares 2 SMS hash codes to check if they can be clubbed together in the metrics. */
+    private static <T> boolean areSmsMergeable(T instance1, T instance2) {
+        return getSmsHashCode(instance1) == getSmsHashCode(instance2);
+    }
+
+    /** Merges sms2 data on top of sms1 and returns the merged value. */
+    private static <T> T mergeSms(T sms1, T sms2) {
+        if (sms1 instanceof OutgoingSms) {
+            OutgoingSms tSms1 = (OutgoingSms) sms1;
+            OutgoingSms tSms2 = (OutgoingSms) sms2;
+            tSms1.intervalMillis = (tSms1.intervalMillis * tSms1.count
+                    + tSms2.intervalMillis * tSms2.count) / (tSms1.count + tSms2.count);
+            tSms1.count += tSms2.count;
+        } else if (sms1 instanceof IncomingSms) {
+            IncomingSms tSms1 = (IncomingSms) sms1;
+            IncomingSms tSms2 = (IncomingSms) sms2;
+            tSms1.count += tSms2.count;
+        }
+        return sms1;
+    }
+
     /** Returns index of the item suitable for eviction when the array is full. */
     private static <T> int findItemToEvict(T[] array) {
         if (array instanceof CellularServiceState[]) {
diff --git a/src/java/com/android/internal/telephony/metrics/SmsStats.java b/src/java/com/android/internal/telephony/metrics/SmsStats.java
index af7e23e..48826fd 100644
--- a/src/java/com/android/internal/telephony/metrics/SmsStats.java
+++ b/src/java/com/android/internal/telephony/metrics/SmsStats.java
@@ -61,6 +61,7 @@
 import com.android.internal.telephony.nano.PersistAtomsProto.OutgoingSms;
 import com.android.telephony.Rlog;
 
+import java.util.Objects;
 import java.util.Random;
 
 /** Collects sms events per phone ID for the pulled atom. */
@@ -214,6 +215,7 @@
         // Message ID is initialized with random number, as it is not available for all incoming
         // SMS messages (e.g. those handled by OS or error cases).
         proto.messageId = RANDOM.nextLong();
+        proto.count = 1;
         return proto;
     }
 
@@ -238,6 +240,7 @@
         // in the persistent storage.
         proto.retryId = 0;
         proto.intervalMillis = intervalMillis;
+        proto.count = 1;
         return proto;
     }
 
@@ -304,6 +307,26 @@
         }
     }
 
+    /**
+     * Returns a hash value to identify messages that are identical for the purpose of merging them
+     * together when storage is full.
+     */
+    static int getSmsHashCode(OutgoingSms sms) {
+        return Objects.hash(sms.smsFormat, sms.smsTech, sms.rat, sms.sendResult, sms.errorCode,
+                    sms.isRoaming, sms.isFromDefaultApp, sms.simSlotIndex, sms.isMultiSim,
+                    sms.isEsim, sms.carrierId);
+    }
+
+    /**
+     * Returns a hash value to identify messages that are identical for the purpose of merging them
+     * together when storage is full.
+     */
+    static int getSmsHashCode(IncomingSms sms) {
+        return Objects.hash(sms.smsFormat, sms.smsTech, sms.rat, sms.smsType,
+            sms.totalParts, sms.receivedParts, sms.blocked, sms.error,
+            sms.isRoaming, sms.simSlotIndex, sms.isMultiSim, sms.isEsim, sms.carrierId);
+    }
+
     private int getPhoneId() {
         Phone phone = mPhone;
         if (mPhone.getPhoneType() == PhoneConstants.PHONE_TYPE_IMS) {