Home | History | Annotate | Download | only in database
      1 /*
      2  * Copyright (C) 2017 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License
     15  */
     16 package com.android.dialer.calllog.database;
     17 
     18 import android.content.ContentValues;
     19 import android.database.Cursor;
     20 import android.database.MatrixCursor;
     21 import android.support.annotation.NonNull;
     22 import android.support.annotation.WorkerThread;
     23 import com.android.dialer.DialerPhoneNumber;
     24 import com.android.dialer.calllog.database.contract.AnnotatedCallLogContract.AnnotatedCallLog;
     25 import com.android.dialer.calllog.database.contract.AnnotatedCallLogContract.CoalescedAnnotatedCallLog;
     26 import com.android.dialer.calllog.datasources.CallLogDataSource;
     27 import com.android.dialer.calllog.datasources.DataSources;
     28 import com.android.dialer.common.Assert;
     29 import com.android.dialer.phonenumberproto.DialerPhoneNumberUtil;
     30 import com.google.i18n.phonenumbers.PhoneNumberUtil;
     31 import com.google.protobuf.InvalidProtocolBufferException;
     32 import java.util.ArrayList;
     33 import java.util.List;
     34 import java.util.Map;
     35 import javax.inject.Inject;
     36 
     37 /**
     38  * Coalesces call log rows by combining some adjacent rows.
     39  *
     40  * <p>Applies the business which logic which determines which adjacent rows should be coalasced, and
     41  * then delegates to each data source to determine how individual columns should be aggregated.
     42  */
     43 public class Coalescer {
     44 
     45   private final DataSources dataSources;
     46 
     47   @Inject
     48   Coalescer(DataSources dataSources) {
     49     this.dataSources = dataSources;
     50   }
     51 
     52   /**
     53    * Reads the entire {@link AnnotatedCallLog} database into memory from the provided {@code
     54    * allAnnotatedCallLog} parameter and then builds and returns a new {@link MatrixCursor} which is
     55    * the result of combining adjacent rows which should be collapsed for display purposes.
     56    *
     57    * @param allAnnotatedCallLogRowsSortedByTimestampDesc all {@link AnnotatedCallLog} rows, sorted
     58    *     by timestamp descending
     59    * @return a new {@link MatrixCursor} containing the {@link CoalescedAnnotatedCallLog} rows to
     60    *     display
     61    */
     62   @WorkerThread
     63   @NonNull
     64   Cursor coalesce(@NonNull Cursor allAnnotatedCallLogRowsSortedByTimestampDesc) {
     65     Assert.isWorkerThread();
     66 
     67     // Note: This method relies on rowsShouldBeCombined to determine which rows should be combined,
     68     // but delegates to data sources to actually aggregate column values.
     69 
     70     DialerPhoneNumberUtil dialerPhoneNumberUtil =
     71         new DialerPhoneNumberUtil(PhoneNumberUtil.getInstance());
     72 
     73     MatrixCursor allCoalescedRowsMatrixCursor =
     74         new MatrixCursor(
     75             CoalescedAnnotatedCallLog.ALL_COLUMNS,
     76             Assert.isNotNull(allAnnotatedCallLogRowsSortedByTimestampDesc).getCount());
     77 
     78     if (allAnnotatedCallLogRowsSortedByTimestampDesc.moveToFirst()) {
     79       int coalescedRowId = 0;
     80 
     81       List<ContentValues> currentRowGroup = new ArrayList<>();
     82 
     83       do {
     84         ContentValues currentRow =
     85             cursorRowToContentValues(allAnnotatedCallLogRowsSortedByTimestampDesc);
     86 
     87         if (currentRowGroup.isEmpty()) {
     88           currentRowGroup.add(currentRow);
     89           continue;
     90         }
     91 
     92         ContentValues previousRow = currentRowGroup.get(currentRowGroup.size() - 1);
     93 
     94         if (!rowsShouldBeCombined(dialerPhoneNumberUtil, previousRow, currentRow)) {
     95           ContentValues coalescedRow = coalesceRowsForAllDataSources(currentRowGroup);
     96           coalescedRow.put(CoalescedAnnotatedCallLog.NUMBER_CALLS, currentRowGroup.size());
     97           addContentValuesToMatrixCursor(
     98               coalescedRow, allCoalescedRowsMatrixCursor, coalescedRowId++);
     99           currentRowGroup.clear();
    100         }
    101         currentRowGroup.add(currentRow);
    102       } while (allAnnotatedCallLogRowsSortedByTimestampDesc.moveToNext());
    103 
    104       // Deal with leftover rows.
    105       ContentValues coalescedRow = coalesceRowsForAllDataSources(currentRowGroup);
    106       coalescedRow.put(CoalescedAnnotatedCallLog.NUMBER_CALLS, currentRowGroup.size());
    107       addContentValuesToMatrixCursor(coalescedRow, allCoalescedRowsMatrixCursor, coalescedRowId);
    108     }
    109     return allCoalescedRowsMatrixCursor;
    110   }
    111 
    112   private static ContentValues cursorRowToContentValues(Cursor cursor) {
    113     ContentValues values = new ContentValues();
    114     String[] columns = cursor.getColumnNames();
    115     int length = columns.length;
    116     for (int i = 0; i < length; i++) {
    117       if (cursor.getType(i) == Cursor.FIELD_TYPE_BLOB) {
    118         values.put(columns[i], cursor.getBlob(i));
    119       } else {
    120         values.put(columns[i], cursor.getString(i));
    121       }
    122     }
    123     return values;
    124   }
    125 
    126   /**
    127    * @param row1 a row from {@link AnnotatedCallLog}
    128    * @param row2 a row from {@link AnnotatedCallLog}
    129    */
    130   private static boolean rowsShouldBeCombined(
    131       DialerPhoneNumberUtil dialerPhoneNumberUtil, ContentValues row1, ContentValues row2) {
    132     // TODO: Real implementation.
    133     DialerPhoneNumber number1;
    134     DialerPhoneNumber number2;
    135     try {
    136       number1 = DialerPhoneNumber.parseFrom(row1.getAsByteArray(AnnotatedCallLog.NUMBER));
    137       number2 = DialerPhoneNumber.parseFrom(row2.getAsByteArray(AnnotatedCallLog.NUMBER));
    138     } catch (InvalidProtocolBufferException e) {
    139       throw Assert.createAssertionFailException("error parsing DialerPhoneNumber proto", e);
    140     }
    141 
    142     if (!number1.hasDialerInternalPhoneNumber() && !number2.hasDialerInternalPhoneNumber()) {
    143       // Empty numbers should not be combined.
    144       return false;
    145     }
    146 
    147     if (!number1.hasDialerInternalPhoneNumber() || !number2.hasDialerInternalPhoneNumber()) {
    148       // An empty number should not be combined with a non-empty number.
    149       return false;
    150     }
    151     return dialerPhoneNumberUtil.isExactMatch(number1, number2);
    152   }
    153 
    154   /**
    155    * Delegates to data sources to aggregate individual columns to create a new coalesced row.
    156    *
    157    * @param individualRows {@link AnnotatedCallLog} rows sorted by timestamp descending
    158    * @return a {@link CoalescedAnnotatedCallLog} row
    159    */
    160   private ContentValues coalesceRowsForAllDataSources(List<ContentValues> individualRows) {
    161     ContentValues coalescedValues = new ContentValues();
    162     for (CallLogDataSource dataSource : dataSources.getDataSourcesIncludingSystemCallLog()) {
    163       coalescedValues.putAll(dataSource.coalesce(individualRows));
    164     }
    165     return coalescedValues;
    166   }
    167 
    168   /**
    169    * @param contentValues a {@link CoalescedAnnotatedCallLog} row
    170    * @param matrixCursor represents {@link CoalescedAnnotatedCallLog}
    171    */
    172   private static void addContentValuesToMatrixCursor(
    173       ContentValues contentValues, MatrixCursor matrixCursor, int rowId) {
    174     MatrixCursor.RowBuilder rowBuilder = matrixCursor.newRow();
    175     rowBuilder.add(CoalescedAnnotatedCallLog._ID, rowId);
    176     for (Map.Entry<String, Object> entry : contentValues.valueSet()) {
    177       rowBuilder.add(entry.getKey(), entry.getValue());
    178     }
    179   }
    180 }
    181