1 /* 2 * Copyright (C) 2015 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.calllogbackup; 18 19 import static android.provider.CallLog.Calls.MISSED_REASON_NOT_MISSED; 20 21 import android.app.backup.BackupAgent; 22 import android.app.backup.BackupDataInput; 23 import android.app.backup.BackupDataOutput; 24 import android.content.ComponentName; 25 import android.content.ContentResolver; 26 import android.content.Context; 27 import android.database.Cursor; 28 import android.os.ParcelFileDescriptor; 29 import android.provider.CallLog; 30 import android.provider.CallLog.Calls; 31 import android.provider.Settings; 32 import android.telecom.PhoneAccountHandle; 33 import android.util.Log; 34 35 import com.android.internal.annotations.VisibleForTesting; 36 37 import java.io.BufferedOutputStream; 38 import java.io.ByteArrayInputStream; 39 import java.io.ByteArrayOutputStream; 40 import java.io.DataInput; 41 import java.io.DataInputStream; 42 import java.io.DataOutput; 43 import java.io.DataOutputStream; 44 import java.io.EOFException; 45 import java.io.FileInputStream; 46 import java.io.FileOutputStream; 47 import java.io.IOException; 48 import java.util.LinkedList; 49 import java.util.List; 50 import java.util.SortedSet; 51 import java.util.TreeSet; 52 53 /** 54 * Call log backup agent. 55 */ 56 public class CallLogBackupAgent extends BackupAgent { 57 58 @VisibleForTesting 59 static class CallLogBackupState { 60 int version; 61 SortedSet<Integer> callIds; 62 } 63 64 @VisibleForTesting 65 static class Call { 66 int id; 67 long date; 68 long duration; 69 String number; 70 String postDialDigits = ""; 71 String viaNumber = ""; 72 int type; 73 int numberPresentation; 74 String accountComponentName; 75 String accountId; 76 String accountAddress; 77 Long dataUsage; 78 int features; 79 int addForAllUsers = 1; 80 int callBlockReason = Calls.BLOCK_REASON_NOT_BLOCKED; 81 String callScreeningAppName = null; 82 String callScreeningComponentName = null; 83 long missedReason = MISSED_REASON_NOT_MISSED; 84 85 @Override toString()86 public String toString() { 87 if (isDebug()) { 88 return "[" + id + ", account: [" + accountComponentName + " : " + accountId + 89 "]," + number + ", " + date + "]"; 90 } else { 91 return "[" + id + "]"; 92 } 93 } 94 } 95 96 static class OEMData { 97 String namespace; 98 byte[] bytes; 99 OEMData(String namespace, byte[] bytes)100 public OEMData(String namespace, byte[] bytes) { 101 this.namespace = namespace; 102 this.bytes = bytes == null ? ZERO_BYTE_ARRAY : bytes; 103 } 104 } 105 106 private static final String TAG = "CallLogBackupAgent"; 107 108 /** Current version of CallLogBackup. Used to track the backup format. */ 109 @VisibleForTesting 110 static final int VERSION = 1008; 111 /** Version indicating that there exists no previous backup entry. */ 112 @VisibleForTesting 113 static final int VERSION_NO_PREVIOUS_STATE = 0; 114 115 static final String NO_OEM_NAMESPACE = "no-oem-namespace"; 116 117 static final byte[] ZERO_BYTE_ARRAY = new byte[0]; 118 119 static final int END_OEM_DATA_MARKER = 0x60061E; 120 121 122 private static final String[] CALL_LOG_PROJECTION = new String[] { 123 CallLog.Calls._ID, 124 CallLog.Calls.DATE, 125 CallLog.Calls.DURATION, 126 CallLog.Calls.NUMBER, 127 CallLog.Calls.POST_DIAL_DIGITS, 128 CallLog.Calls.VIA_NUMBER, 129 CallLog.Calls.TYPE, 130 CallLog.Calls.COUNTRY_ISO, 131 CallLog.Calls.GEOCODED_LOCATION, 132 CallLog.Calls.NUMBER_PRESENTATION, 133 CallLog.Calls.PHONE_ACCOUNT_COMPONENT_NAME, 134 CallLog.Calls.PHONE_ACCOUNT_ID, 135 CallLog.Calls.PHONE_ACCOUNT_ADDRESS, 136 CallLog.Calls.DATA_USAGE, 137 CallLog.Calls.FEATURES, 138 CallLog.Calls.ADD_FOR_ALL_USERS, 139 CallLog.Calls.BLOCK_REASON, 140 CallLog.Calls.CALL_SCREENING_APP_NAME, 141 CallLog.Calls.CALL_SCREENING_COMPONENT_NAME, 142 CallLog.Calls.MISSED_REASON 143 }; 144 145 /** ${inheritDoc} */ 146 @Override onBackup(ParcelFileDescriptor oldStateDescriptor, BackupDataOutput data, ParcelFileDescriptor newStateDescriptor)147 public void onBackup(ParcelFileDescriptor oldStateDescriptor, BackupDataOutput data, 148 ParcelFileDescriptor newStateDescriptor) throws IOException { 149 // Get the list of the previous calls IDs which were backed up. 150 DataInputStream dataInput = new DataInputStream( 151 new FileInputStream(oldStateDescriptor.getFileDescriptor())); 152 final CallLogBackupState state; 153 try { 154 state = readState(dataInput); 155 } finally { 156 dataInput.close(); 157 } 158 159 // Run the actual backup of data 160 runBackup(state, data, getAllCallLogEntries()); 161 162 // Rewrite the backup state. 163 DataOutputStream dataOutput = new DataOutputStream(new BufferedOutputStream( 164 new FileOutputStream(newStateDescriptor.getFileDescriptor()))); 165 try { 166 writeState(dataOutput, state); 167 } finally { 168 dataOutput.close(); 169 } 170 } 171 172 /** ${inheritDoc} */ 173 @Override onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState)174 public void onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState) 175 throws IOException { 176 177 if (isDebug()) { 178 Log.d(TAG, "Performing Restore"); 179 } 180 181 while (data.readNextHeader()) { 182 Call call = readCallFromData(data); 183 if (call != null && call.type != Calls.VOICEMAIL_TYPE) { 184 writeCallToProvider(call); 185 if (isDebug()) { 186 Log.d(TAG, "Restored call: " + call); 187 } 188 } 189 } 190 } 191 192 @VisibleForTesting runBackup(CallLogBackupState state, BackupDataOutput data, Iterable<Call> calls)193 void runBackup(CallLogBackupState state, BackupDataOutput data, Iterable<Call> calls) { 194 SortedSet<Integer> callsToRemove = new TreeSet<>(state.callIds); 195 196 // Loop through all the call log entries to identify: 197 // (1) new calls 198 // (2) calls which have been deleted. 199 for (Call call : calls) { 200 if (!state.callIds.contains(call.id)) { 201 202 if (isDebug()) { 203 Log.d(TAG, "Adding call to backup: " + call); 204 } 205 206 // This call new (not in our list from the last backup), lets back it up. 207 addCallToBackup(data, call); 208 state.callIds.add(call.id); 209 } else { 210 // This call still exists in the current call log so delete it from the 211 // "callsToRemove" set since we want to keep it. 212 callsToRemove.remove(call.id); 213 } 214 } 215 216 // Remove calls which no longer exist in the set. 217 for (Integer i : callsToRemove) { 218 if (isDebug()) { 219 Log.d(TAG, "Removing call from backup: " + i); 220 } 221 222 removeCallFromBackup(data, i); 223 state.callIds.remove(i); 224 } 225 } 226 getAllCallLogEntries()227 private Iterable<Call> getAllCallLogEntries() { 228 List<Call> calls = new LinkedList<>(); 229 230 // We use the API here instead of querying ContactsDatabaseHelper directly because 231 // CallLogProvider has special locks in place for sychronizing when to read. Using the APIs 232 // gives us that for free. 233 ContentResolver resolver = getContentResolver(); 234 Cursor cursor = resolver.query( 235 CallLog.Calls.CONTENT_URI, CALL_LOG_PROJECTION, null, null, null); 236 if (cursor != null) { 237 try { 238 while (cursor.moveToNext()) { 239 Call call = readCallFromCursor(cursor); 240 if (call != null && call.type != Calls.VOICEMAIL_TYPE) { 241 calls.add(call); 242 } 243 } 244 } finally { 245 cursor.close(); 246 } 247 } 248 249 return calls; 250 } 251 writeCallToProvider(Call call)252 private void writeCallToProvider(Call call) { 253 Long dataUsage = call.dataUsage == 0 ? null : call.dataUsage; 254 255 PhoneAccountHandle handle = null; 256 if (call.accountComponentName != null && call.accountId != null) { 257 handle = new PhoneAccountHandle( 258 ComponentName.unflattenFromString(call.accountComponentName), call.accountId); 259 } 260 boolean addForAllUsers = call.addForAllUsers == 1; 261 // We backup the calllog in the user running this backup agent, so write calls to this user. 262 Calls.addCall(null /* CallerInfo */, this, call.number, call.postDialDigits, call.viaNumber, 263 call.numberPresentation, call.type, call.features, handle, call.date, 264 (int) call.duration, dataUsage, addForAllUsers, null, true /* isRead */, 265 call.callBlockReason /*callBlockReason*/, 266 call.callScreeningAppName /*callScreeningAppName*/, 267 call.callScreeningComponentName /*callScreeningComponentName*/, 268 call.missedReason); 269 } 270 271 @VisibleForTesting readState(DataInput dataInput)272 CallLogBackupState readState(DataInput dataInput) throws IOException { 273 CallLogBackupState state = new CallLogBackupState(); 274 state.callIds = new TreeSet<>(); 275 276 try { 277 // Read the version. 278 state.version = dataInput.readInt(); 279 280 if (state.version >= 1) { 281 // Read the size. 282 int size = dataInput.readInt(); 283 284 // Read all of the call IDs. 285 for (int i = 0; i < size; i++) { 286 state.callIds.add(dataInput.readInt()); 287 } 288 } 289 } catch (EOFException e) { 290 state.version = VERSION_NO_PREVIOUS_STATE; 291 } 292 293 return state; 294 } 295 296 @VisibleForTesting writeState(DataOutput dataOutput, CallLogBackupState state)297 void writeState(DataOutput dataOutput, CallLogBackupState state) 298 throws IOException { 299 // Write version first of all 300 dataOutput.writeInt(VERSION); 301 302 // [Version 1] 303 // size + callIds 304 dataOutput.writeInt(state.callIds.size()); 305 for (Integer i : state.callIds) { 306 dataOutput.writeInt(i); 307 } 308 } 309 310 @VisibleForTesting readCallFromData(BackupDataInput data)311 Call readCallFromData(BackupDataInput data) { 312 final int callId; 313 try { 314 callId = Integer.parseInt(data.getKey()); 315 } catch (NumberFormatException e) { 316 Log.e(TAG, "Unexpected key found in restore: " + data.getKey()); 317 return null; 318 } 319 320 try { 321 byte [] byteArray = new byte[data.getDataSize()]; 322 data.readEntityData(byteArray, 0, byteArray.length); 323 DataInputStream dataInput = new DataInputStream(new ByteArrayInputStream(byteArray)); 324 325 Call call = new Call(); 326 call.id = callId; 327 328 int version = dataInput.readInt(); 329 if (version >= 1) { 330 call.date = dataInput.readLong(); 331 call.duration = dataInput.readLong(); 332 call.number = readString(dataInput); 333 call.type = dataInput.readInt(); 334 call.numberPresentation = dataInput.readInt(); 335 call.accountComponentName = readString(dataInput); 336 call.accountId = readString(dataInput); 337 call.accountAddress = readString(dataInput); 338 call.dataUsage = dataInput.readLong(); 339 call.features = dataInput.readInt(); 340 } 341 342 if (version >= 1002) { 343 String namespace = dataInput.readUTF(); 344 int length = dataInput.readInt(); 345 byte[] buffer = new byte[length]; 346 dataInput.read(buffer); 347 readOEMDataForCall(call, new OEMData(namespace, buffer)); 348 349 int marker = dataInput.readInt(); 350 if (marker != END_OEM_DATA_MARKER) { 351 Log.e(TAG, "Did not find END-OEM marker for call " + call.id); 352 // The marker does not match the expected value, ignore this call completely. 353 return null; 354 } 355 } 356 357 if (version >= 1003) { 358 call.addForAllUsers = dataInput.readInt(); 359 } 360 361 if (version >= 1004) { 362 call.postDialDigits = readString(dataInput); 363 } 364 365 if(version >= 1005) { 366 call.viaNumber = readString(dataInput); 367 } 368 369 if(version >= 1006) { 370 call.callBlockReason = dataInput.readInt(); 371 call.callScreeningAppName = readString(dataInput); 372 call.callScreeningComponentName = readString(dataInput); 373 } 374 if(version >= 1007) { 375 // Version 1007 had call id columns early in the Q release; they were pulled so we 376 // will just read the values out here if they exist in a backup and ignore them. 377 readString(dataInput); 378 readString(dataInput); 379 readString(dataInput); 380 readString(dataInput); 381 readString(dataInput); 382 readInteger(dataInput); 383 } 384 if (version >= 1008) { 385 call.missedReason = dataInput.readLong(); 386 } 387 return call; 388 } catch (IOException e) { 389 Log.e(TAG, "Error reading call data for " + callId, e); 390 return null; 391 } 392 } 393 readCallFromCursor(Cursor cursor)394 private Call readCallFromCursor(Cursor cursor) { 395 Call call = new Call(); 396 call.id = cursor.getInt(cursor.getColumnIndex(CallLog.Calls._ID)); 397 call.date = cursor.getLong(cursor.getColumnIndex(CallLog.Calls.DATE)); 398 call.duration = cursor.getLong(cursor.getColumnIndex(CallLog.Calls.DURATION)); 399 call.number = cursor.getString(cursor.getColumnIndex(CallLog.Calls.NUMBER)); 400 call.postDialDigits = cursor.getString( 401 cursor.getColumnIndex(CallLog.Calls.POST_DIAL_DIGITS)); 402 call.viaNumber = cursor.getString(cursor.getColumnIndex(CallLog.Calls.VIA_NUMBER)); 403 call.type = cursor.getInt(cursor.getColumnIndex(CallLog.Calls.TYPE)); 404 call.numberPresentation = 405 cursor.getInt(cursor.getColumnIndex(CallLog.Calls.NUMBER_PRESENTATION)); 406 call.accountComponentName = 407 cursor.getString(cursor.getColumnIndex(CallLog.Calls.PHONE_ACCOUNT_COMPONENT_NAME)); 408 call.accountId = 409 cursor.getString(cursor.getColumnIndex(CallLog.Calls.PHONE_ACCOUNT_ID)); 410 call.accountAddress = 411 cursor.getString(cursor.getColumnIndex(CallLog.Calls.PHONE_ACCOUNT_ADDRESS)); 412 call.dataUsage = cursor.getLong(cursor.getColumnIndex(CallLog.Calls.DATA_USAGE)); 413 call.features = cursor.getInt(cursor.getColumnIndex(CallLog.Calls.FEATURES)); 414 call.addForAllUsers = cursor.getInt(cursor.getColumnIndex(Calls.ADD_FOR_ALL_USERS)); 415 call.callBlockReason = cursor.getInt(cursor.getColumnIndex(CallLog.Calls.BLOCK_REASON)); 416 call.callScreeningAppName = cursor 417 .getString(cursor.getColumnIndex(CallLog.Calls.CALL_SCREENING_APP_NAME)); 418 call.callScreeningComponentName = cursor 419 .getString(cursor.getColumnIndex(CallLog.Calls.CALL_SCREENING_COMPONENT_NAME)); 420 call.missedReason = cursor 421 .getInt(cursor.getColumnIndex(CallLog.Calls.MISSED_REASON)); 422 return call; 423 } 424 addCallToBackup(BackupDataOutput output, Call call)425 private void addCallToBackup(BackupDataOutput output, Call call) { 426 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 427 DataOutputStream data = new DataOutputStream(baos); 428 429 try { 430 data.writeInt(VERSION); 431 data.writeLong(call.date); 432 data.writeLong(call.duration); 433 writeString(data, call.number); 434 data.writeInt(call.type); 435 data.writeInt(call.numberPresentation); 436 writeString(data, call.accountComponentName); 437 writeString(data, call.accountId); 438 writeString(data, call.accountAddress); 439 data.writeLong(call.dataUsage == null ? 0 : call.dataUsage); 440 data.writeInt(call.features); 441 442 OEMData oemData = getOEMDataForCall(call); 443 data.writeUTF(oemData.namespace); 444 data.writeInt(oemData.bytes.length); 445 data.write(oemData.bytes); 446 data.writeInt(END_OEM_DATA_MARKER); 447 448 data.writeInt(call.addForAllUsers); 449 450 writeString(data, call.postDialDigits); 451 452 writeString(data, call.viaNumber); 453 454 data.writeInt(call.callBlockReason); 455 writeString(data, call.callScreeningAppName); 456 writeString(data, call.callScreeningComponentName); 457 458 // Step 1007 used to write caller ID data; those were pulled. Keeping that in here 459 // to maintain compatibility for backups which had this data. 460 writeString(data, ""); 461 writeString(data, ""); 462 writeString(data, ""); 463 writeString(data, ""); 464 writeString(data, ""); 465 writeInteger(data, null); 466 467 data.writeLong(call.missedReason); 468 469 data.flush(); 470 471 output.writeEntityHeader(Integer.toString(call.id), baos.size()); 472 output.writeEntityData(baos.toByteArray(), baos.size()); 473 474 if (isDebug()) { 475 Log.d(TAG, "Wrote call to backup: " + call + " with byte array: " + baos); 476 } 477 } catch (IOException e) { 478 Log.e(TAG, "Failed to backup call: " + call, e); 479 } 480 } 481 482 /** 483 * Allows OEMs to provide proprietary data to backup along with the rest of the call log 484 * data. Because there is no way to provide a Backup Transport implementation 485 * nor peek into the data format of backup entries without system-level permissions, it is 486 * not possible (at the time of this writing) to write CTS tests for this piece of code. 487 * It is, therefore, important that if you alter this portion of code that you 488 * test backup and restore of call log is working as expected; ideally this would be tested by 489 * backing up and restoring between two different Android phone devices running M+. 490 */ getOEMDataForCall(Call call)491 private OEMData getOEMDataForCall(Call call) { 492 return new OEMData(NO_OEM_NAMESPACE, ZERO_BYTE_ARRAY); 493 494 // OEMs that want to add their own proprietary data to call log backup should replace the 495 // code above with their own namespace and add any additional data they need. 496 // Versioning and size-prefixing the data should be done here as needed. 497 // 498 // Example: 499 500 /* 501 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 502 DataOutputStream data = new DataOutputStream(baos); 503 504 String customData1 = "Generic OEM"; 505 int customData2 = 42; 506 507 // Write a version for the data 508 data.writeInt(OEM_DATA_VERSION); 509 510 // Write the data and flush 511 data.writeUTF(customData1); 512 data.writeInt(customData2); 513 data.flush(); 514 515 String oemNamespace = "com.oem.namespace"; 516 return new OEMData(oemNamespace, baos.toByteArray()); 517 */ 518 } 519 520 /** 521 * Allows OEMs to read their own proprietary data when doing a call log restore. It is important 522 * that the implementation verify the namespace of the data matches their expected value before 523 * attempting to read the data or else you may risk reading invalid data. 524 * 525 * See {@link #getOEMDataForCall} for information concerning proper testing of this code. 526 */ readOEMDataForCall(Call call, OEMData oemData)527 private void readOEMDataForCall(Call call, OEMData oemData) { 528 // OEMs that want to read proprietary data from a call log restore should do so here. 529 // Before reading from the data, an OEM should verify that the data matches their 530 // expected namespace. 531 // 532 // Example: 533 534 /* 535 if ("com.oem.expected.namespace".equals(oemData.namespace)) { 536 ByteArrayInputStream bais = new ByteArrayInputStream(oemData.bytes); 537 DataInputStream data = new DataInputStream(bais); 538 539 // Check against this version as we read data. 540 int version = data.readInt(); 541 String customData1 = data.readUTF(); 542 int customData2 = data.readInt(); 543 // do something with data 544 } 545 */ 546 } 547 548 writeString(DataOutputStream data, String str)549 private void writeString(DataOutputStream data, String str) throws IOException { 550 if (str == null) { 551 data.writeBoolean(false); 552 } else { 553 data.writeBoolean(true); 554 data.writeUTF(str); 555 } 556 } 557 readString(DataInputStream data)558 private String readString(DataInputStream data) throws IOException { 559 if (data.readBoolean()) { 560 return data.readUTF(); 561 } else { 562 return null; 563 } 564 } 565 writeInteger(DataOutputStream data, Integer num)566 private void writeInteger(DataOutputStream data, Integer num) throws IOException { 567 if (num == null) { 568 data.writeBoolean(false); 569 } else { 570 data.writeBoolean(true); 571 data.writeInt(num); 572 } 573 } 574 readInteger(DataInputStream data)575 private Integer readInteger(DataInputStream data) throws IOException { 576 if (data.readBoolean()) { 577 return data.readInt(); 578 } else { 579 return null; 580 } 581 } 582 removeCallFromBackup(BackupDataOutput output, int callId)583 private void removeCallFromBackup(BackupDataOutput output, int callId) { 584 try { 585 output.writeEntityHeader(Integer.toString(callId), -1); 586 } catch (IOException e) { 587 Log.e(TAG, "Failed to remove call: " + callId, e); 588 } 589 } 590 isDebug()591 private static boolean isDebug() { 592 return Log.isLoggable(TAG, Log.DEBUG); 593 } 594 } 595