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