diff options
Diffstat (limited to 'java/com/android/dialer/calllog/database')
4 files changed, 119 insertions, 14 deletions
diff --git a/java/com/android/dialer/calllog/database/AnnotatedCallLogContentProvider.java b/java/com/android/dialer/calllog/database/AnnotatedCallLogContentProvider.java index a9c0d36b0..30aa2bff5 100644 --- a/java/com/android/dialer/calllog/database/AnnotatedCallLogContentProvider.java +++ b/java/com/android/dialer/calllog/database/AnnotatedCallLogContentProvider.java @@ -41,6 +41,13 @@ import java.util.ArrayList; /** {@link ContentProvider} for the annotated call log. */ public class AnnotatedCallLogContentProvider extends ContentProvider { + /** + * We sometimes run queries where we potentially pass every ID into a where clause using the + * (?,?,?,...) syntax. The maximum number of host parameters is 999, so that's the maximum size + * this table can be. See https://www.sqlite.org/limits.html for more details. + */ + private static final int MAX_ROWS = 999; + private static final int ANNOTATED_CALL_LOG_TABLE_CODE = 1; private static final int ANNOTATED_CALL_LOG_TABLE_ID_CODE = 2; private static final int COALESCED_ANNOTATED_CALL_LOG_TABLE_CODE = 3; @@ -72,7 +79,7 @@ public class AnnotatedCallLogContentProvider extends ContentProvider { @Override public boolean onCreate() { - databaseHelper = new AnnotatedCallLogDatabaseHelper(getContext()); + databaseHelper = new AnnotatedCallLogDatabaseHelper(getContext(), MAX_ROWS); coalescer = CallLogDatabaseComponent.get(getContext()).coalescer(); return true; } @@ -283,7 +290,21 @@ public class AnnotatedCallLogContentProvider extends ContentProvider { throw new OperationApplicationException("error inserting row"); } } else if (result.count == 0) { - throw new OperationApplicationException("error updating or deleting rows"); + /* + * The batches built by MutationApplier happen to contain operations in order of: + * + * 1. Inserts + * 2. Updates + * 3. Deletes + * + * Let's say the last row in the table is row Z, and MutationApplier wishes to update it, + * as well as insert row A. When row A gets inserted, row Z will be deleted via the + * trigger if the table is full. Then later, when we try to process the update for row Z, + * it won't exist. + */ + LogUtil.w( + "AnnotatedCallLogContentProvider.applyBatch", + "update or delete failed, possibly because row got cleaned up"); } results[i] = result; } diff --git a/java/com/android/dialer/calllog/database/AnnotatedCallLogDatabaseHelper.java b/java/com/android/dialer/calllog/database/AnnotatedCallLogDatabaseHelper.java index 3cca639ff..887dfcbb6 100644 --- a/java/com/android/dialer/calllog/database/AnnotatedCallLogDatabaseHelper.java +++ b/java/com/android/dialer/calllog/database/AnnotatedCallLogDatabaseHelper.java @@ -21,28 +21,53 @@ import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import com.android.dialer.calllog.database.contract.AnnotatedCallLogContract.AnnotatedCallLog; import com.android.dialer.common.LogUtil; +import java.util.Locale; /** {@link SQLiteOpenHelper} for the AnnotatedCallLog database. */ class AnnotatedCallLogDatabaseHelper extends SQLiteOpenHelper { + private final int maxRows; - AnnotatedCallLogDatabaseHelper(Context appContext) { + AnnotatedCallLogDatabaseHelper(Context appContext, int maxRows) { super(appContext, "annotated_call_log.db", null, 1); + this.maxRows = maxRows; } - private static final String CREATE_SQL = + private static final String CREATE_TABLE_SQL = new StringBuilder() .append("create table if not exists " + AnnotatedCallLog.TABLE + " (") .append(AnnotatedCallLog._ID + " integer primary key, ") .append(AnnotatedCallLog.TIMESTAMP + " integer, ") - .append(AnnotatedCallLog.CONTACT_NAME + " string") + .append(AnnotatedCallLog.CONTACT_NAME + " string, ") + .append(AnnotatedCallLog.NUMBER + " blob") .append(");") .toString(); + /** Deletes all but the first maxRows rows (by timestamp) 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 + + ") > %d" + + " begin delete from " + + AnnotatedCallLog.TABLE + + " where " + + AnnotatedCallLog._ID + + " in (select " + + AnnotatedCallLog._ID + + " from " + + AnnotatedCallLog.TABLE + + " order by timestamp limit (select count(*)-%d" + + " from " + + AnnotatedCallLog.TABLE + + " )); end;"; + @Override public void onCreate(SQLiteDatabase db) { LogUtil.enterBlock("AnnotatedCallLogDatabaseHelper.onCreate"); long startTime = System.currentTimeMillis(); - db.execSQL(CREATE_SQL); + db.execSQL(CREATE_TABLE_SQL); + db.execSQL(String.format(Locale.US, CREATE_TRIGGER_SQL, maxRows, maxRows)); // TODO: Consider logging impression. LogUtil.i( "AnnotatedCallLogDatabaseHelper.onCreate", diff --git a/java/com/android/dialer/calllog/database/Coalescer.java b/java/com/android/dialer/calllog/database/Coalescer.java index e3dfb7ece..5683687fd 100644 --- a/java/com/android/dialer/calllog/database/Coalescer.java +++ b/java/com/android/dialer/calllog/database/Coalescer.java @@ -17,15 +17,18 @@ package com.android.dialer.calllog.database; import android.content.ContentValues; import android.database.Cursor; -import android.database.DatabaseUtils; import android.database.MatrixCursor; import android.support.annotation.NonNull; import android.support.annotation.WorkerThread; +import com.android.dialer.DialerPhoneNumber; import com.android.dialer.calllog.database.contract.AnnotatedCallLogContract.AnnotatedCallLog; import com.android.dialer.calllog.database.contract.AnnotatedCallLogContract.CoalescedAnnotatedCallLog; import com.android.dialer.calllog.datasources.CallLogDataSource; import com.android.dialer.calllog.datasources.DataSources; import com.android.dialer.common.Assert; +import com.android.dialer.phonenumberproto.DialerPhoneNumberUtil; +import com.google.i18n.phonenumbers.PhoneNumberUtil; +import com.google.protobuf.InvalidProtocolBufferException; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -64,6 +67,9 @@ public class Coalescer { // Note: This method relies on rowsShouldBeCombined to determine which rows should be combined, // but delegates to data sources to actually aggregate column values. + DialerPhoneNumberUtil dialerPhoneNumberUtil = + new DialerPhoneNumberUtil(PhoneNumberUtil.getInstance()); + MatrixCursor allCoalescedRowsMatrixCursor = new MatrixCursor( CoalescedAnnotatedCallLog.ALL_COLUMNS, @@ -75,9 +81,8 @@ public class Coalescer { List<ContentValues> currentRowGroup = new ArrayList<>(); do { - ContentValues currentRow = new ContentValues(); - DatabaseUtils.cursorRowToContentValues( - allAnnotatedCallLogRowsSortedByTimestampDesc, currentRow); + ContentValues currentRow = + cursorRowToContentValues(allAnnotatedCallLogRowsSortedByTimestampDesc); if (currentRowGroup.isEmpty()) { currentRowGroup.add(currentRow); @@ -86,7 +91,7 @@ public class Coalescer { ContentValues previousRow = currentRowGroup.get(currentRowGroup.size() - 1); - if (!rowsShouldBeCombined(previousRow, currentRow)) { + if (!rowsShouldBeCombined(dialerPhoneNumberUtil, previousRow, currentRow)) { ContentValues coalescedRow = coalesceRowsForAllDataSources(currentRowGroup); coalescedRow.put(CoalescedAnnotatedCallLog.NUMBER_CALLS, currentRowGroup.size()); addContentValuesToMatrixCursor( @@ -104,13 +109,46 @@ public class Coalescer { return allCoalescedRowsMatrixCursor; } + private static ContentValues cursorRowToContentValues(Cursor cursor) { + ContentValues values = new ContentValues(); + String[] columns = cursor.getColumnNames(); + int length = columns.length; + for (int i = 0; i < length; i++) { + if (cursor.getType(i) == Cursor.FIELD_TYPE_BLOB) { + values.put(columns[i], cursor.getBlob(i)); + } else { + values.put(columns[i], cursor.getString(i)); + } + } + return values; + } + /** * @param row1 a row from {@link AnnotatedCallLog} * @param row2 a row from {@link AnnotatedCallLog} */ - private static boolean rowsShouldBeCombined(ContentValues row1, ContentValues row2) { + private static boolean rowsShouldBeCombined( + DialerPhoneNumberUtil dialerPhoneNumberUtil, ContentValues row1, ContentValues row2) { // TODO: Real implementation. - return row1.get(AnnotatedCallLog.TIMESTAMP).equals(row2.get(AnnotatedCallLog.TIMESTAMP)); + DialerPhoneNumber number1; + DialerPhoneNumber number2; + try { + number1 = DialerPhoneNumber.parseFrom(row1.getAsByteArray(AnnotatedCallLog.NUMBER)); + number2 = DialerPhoneNumber.parseFrom(row2.getAsByteArray(AnnotatedCallLog.NUMBER)); + } catch (InvalidProtocolBufferException e) { + throw Assert.createAssertionFailException("error parsing DialerPhoneNumber proto", e); + } + + if (!number1.hasDialerInternalPhoneNumber() && !number2.hasDialerInternalPhoneNumber()) { + // Empty numbers should not be combined. + return false; + } + + if (!number1.hasDialerInternalPhoneNumber() || !number2.hasDialerInternalPhoneNumber()) { + // An empty number should not be combined with a non-empty number. + return false; + } + return dialerPhoneNumberUtil.isExactMatch(number1, number2); } /** diff --git a/java/com/android/dialer/calllog/database/contract/AnnotatedCallLogContract.java b/java/com/android/dialer/calllog/database/contract/AnnotatedCallLogContract.java index 8b3b0a852..7f314e37c 100644 --- a/java/com/android/dialer/calllog/database/contract/AnnotatedCallLogContract.java +++ b/java/com/android/dialer/calllog/database/contract/AnnotatedCallLogContract.java @@ -66,6 +66,18 @@ public class AnnotatedCallLogContract { /** The MIME type of a {@link android.content.ContentProvider#getType(Uri)} single entry. */ public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/annotated_call_log"; + + /** + * The phone number called or number the call came from, encoded as a {@link + * com.android.dialer.DialerPhoneNumber} proto. The number may be empty if it was an incoming + * call and the number was unknown. + * + * <p>This column is only present in the annotated call log, and not the coalesced annotated + * call log. The coalesced version uses a formatted number string rather than proto bytes. + * + * <p>Type: BLOB + */ + public static final String NUMBER = "number"; } /** @@ -96,10 +108,19 @@ public class AnnotatedCallLogContract { public static final String NUMBER_CALLS = "number_calls"; /** + * The phone number formatted in a way suitable for display to the user. This value is generated + * on the fly when the {@link CoalescedAnnotatedCallLog} is generated. + * + * <p>Type: TEXT + */ + public static final String FORMATTED_NUMBER = "formatted_number"; + + /** * Columns that are only in the {@link CoalescedAnnotatedCallLog} but not the {@link * AnnotatedCallLog}. */ - private static final String[] COLUMNS_ONLY_IN_COALESCED_CALL_LOG = new String[] {NUMBER_CALLS}; + private static final String[] COLUMNS_ONLY_IN_COALESCED_CALL_LOG = + new String[] {NUMBER_CALLS, FORMATTED_NUMBER}; /** All columns in the {@link CoalescedAnnotatedCallLog}. */ public static final String[] ALL_COLUMNS = |