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