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 17 package com.android.dialer.calllog.datasources.systemcalllog; 18 19 import android.Manifest.permission; 20 import android.annotation.TargetApi; 21 import android.content.ContentValues; 22 import android.content.Context; 23 import android.database.ContentObserver; 24 import android.database.Cursor; 25 import android.net.Uri; 26 import android.os.Build; 27 import android.os.Handler; 28 import android.preference.PreferenceManager; 29 import android.provider.CallLog; 30 import android.provider.CallLog.Calls; 31 import android.support.annotation.MainThread; 32 import android.support.annotation.Nullable; 33 import android.support.annotation.VisibleForTesting; 34 import android.support.annotation.WorkerThread; 35 import android.text.TextUtils; 36 import android.util.ArraySet; 37 import com.android.dialer.DialerPhoneNumber; 38 import com.android.dialer.calllog.database.contract.AnnotatedCallLogContract.AnnotatedCallLog; 39 import com.android.dialer.calllog.database.contract.AnnotatedCallLogContract.CoalescedAnnotatedCallLog; 40 import com.android.dialer.calllog.datasources.CallLogDataSource; 41 import com.android.dialer.calllog.datasources.CallLogMutations; 42 import com.android.dialer.calllog.datasources.util.RowCombiner; 43 import com.android.dialer.common.Assert; 44 import com.android.dialer.common.LogUtil; 45 import com.android.dialer.common.concurrent.ThreadUtil; 46 import com.android.dialer.phonenumberproto.DialerPhoneNumberUtil; 47 import com.android.dialer.util.PermissionsUtil; 48 import com.google.i18n.phonenumbers.PhoneNumberUtil; 49 import com.google.protobuf.InvalidProtocolBufferException; 50 import java.util.Arrays; 51 import java.util.List; 52 import java.util.Set; 53 import javax.inject.Inject; 54 55 /** 56 * Responsible for defining the rows in the annotated call log and maintaining the columns in it 57 * which are derived from the system call log. 58 */ 59 @SuppressWarnings("MissingPermission") 60 public class SystemCallLogDataSource implements CallLogDataSource { 61 62 @VisibleForTesting 63 static final String PREF_LAST_TIMESTAMP_PROCESSED = "systemCallLogLastTimestampProcessed"; 64 65 @Nullable private Long lastTimestampProcessed; 66 67 @Inject 68 public SystemCallLogDataSource() {} 69 70 @MainThread 71 @Override 72 public void registerContentObservers( 73 Context appContext, ContentObserverCallbacks contentObserverCallbacks) { 74 Assert.isMainThread(); 75 76 LogUtil.enterBlock("SystemCallLogDataSource.registerContentObservers"); 77 78 if (!PermissionsUtil.hasCallLogReadPermissions(appContext)) { 79 LogUtil.i("SystemCallLogDataSource.registerContentObservers", "no call log permissions"); 80 return; 81 } 82 83 appContext 84 .getContentResolver() 85 .registerContentObserver( 86 CallLog.Calls.CONTENT_URI, 87 true, 88 new CallLogObserver( 89 ThreadUtil.getUiThreadHandler(), appContext, contentObserverCallbacks)); 90 } 91 92 @WorkerThread 93 @Override 94 public boolean isDirty(Context appContext) { 95 Assert.isWorkerThread(); 96 97 /* 98 * The system call log has a last updated timestamp, but deletes are physical (the "deleted" 99 * column is unused). This means that we can't detect deletes without scanning the entire table, 100 * which would be too slow. So, we just rely on content observers to trigger rebuilds when any 101 * change is made to the system call log. 102 */ 103 return false; 104 } 105 106 @WorkerThread 107 @Override 108 public void fill(Context appContext, CallLogMutations mutations) { 109 Assert.isWorkerThread(); 110 111 lastTimestampProcessed = null; 112 113 if (!PermissionsUtil.hasPermission(appContext, permission.READ_CALL_LOG)) { 114 LogUtil.i("SystemCallLogDataSource.fill", "no call log permissions"); 115 return; 116 } 117 118 // This data source should always run first so the mutations should always be empty. 119 Assert.checkArgument(mutations.isEmpty()); 120 121 Set<Long> annotatedCallLogIds = getAnnotatedCallLogIds(appContext); 122 123 LogUtil.i( 124 "SystemCallLogDataSource.fill", 125 "found %d existing annotated call log ids", 126 annotatedCallLogIds.size()); 127 128 handleInsertsAndUpdates(appContext, mutations, annotatedCallLogIds); 129 handleDeletes(appContext, annotatedCallLogIds, mutations); 130 } 131 132 @WorkerThread 133 @Override 134 public void onSuccessfulFill(Context appContext) { 135 // If a fill operation was a no-op, lastTimestampProcessed could still be null. 136 if (lastTimestampProcessed != null) { 137 PreferenceManager.getDefaultSharedPreferences(appContext) 138 .edit() 139 .putLong(PREF_LAST_TIMESTAMP_PROCESSED, lastTimestampProcessed) 140 .apply(); 141 } 142 } 143 144 @Override 145 public ContentValues coalesce(List<ContentValues> individualRowsSortedByTimestampDesc) { 146 // TODO: Complete implementation. 147 ContentValues coalescedValues = 148 new RowCombiner(individualRowsSortedByTimestampDesc) 149 .useMostRecentLong(AnnotatedCallLog.TIMESTAMP) 150 .combine(); 151 152 // All phone numbers in the provided group should be equivalent (but could be formatted 153 // differently). Arbitrarily show the raw phone number of the most recent call. 154 DialerPhoneNumber mostRecentPhoneNumber = 155 getMostRecentPhoneNumber(individualRowsSortedByTimestampDesc); 156 coalescedValues.put( 157 CoalescedAnnotatedCallLog.FORMATTED_NUMBER, 158 mostRecentPhoneNumber.getRawInput().getNumber()); 159 return coalescedValues; 160 } 161 162 private static DialerPhoneNumber getMostRecentPhoneNumber( 163 List<ContentValues> individualRowsSortedByTimestampDesc) { 164 DialerPhoneNumber dialerPhoneNumber; 165 byte[] protoBytes = 166 individualRowsSortedByTimestampDesc.get(0).getAsByteArray(AnnotatedCallLog.NUMBER); 167 try { 168 dialerPhoneNumber = DialerPhoneNumber.parseFrom(protoBytes); 169 } catch (InvalidProtocolBufferException e) { 170 throw Assert.createAssertionFailException("couldn't parse DialerPhoneNumber", e); 171 } 172 return dialerPhoneNumber; 173 } 174 175 @TargetApi(Build.VERSION_CODES.M) // Uses try-with-resources 176 private void handleInsertsAndUpdates( 177 Context appContext, CallLogMutations mutations, Set<Long> existingAnnotatedCallLogIds) { 178 long previousTimestampProcessed = 179 PreferenceManager.getDefaultSharedPreferences(appContext) 180 .getLong(PREF_LAST_TIMESTAMP_PROCESSED, 0L); 181 182 DialerPhoneNumberUtil dialerPhoneNumberUtil = 183 new DialerPhoneNumberUtil(PhoneNumberUtil.getInstance()); 184 185 // TODO: Really should be getting last 1000 by timestamp, not by last modified. 186 try (Cursor cursor = 187 appContext 188 .getContentResolver() 189 .query( 190 Calls.CONTENT_URI, // Excludes voicemail 191 new String[] { 192 Calls._ID, Calls.DATE, Calls.LAST_MODIFIED, Calls.NUMBER, Calls.COUNTRY_ISO 193 }, 194 Calls.LAST_MODIFIED + " > ?", 195 new String[] {String.valueOf(previousTimestampProcessed)}, 196 Calls.LAST_MODIFIED + " DESC LIMIT 1000")) { 197 198 if (cursor == null) { 199 LogUtil.e("SystemCallLogDataSource.handleInsertsAndUpdates", "null cursor"); 200 return; 201 } 202 203 LogUtil.i( 204 "SystemCallLogDataSource.handleInsertsAndUpdates", 205 "found %d entries to insert/update", 206 cursor.getCount()); 207 208 if (cursor.moveToFirst()) { 209 int idColumn = cursor.getColumnIndexOrThrow(Calls._ID); 210 int dateColumn = cursor.getColumnIndexOrThrow(Calls.DATE); 211 int lastModifiedColumn = cursor.getColumnIndexOrThrow(Calls.LAST_MODIFIED); 212 int numberColumn = cursor.getColumnIndexOrThrow(Calls.NUMBER); 213 int countryIsoColumn = cursor.getColumnIndexOrThrow(Calls.COUNTRY_ISO); 214 215 // The cursor orders by LAST_MODIFIED DESC, so the first result is the most recent timestamp 216 // processed. 217 lastTimestampProcessed = cursor.getLong(lastModifiedColumn); 218 do { 219 long id = cursor.getLong(idColumn); 220 long date = cursor.getLong(dateColumn); 221 String numberAsStr = cursor.getString(numberColumn); 222 String countryIso = cursor.getString(countryIsoColumn); 223 224 byte[] numberAsProtoBytes = 225 dialerPhoneNumberUtil.parse(numberAsStr, countryIso).toByteArray(); 226 227 ContentValues contentValues = new ContentValues(); 228 contentValues.put(AnnotatedCallLog.TIMESTAMP, date); 229 contentValues.put(AnnotatedCallLog.NUMBER, numberAsProtoBytes); 230 231 if (existingAnnotatedCallLogIds.contains(id)) { 232 mutations.update(id, contentValues); 233 } else { 234 mutations.insert(id, contentValues); 235 } 236 } while (cursor.moveToNext()); 237 } // else no new results, do nothing. 238 } 239 } 240 241 private static void handleDeletes( 242 Context appContext, Set<Long> existingAnnotatedCallLogIds, CallLogMutations mutations) { 243 Set<Long> systemCallLogIds = 244 getIdsFromSystemCallLogThatMatch(appContext, existingAnnotatedCallLogIds); 245 LogUtil.i( 246 "SystemCallLogDataSource.handleDeletes", 247 "found %d matching entries in system call log", 248 systemCallLogIds.size()); 249 Set<Long> idsInAnnotatedCallLogNoLongerInSystemCallLog = new ArraySet<>(); 250 idsInAnnotatedCallLogNoLongerInSystemCallLog.addAll(existingAnnotatedCallLogIds); 251 idsInAnnotatedCallLogNoLongerInSystemCallLog.removeAll(systemCallLogIds); 252 253 LogUtil.i( 254 "SystemCallLogDataSource.handleDeletes", 255 "found %d call log entries to remove", 256 idsInAnnotatedCallLogNoLongerInSystemCallLog.size()); 257 258 for (long id : idsInAnnotatedCallLogNoLongerInSystemCallLog) { 259 mutations.delete(id); 260 } 261 } 262 263 @TargetApi(Build.VERSION_CODES.M) // Uses try-with-resources 264 private static Set<Long> getAnnotatedCallLogIds(Context appContext) { 265 ArraySet<Long> ids = new ArraySet<>(); 266 267 try (Cursor cursor = 268 appContext 269 .getContentResolver() 270 .query( 271 AnnotatedCallLog.CONTENT_URI, 272 new String[] {AnnotatedCallLog._ID}, 273 null, 274 null, 275 null)) { 276 277 if (cursor == null) { 278 LogUtil.e("SystemCallLogDataSource.getAnnotatedCallLogIds", "null cursor"); 279 return ids; 280 } 281 282 if (cursor.moveToFirst()) { 283 int idColumn = cursor.getColumnIndexOrThrow(AnnotatedCallLog._ID); 284 do { 285 ids.add(cursor.getLong(idColumn)); 286 } while (cursor.moveToNext()); 287 } 288 } 289 return ids; 290 } 291 292 @TargetApi(Build.VERSION_CODES.M) // Uses try-with-resources 293 private static Set<Long> getIdsFromSystemCallLogThatMatch( 294 Context appContext, Set<Long> matchingIds) { 295 ArraySet<Long> ids = new ArraySet<>(); 296 297 String[] questionMarks = new String[matchingIds.size()]; 298 Arrays.fill(questionMarks, "?"); 299 String whereClause = (Calls._ID + " in (") + TextUtils.join(",", questionMarks) + ")"; 300 String[] whereArgs = new String[matchingIds.size()]; 301 int i = 0; 302 for (long id : matchingIds) { 303 whereArgs[i++] = String.valueOf(id); 304 } 305 306 try (Cursor cursor = 307 appContext 308 .getContentResolver() 309 .query(Calls.CONTENT_URI, new String[] {Calls._ID}, whereClause, whereArgs, null)) { 310 311 if (cursor == null) { 312 LogUtil.e("SystemCallLogDataSource.getIdsFromSystemCallLog", "null cursor"); 313 return ids; 314 } 315 316 if (cursor.moveToFirst()) { 317 int idColumn = cursor.getColumnIndexOrThrow(Calls._ID); 318 do { 319 ids.add(cursor.getLong(idColumn)); 320 } while (cursor.moveToNext()); 321 } 322 return ids; 323 } 324 } 325 326 private static class CallLogObserver extends ContentObserver { 327 private final Context appContext; 328 private final ContentObserverCallbacks contentObserverCallbacks; 329 330 CallLogObserver( 331 Handler handler, Context appContext, ContentObserverCallbacks contentObserverCallbacks) { 332 super(handler); 333 this.appContext = appContext; 334 this.contentObserverCallbacks = contentObserverCallbacks; 335 } 336 337 @MainThread 338 @Override 339 public void onChange(boolean selfChange, Uri uri) { 340 Assert.isMainThread(); 341 LogUtil.enterBlock("SystemCallLogDataSource.CallLogObserver.onChange"); 342 super.onChange(selfChange, uri); 343 344 /* 345 * The system call log has a last updated timestamp, but deletes are physical (the "deleted" 346 * column is unused). This means that we can't detect deletes without scanning the entire 347 * table, which would be too slow. So, we just rely on content observers to trigger rebuilds 348 * when any change is made to the system call log. 349 */ 350 contentObserverCallbacks.markDirtyAndNotify(appContext); 351 } 352 } 353 } 354