1 /*
2  * Copyright (C) 2011 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 
17 package com.android.dialer.app.calllog;
18 
19 import android.content.Context;
20 import android.database.Cursor;
21 import android.provider.CallLog.Calls;
22 import android.support.annotation.Nullable;
23 import android.support.annotation.VisibleForTesting;
24 import android.telephony.PhoneNumberUtils;
25 import android.text.TextUtils;
26 import com.android.contacts.common.util.DateUtils;
27 import com.android.dialer.calllogutils.CallbackActionHelper;
28 import com.android.dialer.calllogutils.CallbackActionHelper.CallbackAction;
29 import com.android.dialer.compat.telephony.TelephonyManagerCompat;
30 import com.android.dialer.inject.ApplicationContext;
31 import com.android.dialer.phonenumbercache.CallLogQuery;
32 import com.android.dialer.phonenumberutil.PhoneNumberHelper;
33 
34 import java.time.ZoneId;
35 import java.util.Objects;
36 
37 /**
38  * Groups together calls in the call log. The primary grouping attempts to group together calls to
39  * and from the same number into a single row on the call log. A secondary grouping assigns calls,
40  * grouped via the primary grouping, to "day groups". The day groups provide a means of identifying
41  * the calls which occurred "Today", "Yesterday", "Last week", or "Other".
42  *
43  * <p>This class is meant to be used in conjunction with {@link GroupingListAdapter}.
44  */
45 public class CallLogGroupBuilder {
46 
47   /**
48    * Day grouping for call log entries used to represent no associated day group. Used primarily
49    * when retrieving the previous day group, but there is no previous day group (i.e. we are at the
50    * start of the list).
51    */
52   public static final int DAY_GROUP_NONE = -1;
53   /** Day grouping for calls which occurred today. */
54   public static final int DAY_GROUP_TODAY = 0;
55   /** Day grouping for calls which occurred yesterday. */
56   public static final int DAY_GROUP_YESTERDAY = 1;
57   /** Day grouping for calls which occurred before last week. */
58   public static final int DAY_GROUP_OTHER = 2;
59   /** Instance of the time object used for time calculations. */
60   private static final ZoneId TIME_ZONE = ZoneId.systemDefault();
61 
62   private final Context appContext;
63   /** The object on which the groups are created. */
64   private final GroupCreator groupCreator;
65 
CallLogGroupBuilder(@pplicationContext Context appContext, GroupCreator groupCreator)66   public CallLogGroupBuilder(@ApplicationContext Context appContext, GroupCreator groupCreator) {
67     this.appContext = appContext;
68     this.groupCreator = groupCreator;
69   }
70 
71   /**
72    * Finds all groups of adjacent entries in the call log which should be grouped together and calls
73    * {@link GroupCreator#addGroup(int, int)} on {@link #groupCreator} for each of them.
74    *
75    * <p>For entries that are not grouped with others, we do not need to create a group of size one.
76    *
77    * <p>It assumes that the cursor will not change during its execution.
78    *
79    * @see GroupingListAdapter#addGroups(Cursor)
80    */
addGroups(Cursor cursor)81   public void addGroups(Cursor cursor) {
82     final int count = cursor.getCount();
83     if (count == 0) {
84       return;
85     }
86 
87     // Clear any previous day grouping information.
88     groupCreator.clearDayGroups();
89 
90     // Get current system time, used for calculating which day group calls belong to.
91     long currentTime = System.currentTimeMillis();
92     cursor.moveToFirst();
93 
94     // Determine the day group for the first call in the cursor.
95     final long firstDate = cursor.getLong(CallLogQuery.DATE);
96     final long firstRowId = cursor.getLong(CallLogQuery.ID);
97     int groupDayGroup = getDayGroup(firstDate, currentTime);
98     groupCreator.setDayGroup(firstRowId, groupDayGroup);
99 
100     // Determine the callback action for the first call in the cursor.
101     String groupNumber = cursor.getString(CallLogQuery.NUMBER);
102     String groupAccountComponentName = cursor.getString(CallLogQuery.ACCOUNT_COMPONENT_NAME);
103     int groupFeatures = cursor.getInt(CallLogQuery.FEATURES);
104     int groupCallbackAction =
105         CallbackActionHelper.getCallbackAction(
106             appContext, groupNumber, groupFeatures, groupAccountComponentName);
107     groupCreator.setCallbackAction(firstRowId, groupCallbackAction);
108 
109     // Instantiate other group values to those of the first call in the cursor.
110     String groupAccountId = cursor.getString(CallLogQuery.ACCOUNT_ID);
111     String groupPostDialDigits = cursor.getString(CallLogQuery.POST_DIAL_DIGITS);
112     String groupViaNumbers = cursor.getString(CallLogQuery.VIA_NUMBER);
113     int groupCallType = cursor.getInt(CallLogQuery.CALL_TYPE);
114     int groupSize = 1;
115 
116     String number;
117     String numberPostDialDigits;
118     String numberViaNumbers;
119     int callType;
120     int callFeatures;
121     String accountComponentName;
122     String accountId;
123     int callbackAction;
124 
125     while (cursor.moveToNext()) {
126       // Obtain the values for the current call to group.
127       number = cursor.getString(CallLogQuery.NUMBER);
128       numberPostDialDigits = cursor.getString(CallLogQuery.POST_DIAL_DIGITS);
129       numberViaNumbers = cursor.getString(CallLogQuery.VIA_NUMBER);
130       callType = cursor.getInt(CallLogQuery.CALL_TYPE);
131       callFeatures = cursor.getInt(CallLogQuery.FEATURES);
132       accountComponentName = cursor.getString(CallLogQuery.ACCOUNT_COMPONENT_NAME);
133       accountId = cursor.getString(CallLogQuery.ACCOUNT_ID);
134       callbackAction =
135           CallbackActionHelper.getCallbackAction(
136               appContext, number, callFeatures, accountComponentName);
137 
138       final boolean isSameNumber = equalNumbers(groupNumber, number);
139       final boolean isSamePostDialDigits = groupPostDialDigits.equals(numberPostDialDigits);
140       final boolean isSameViaNumbers = groupViaNumbers.equals(numberViaNumbers);
141       final boolean isSameAccount =
142           isSameAccount(groupAccountComponentName, accountComponentName, groupAccountId, accountId);
143       final boolean isSameCallbackAction = (groupCallbackAction == callbackAction);
144 
145       // Group calls with the following criteria:
146       // (1) Calls with the same number, account, and callback action should be in the same group;
147       // (2) Never group voice mails; and
148       // (3) Only group blocked calls with other blocked calls.
149       // (4) Only group calls that were assisted dialed with other calls that were assisted dialed.
150       if (isSameNumber
151           && isSameAccount
152           && isSamePostDialDigits
153           && isSameViaNumbers
154           && isSameCallbackAction
155           && areBothNotVoicemail(callType, groupCallType)
156           && (areBothNotBlocked(callType, groupCallType) || areBothBlocked(callType, groupCallType))
157           && meetsAssistedDialingGroupingCriteria(groupFeatures, callFeatures)) {
158         // Increment the size of the group to include the current call, but do not create
159         // the group until finding a call that does not match.
160         groupSize++;
161       } else {
162         // The call group has changed. Determine the day group for the new call group.
163         final long date = cursor.getLong(CallLogQuery.DATE);
164         groupDayGroup = getDayGroup(date, currentTime);
165 
166         // Create a group for the previous group of calls, which does not include the
167         // current call.
168         groupCreator.addGroup(cursor.getPosition() - groupSize, groupSize);
169 
170         // Start a new group; it will include at least the current call.
171         groupSize = 1;
172 
173         // Update the group values to those of the current call.
174         groupNumber = number;
175         groupPostDialDigits = numberPostDialDigits;
176         groupViaNumbers = numberViaNumbers;
177         groupCallType = callType;
178         groupAccountComponentName = accountComponentName;
179         groupAccountId = accountId;
180         groupCallbackAction = callbackAction;
181         groupFeatures = callFeatures;
182       }
183 
184       // Save the callback action and the day group associated with the current call.
185       final long currentCallId = cursor.getLong(CallLogQuery.ID);
186       groupCreator.setCallbackAction(currentCallId, groupCallbackAction);
187       groupCreator.setDayGroup(currentCallId, groupDayGroup);
188     }
189 
190     // Create a group for the last set of calls.
191     groupCreator.addGroup(count - groupSize, groupSize);
192   }
193 
194   /**
195    * Returns true when the two input numbers can be considered identical enough for caller ID
196    * purposes and put in a call log group.
197    */
198   @VisibleForTesting
equalNumbers(@ullable String number1, @Nullable String number2)199   boolean equalNumbers(@Nullable String number1, @Nullable String number2) {
200     if (PhoneNumberHelper.isUriNumber(number1) || PhoneNumberHelper.isUriNumber(number2)) {
201       return compareSipAddresses(number1, number2);
202     }
203 
204     // PhoneNumberUtils.compare(String, String) ignores special characters such as '#'. For example,
205     // it thinks "123" and "#123" are identical enough for caller ID purposes.
206     // When either input number contains special characters, we put the two in the same group iff
207     // their raw numbers are exactly the same.
208     if (PhoneNumberHelper.numberHasSpecialChars(number1)
209         || PhoneNumberHelper.numberHasSpecialChars(number2)) {
210       return PhoneNumberHelper.sameRawNumbers(number1, number2);
211     }
212 
213     return PhoneNumberUtils.compare(number1, number2);
214   }
215 
isSameAccount(String name1, String name2, String id1, String id2)216   private boolean isSameAccount(String name1, String name2, String id1, String id2) {
217     return TextUtils.equals(name1, name2) && TextUtils.equals(id1, id2);
218   }
219 
220   @VisibleForTesting
compareSipAddresses(@ullable String number1, @Nullable String number2)221   boolean compareSipAddresses(@Nullable String number1, @Nullable String number2) {
222     if (number1 == null || number2 == null) {
223       return Objects.equals(number1, number2);
224     }
225 
226     int index1 = number1.indexOf('@');
227     final String userinfo1;
228     final String rest1;
229     if (index1 != -1) {
230       userinfo1 = number1.substring(0, index1);
231       rest1 = number1.substring(index1);
232     } else {
233       userinfo1 = number1;
234       rest1 = "";
235     }
236 
237     int index2 = number2.indexOf('@');
238     final String userinfo2;
239     final String rest2;
240     if (index2 != -1) {
241       userinfo2 = number2.substring(0, index2);
242       rest2 = number2.substring(index2);
243     } else {
244       userinfo2 = number2;
245       rest2 = "";
246     }
247 
248     return userinfo1.equals(userinfo2) && rest1.equalsIgnoreCase(rest2);
249   }
250 
251   /**
252    * Given a call date and the current date, determine which date group the call belongs in.
253    *
254    * @param date The call date.
255    * @param now The current date.
256    * @return The date group the call belongs in.
257    */
getDayGroup(long date, long now)258   private int getDayGroup(long date, long now) {
259     int days = DateUtils.getDayDifference(TIME_ZONE, date, now);
260 
261     if (days == 0) {
262       return DAY_GROUP_TODAY;
263     } else if (days == 1) {
264       return DAY_GROUP_YESTERDAY;
265     } else {
266       return DAY_GROUP_OTHER;
267     }
268   }
269 
areBothNotVoicemail(int callType, int groupCallType)270   private boolean areBothNotVoicemail(int callType, int groupCallType) {
271     return callType != Calls.VOICEMAIL_TYPE && groupCallType != Calls.VOICEMAIL_TYPE;
272   }
273 
areBothNotBlocked(int callType, int groupCallType)274   private boolean areBothNotBlocked(int callType, int groupCallType) {
275     return callType != Calls.BLOCKED_TYPE && groupCallType != Calls.BLOCKED_TYPE;
276   }
277 
areBothBlocked(int callType, int groupCallType)278   private boolean areBothBlocked(int callType, int groupCallType) {
279     return callType == Calls.BLOCKED_TYPE && groupCallType == Calls.BLOCKED_TYPE;
280   }
281 
meetsAssistedDialingGroupingCriteria(int groupFeatures, int callFeatures)282   private boolean meetsAssistedDialingGroupingCriteria(int groupFeatures, int callFeatures) {
283     int groupAssisted = (groupFeatures & TelephonyManagerCompat.FEATURES_ASSISTED_DIALING);
284     int callAssisted = (callFeatures & TelephonyManagerCompat.FEATURES_ASSISTED_DIALING);
285 
286     return groupAssisted == callAssisted;
287   }
288 
289   public interface GroupCreator {
290 
291     /**
292      * Defines the interface for adding a group to the call log. The primary group for a call log
293      * groups the calls together based on the number which was dialed.
294      *
295      * @param cursorPosition The starting position of the group in the cursor.
296      * @param size The size of the group.
297      */
addGroup(int cursorPosition, int size)298     void addGroup(int cursorPosition, int size);
299 
300     /**
301      * Defines the interface for tracking each call's callback action. Calls in a call group are
302      * associated with the same callback action as the first call in the group. The value of a
303      * callback action should be one of the categories in {@link CallbackAction}.
304      *
305      * @param rowId The row ID of the current call.
306      * @param callbackAction The current call's callback action.
307      */
setCallbackAction(long rowId, @CallbackAction int callbackAction)308     void setCallbackAction(long rowId, @CallbackAction int callbackAction);
309 
310     /**
311      * Defines the interface for tracking the day group each call belongs to. Calls in a call group
312      * are assigned the same day group as the first call in the group. The day group assigns calls
313      * to the buckets: Today, Yesterday, Last week, and Other
314      *
315      * @param rowId The row ID of the current call.
316      * @param dayGroup The day group the call belongs to.
317      */
setDayGroup(long rowId, int dayGroup)318     void setDayGroup(long rowId, int dayGroup);
319 
320     /** Defines the interface for clearing the day groupings information on rebind/regroup. */
clearDayGroups()321     void clearDayGroups();
322   }
323 }
324