• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
Coalescer(DataSources dataSources)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
coalesce(@onNull Cursor allAnnotatedCallLogRowsSortedByTimestampDesc)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 
cursorRowToContentValues(Cursor cursor)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    */
rowsShouldBeCombined( DialerPhoneNumberUtil dialerPhoneNumberUtil, ContentValues row1, ContentValues row2)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    */
meetsAssistedDialingCriteria(ContentValues row1, ContentValues row2)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    */
coalesceRowsForAllDataSources(List<ContentValues> individualRows)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    */
getCoalescedIds(List<ContentValues> individualRows)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    */
addContentValuesToMatrixCursor( ContentValues contentValues, MatrixCursor matrixCursor, int rowId)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