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