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 android.telecom.PhoneAccountHandle;
     24 import com.android.dialer.CoalescedIds;
     25 import com.android.dialer.DialerPhoneNumber;
     26 import com.android.dialer.calllog.database.contract.AnnotatedCallLogContract.AnnotatedCallLog;
     27 import com.android.dialer.calllog.database.contract.AnnotatedCallLogContract.CoalescedAnnotatedCallLog;
     28 import com.android.dialer.calllog.datasources.CallLogDataSource;
     29 import com.android.dialer.calllog.datasources.DataSources;
     30 import com.android.dialer.common.Assert;
     31 import com.android.dialer.compat.telephony.TelephonyManagerCompat;
     32 import com.android.dialer.phonenumberproto.DialerPhoneNumberUtil;
     33 import com.android.dialer.telecom.TelecomUtil;
     34 import com.google.common.base.Preconditions;
     35 import com.google.i18n.phonenumbers.PhoneNumberUtil;
     36 import com.google.protobuf.InvalidProtocolBufferException;
     37 import java.util.ArrayList;
     38 import java.util.List;
     39 import java.util.Map;
     40 import java.util.Objects;
     41 import javax.inject.Inject;
     42 
     43 /**
     44  * Coalesces call log rows by combining some adjacent rows.
     45  *
     46  * <p>Applies the logic that determines which adjacent rows should be coalesced, and then delegates
     47  * to each data source to determine how individual columns should be aggregated.
     48  */
     49 public class Coalescer {
     50   private final DataSources dataSources;
     51 
     52   @Inject
     53   Coalescer(DataSources dataSources) {
     54     this.dataSources = dataSources;
     55   }
     56 
     57   /**
     58    * Reads the entire {@link AnnotatedCallLog} database into memory from the provided {@code
     59    * allAnnotatedCallLog} parameter and then builds and returns a new {@link MatrixCursor} which is
     60    * the result of combining adjacent rows which should be collapsed for display purposes.
     61    *
     62    * @param allAnnotatedCallLogRowsSortedByTimestampDesc all {@link AnnotatedCallLog} rows, sorted
     63    *     by timestamp descending
     64    * @return a new {@link MatrixCursor} containing the {@link CoalescedAnnotatedCallLog} rows to
     65    *     display
     66    */
     67   @WorkerThread
     68   @NonNull
     69   Cursor coalesce(@NonNull Cursor allAnnotatedCallLogRowsSortedByTimestampDesc) {
     70     Assert.isWorkerThread();
     71 
     72     // Note: This method relies on rowsShouldBeCombined to determine which rows should be combined,
     73     // but delegates to data sources to actually aggregate column values.
     74 
     75     DialerPhoneNumberUtil dialerPhoneNumberUtil =
     76         new DialerPhoneNumberUtil(PhoneNumberUtil.getInstance());
     77 
     78     MatrixCursor allCoalescedRowsMatrixCursor =
     79         new MatrixCursor(
     80             CoalescedAnnotatedCallLog.ALL_COLUMNS,
     81             Assert.isNotNull(allAnnotatedCallLogRowsSortedByTimestampDesc).getCount());
     82 
     83     if (!allAnnotatedCallLogRowsSortedByTimestampDesc.moveToFirst()) {
     84       return allCoalescedRowsMatrixCursor;
     85     }
     86 
     87     int coalescedRowId = 0;
     88     List<ContentValues> currentRowGroup = new ArrayList<>();
     89 
     90     ContentValues firstRow = cursorRowToContentValues(allAnnotatedCallLogRowsSortedByTimestampDesc);
     91     currentRowGroup.add(firstRow);
     92 
     93     while (!currentRowGroup.isEmpty()) {
     94       // Group consecutive rows
     95       ContentValues firstRowInGroup = currentRowGroup.get(0);
     96       ContentValues currentRow = null;
     97       while (allAnnotatedCallLogRowsSortedByTimestampDesc.moveToNext()) {
     98         currentRow = cursorRowToContentValues(allAnnotatedCallLogRowsSortedByTimestampDesc);
     99 
    100         if (!rowsShouldBeCombined(dialerPhoneNumberUtil, firstRowInGroup, currentRow)) {
    101           break;
    102         }
    103 
    104         currentRowGroup.add(currentRow);
    105       }
    106 
    107       // Coalesce the group into a single row
    108       ContentValues coalescedRow = coalesceRowsForAllDataSources(currentRowGroup);
    109       coalescedRow.put(
    110           CoalescedAnnotatedCallLog.COALESCED_IDS, getCoalescedIds(currentRowGroup).toByteArray());
    111       addContentValuesToMatrixCursor(coalescedRow, allCoalescedRowsMatrixCursor, coalescedRowId++);
    112 
    113       // Clear the current group after the rows are coalesced.
    114       currentRowGroup.clear();
    115 
    116       // Add the first of the remaining rows to the current group.
    117       if (!allAnnotatedCallLogRowsSortedByTimestampDesc.isAfterLast()) {
    118         currentRowGroup.add(currentRow);
    119       }
    120     }
    121 
    122     return allCoalescedRowsMatrixCursor;
    123   }
    124 
    125   private static ContentValues cursorRowToContentValues(Cursor cursor) {
    126     ContentValues values = new ContentValues();
    127     String[] columns = cursor.getColumnNames();
    128     int length = columns.length;
    129     for (int i = 0; i < length; i++) {
    130       if (cursor.getType(i) == Cursor.FIELD_TYPE_BLOB) {
    131         values.put(columns[i], cursor.getBlob(i));
    132       } else {
    133         values.put(columns[i], cursor.getString(i));
    134       }
    135     }
    136     return values;
    137   }
    138 
    139   /**
    140    * @param row1 a row from {@link AnnotatedCallLog}
    141    * @param row2 a row from {@link AnnotatedCallLog}
    142    */
    143   private static boolean rowsShouldBeCombined(
    144       DialerPhoneNumberUtil dialerPhoneNumberUtil, ContentValues row1, ContentValues row2) {
    145     // Don't combine rows which don't use the same phone account.
    146     PhoneAccountHandle phoneAccount1 =
    147         TelecomUtil.composePhoneAccountHandle(
    148             row1.getAsString(AnnotatedCallLog.PHONE_ACCOUNT_COMPONENT_NAME),
    149             row1.getAsString(AnnotatedCallLog.PHONE_ACCOUNT_ID));
    150     PhoneAccountHandle phoneAccount2 =
    151         TelecomUtil.composePhoneAccountHandle(
    152             row2.getAsString(AnnotatedCallLog.PHONE_ACCOUNT_COMPONENT_NAME),
    153             row2.getAsString(AnnotatedCallLog.PHONE_ACCOUNT_ID));
    154 
    155     if (!Objects.equals(phoneAccount1, phoneAccount2)) {
    156       return false;
    157     }
    158 
    159     if (!row1.getAsInteger(AnnotatedCallLog.NUMBER_PRESENTATION)
    160         .equals(row2.getAsInteger(AnnotatedCallLog.NUMBER_PRESENTATION))) {
    161       return false;
    162     }
    163 
    164     if (!meetsAssistedDialingCriteria(row1, row2)) {
    165       return false;
    166     }
    167 
    168     DialerPhoneNumber number1;
    169     DialerPhoneNumber number2;
    170     try {
    171       byte[] number1Bytes = row1.getAsByteArray(AnnotatedCallLog.NUMBER);
    172       byte[] number2Bytes = row2.getAsByteArray(AnnotatedCallLog.NUMBER);
    173 
    174       if (number1Bytes == null || number2Bytes == null) {
    175         // Empty numbers should not be combined.
    176         return false;
    177       }
    178 
    179       number1 = DialerPhoneNumber.parseFrom(number1Bytes);
    180       number2 = DialerPhoneNumber.parseFrom(number2Bytes);
    181     } catch (InvalidProtocolBufferException e) {
    182       throw Assert.createAssertionFailException("error parsing DialerPhoneNumber proto", e);
    183     }
    184     return dialerPhoneNumberUtil.isMatch(number1, number2);
    185   }
    186 
    187   /**
    188    * Returns a boolean indicating whether or not FEATURES_ASSISTED_DIALING is mutually exclusive
    189    * between two rows.
    190    */
    191   private static boolean meetsAssistedDialingCriteria(ContentValues row1, ContentValues row2) {
    192     int row1Assisted =
    193         row1.getAsInteger(AnnotatedCallLog.FEATURES)
    194             & TelephonyManagerCompat.FEATURES_ASSISTED_DIALING;
    195     int row2Assisted =
    196         row2.getAsInteger(AnnotatedCallLog.FEATURES)
    197             & TelephonyManagerCompat.FEATURES_ASSISTED_DIALING;
    198 
    199     // FEATURES_ASSISTED_DIALING should not be combined with calls that are
    200     // !FEATURES_ASSISTED_DIALING
    201     return row1Assisted == row2Assisted;
    202   }
    203 
    204   /**
    205    * Delegates to data sources to aggregate individual columns to create a new coalesced row.
    206    *
    207    * @param individualRows {@link AnnotatedCallLog} rows sorted by timestamp descending
    208    * @return a {@link CoalescedAnnotatedCallLog} row
    209    */
    210   private ContentValues coalesceRowsForAllDataSources(List<ContentValues> individualRows) {
    211     ContentValues coalescedValues = new ContentValues();
    212     for (CallLogDataSource dataSource : dataSources.getDataSourcesIncludingSystemCallLog()) {
    213       coalescedValues.putAll(dataSource.coalesce(individualRows));
    214     }
    215     return coalescedValues;
    216   }
    217 
    218   /**
    219    * Build a {@link CoalescedIds} proto that contains IDs of the rows in {@link AnnotatedCallLog}
    220    * that are coalesced into one row in {@link CoalescedAnnotatedCallLog}.
    221    *
    222    * @param individualRows {@link AnnotatedCallLog} rows sorted by timestamp descending
    223    * @return A {@link CoalescedIds} proto containing IDs of {@code individualRows}.
    224    */
    225   private CoalescedIds getCoalescedIds(List<ContentValues> individualRows) {
    226     CoalescedIds.Builder coalescedIds = CoalescedIds.newBuilder();
    227 
    228     for (ContentValues row : individualRows) {
    229       coalescedIds.addCoalescedId(Preconditions.checkNotNull(row.getAsLong(AnnotatedCallLog._ID)));
    230     }
    231 
    232     return coalescedIds.build();
    233   }
    234 
    235   /**
    236    * @param contentValues a {@link CoalescedAnnotatedCallLog} row
    237    * @param matrixCursor represents {@link CoalescedAnnotatedCallLog}
    238    */
    239   private static void addContentValuesToMatrixCursor(
    240       ContentValues contentValues, MatrixCursor matrixCursor, int rowId) {
    241     MatrixCursor.RowBuilder rowBuilder = matrixCursor.newRow();
    242     rowBuilder.add(CoalescedAnnotatedCallLog._ID, rowId);
    243     for (Map.Entry<String, Object> entry : contentValues.valueSet()) {
    244       rowBuilder.add(entry.getKey(), entry.getValue());
    245     }
    246   }
    247 }
    248