Don't garbage collect voicemails in AnnotatedCallLog.

We currently limit the size of AnnotatedCallLog to 999 via a trigger, but it doesn't exclude voicemails. Since we don't want to ever garbage collect the user's voicemails, exclude them from the trigger.

This means that we can no longer assume a maximum size for the table (the user culd have more than 999 voicemails) so I've updated the places in the code where we did that before.

Finally, I changed AnnotatedCallLog's CALL_TYPE column to be non-null. This is so that we can have more confidence that the trigger will work as intended. Null values cannot be compared in SQLite, so an expression like "where call_type != 4" won't actually match a null call type. Rather than implicitly fail to clean up such rows, we just crash completely when encountering such rows (even though I don't expect that to happen).

Bug: 70989634
Test: unit
PiperOrigin-RevId: 183006714
Change-Id: I9f4394a4812afe4b65c1e8427c355d825356557c
diff --git a/java/com/android/dialer/calllog/database/AnnotatedCallLogDatabaseHelper.java b/java/com/android/dialer/calllog/database/AnnotatedCallLogDatabaseHelper.java
index da93ff8..eed77eb 100644
--- a/java/com/android/dialer/calllog/database/AnnotatedCallLogDatabaseHelper.java
+++ b/java/com/android/dialer/calllog/database/AnnotatedCallLogDatabaseHelper.java
@@ -19,6 +19,7 @@
 import android.content.Context;
 import android.database.sqlite.SQLiteDatabase;
 import android.database.sqlite.SQLiteOpenHelper;
+import android.provider.CallLog.Calls;
 import com.android.dialer.calllog.database.contract.AnnotatedCallLogContract.AnnotatedCallLog;
 import com.android.dialer.common.LogUtil;
 import java.util.Locale;
@@ -52,17 +53,23 @@
           + (AnnotatedCallLog.FEATURES + " integer, ")
           + (AnnotatedCallLog.TRANSCRIPTION + " integer, ")
           + (AnnotatedCallLog.VOICEMAIL_URI + " text, ")
-          + (AnnotatedCallLog.CALL_TYPE + " integer, ")
+          + (AnnotatedCallLog.CALL_TYPE + " integer not null, ")
           + (AnnotatedCallLog.NUMBER_ATTRIBUTES + " blob ")
           + ");";
 
-  /** Deletes all but the first maxRows rows (by timestamp) to keep the table a manageable size. */
-  // TODO(zachh): Prevent voicemails from being garbage collected.
+  /**
+   * Deletes all but the first maxRows rows (by timestamp, excluding voicemails) to keep the table a
+   * manageable size.
+   */
   private static final String CREATE_TRIGGER_SQL =
       "create trigger delete_old_rows after insert on "
           + AnnotatedCallLog.TABLE
           + " when (select count(*) from "
           + AnnotatedCallLog.TABLE
+          + " where "
+          + AnnotatedCallLog.CALL_TYPE
+          + " != "
+          + Calls.VOICEMAIL_TYPE
           + ") > %d"
           + " begin delete from "
           + AnnotatedCallLog.TABLE
@@ -72,10 +79,18 @@
           + AnnotatedCallLog._ID
           + " from "
           + AnnotatedCallLog.TABLE
+          + " where "
+          + AnnotatedCallLog.CALL_TYPE
+          + " != "
+          + Calls.VOICEMAIL_TYPE
           + " order by timestamp limit (select count(*)-%d"
           + " from "
           + AnnotatedCallLog.TABLE
-          + " )); end;";
+          + " where "
+          + AnnotatedCallLog.CALL_TYPE
+          + " != "
+          + Calls.VOICEMAIL_TYPE
+          + ")); end;";
 
   private static final String CREATE_INDEX_ON_CALL_TYPE_SQL =
       "create index call_type_index on "
diff --git a/java/com/android/dialer/calllog/database/MutationApplier.java b/java/com/android/dialer/calllog/database/MutationApplier.java
index 2fb5255..c9edd3e 100644
--- a/java/com/android/dialer/calllog/database/MutationApplier.java
+++ b/java/com/android/dialer/calllog/database/MutationApplier.java
@@ -29,11 +29,13 @@
 import com.android.dialer.common.Assert;
 import com.android.dialer.common.LogUtil;
 import com.android.dialer.common.concurrent.Annotations.BackgroundExecutor;
+import com.google.common.collect.Iterables;
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.ListeningExecutorService;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.List;
 import java.util.Map.Entry;
 import javax.inject.Inject;
 
@@ -97,22 +99,27 @@
     if (!mutations.getDeletes().isEmpty()) {
       LogUtil.i(
           "MutationApplier.applyToDatabase", "deleting %d rows", mutations.getDeletes().size());
-      String[] questionMarks = new String[mutations.getDeletes().size()];
-      Arrays.fill(questionMarks, "?");
 
-      String whereClause =
-          (AnnotatedCallLog._ID + " in (") + TextUtils.join(",", questionMarks) + ")";
+      // Batch the deletes into chunks of 999, the maximum size for SQLite selection args.
+      Iterable<List<Long>> batches = Iterables.partition(mutations.getDeletes(), 999);
+      for (List<Long> idsInBatch : batches) {
+        String[] questionMarks = new String[idsInBatch.size()];
+        Arrays.fill(questionMarks, "?");
 
-      String[] whereArgs = new String[mutations.getDeletes().size()];
-      int i = 0;
-      for (long id : mutations.getDeletes()) {
-        whereArgs[i++] = String.valueOf(id);
+        String whereClause =
+            (AnnotatedCallLog._ID + " in (") + TextUtils.join(",", questionMarks) + ")";
+
+        String[] whereArgs = new String[idsInBatch.size()];
+        int i = 0;
+        for (long id : idsInBatch) {
+          whereArgs[i++] = String.valueOf(id);
+        }
+
+        operations.add(
+            ContentProviderOperation.newDelete(AnnotatedCallLog.CONTENT_URI)
+                .withSelection(whereClause, whereArgs)
+                .build());
       }
-
-      operations.add(
-          ContentProviderOperation.newDelete(AnnotatedCallLog.CONTENT_URI)
-              .withSelection(whereClause, whereArgs)
-              .build());
     }
 
     appContext.getContentResolver().applyBatch(AnnotatedCallLogContract.AUTHORITY, operations);
diff --git a/java/com/android/dialer/calllog/datasources/systemcalllog/SystemCallLogDataSource.java b/java/com/android/dialer/calllog/datasources/systemcalllog/SystemCallLogDataSource.java
index ee169e1..14cde46 100644
--- a/java/com/android/dialer/calllog/datasources/systemcalllog/SystemCallLogDataSource.java
+++ b/java/com/android/dialer/calllog/datasources/systemcalllog/SystemCallLogDataSource.java
@@ -53,6 +53,7 @@
 import com.android.dialer.storage.StorageComponent;
 import com.android.dialer.theme.R;
 import com.android.dialer.util.PermissionsUtil;
+import com.google.common.collect.Iterables;
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.i18n.phonenumbers.PhoneNumberUtil;
@@ -293,7 +294,11 @@
           long id = cursor.getLong(idColumn);
           long date = cursor.getLong(dateColumn);
           String numberAsStr = cursor.getString(numberColumn);
-          long type = cursor.getInt(typeColumn);
+          long type;
+          if (cursor.isNull(typeColumn) || (type = cursor.getInt(typeColumn)) == 0) {
+            // CallLog.Calls#TYPE lists the allowed values, which are non-null and non-zero.
+            throw new IllegalStateException("call type is missing");
+          }
           String countryIso = cursor.getString(countryIsoColumn);
           int duration = cursor.getInt(durationsColumn);
           int dataUsage = cursor.getInt(dataUsageColumn);
@@ -434,38 +439,43 @@
       Context appContext, Set<Long> matchingIds) {
     ArraySet<Long> ids = new ArraySet<>();
 
-    String[] questionMarks = new String[matchingIds.size()];
-    Arrays.fill(questionMarks, "?");
-    String whereClause = (Calls._ID + " in (") + TextUtils.join(",", questionMarks) + ")";
-    String[] whereArgs = new String[matchingIds.size()];
-    int i = 0;
-    for (long id : matchingIds) {
-      whereArgs[i++] = String.valueOf(id);
-    }
+    // Batch the select statements into chunks of 999, the maximum size for SQLite selection args.
+    Iterable<List<Long>> batches = Iterables.partition(matchingIds, 999);
+    for (List<Long> idsInBatch : batches) {
+      String[] questionMarks = new String[idsInBatch.size()];
+      Arrays.fill(questionMarks, "?");
 
-    try (Cursor cursor =
-        appContext
-            .getContentResolver()
-            .query(
-                Calls.CONTENT_URI_WITH_VOICEMAIL,
-                new String[] {Calls._ID},
-                whereClause,
-                whereArgs,
-                null)) {
-
-      if (cursor == null) {
-        LogUtil.e("SystemCallLogDataSource.getIdsFromSystemCallLog", "null cursor");
-        return ids;
+      String whereClause = (Calls._ID + " in (") + TextUtils.join(",", questionMarks) + ")";
+      String[] whereArgs = new String[idsInBatch.size()];
+      int i = 0;
+      for (long id : idsInBatch) {
+        whereArgs[i++] = String.valueOf(id);
       }
 
-      if (cursor.moveToFirst()) {
-        int idColumn = cursor.getColumnIndexOrThrow(Calls._ID);
-        do {
-          ids.add(cursor.getLong(idColumn));
-        } while (cursor.moveToNext());
+      try (Cursor cursor =
+          appContext
+              .getContentResolver()
+              .query(
+                  Calls.CONTENT_URI_WITH_VOICEMAIL,
+                  new String[] {Calls._ID},
+                  whereClause,
+                  whereArgs,
+                  null)) {
+
+        if (cursor == null) {
+          LogUtil.e("SystemCallLogDataSource.getIdsFromSystemCallLog", "null cursor");
+          return ids;
+        }
+
+        if (cursor.moveToFirst()) {
+          int idColumn = cursor.getColumnIndexOrThrow(Calls._ID);
+          do {
+            ids.add(cursor.getLong(idColumn));
+          } while (cursor.moveToNext());
+        }
       }
-      return ids;
     }
+    return ids;
   }
 
   private static class CallLogObserver extends ContentObserver {