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
toString()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 
OEMData(String namespace, byte[] bytes)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
onBackup(ParcelFileDescriptor oldStateDescriptor, BackupDataOutput data, ParcelFileDescriptor newStateDescriptor)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
onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState)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
runBackup(CallLogBackupState state, BackupDataOutput data, Iterable<Call> calls)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 
getAllCallLogEntries()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 
writeCallToProvider(Call call)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
readState(DataInput dataInput)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
writeState(DataOutput dataOutput, CallLogBackupState state)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
readCallFromData(BackupDataInput data)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 
readCallFromCursor(Cursor cursor)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 
addCallToBackup(BackupDataOutput output, Call call)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      */
getOEMDataForCall(Call call)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      */
readOEMDataForCall(Call call, OEMData oemData)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 
writeString(DataOutputStream data, String str)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 
readString(DataInputStream data)515     private String readString(DataInputStream data) throws IOException {
516         if (data.readBoolean()) {
517             return data.readUTF();
518         } else {
519             return null;
520         }
521     }
522 
removeCallFromBackup(BackupDataOutput output, int callId)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 
shouldPreventBackup(Context context)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 
isDebug()537     private static boolean isDebug() {
538         return Log.isLoggable(TAG, Log.DEBUG);
539     }
540 }
541