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