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