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