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