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