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