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