• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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         if (shouldPreventBackup(this)) {
178             if (isDebug()) {
179                 Log.d(TAG, "Skipping restore");
180             }
181             return;
182         }
183 
184         if (isDebug()) {
185             Log.d(TAG, "Performing Restore");
186         }
187 
188         while (data.readNextHeader()) {
189             Call call = readCallFromData(data);
190             if (call != null) {
191                 writeCallToProvider(call);
192                 if (isDebug()) {
193                     Log.d(TAG, "Restored call: " + call);
194                 }
195             }
196         }
197     }
198 
199     @VisibleForTesting
runBackup(CallLogBackupState state, BackupDataOutput data, Iterable<Call> calls)200     void runBackup(CallLogBackupState state, BackupDataOutput data, Iterable<Call> calls) {
201         SortedSet<Integer> callsToRemove = new TreeSet<>(state.callIds);
202 
203         // Loop through all the call log entries to identify:
204         // (1) new calls
205         // (2) calls which have been deleted.
206         for (Call call : calls) {
207             if (!state.callIds.contains(call.id)) {
208 
209                 if (isDebug()) {
210                     Log.d(TAG, "Adding call to backup: " + call);
211                 }
212 
213                 // This call new (not in our list from the last backup), lets back it up.
214                 addCallToBackup(data, call);
215                 state.callIds.add(call.id);
216             } else {
217                 // This call still exists in the current call log so delete it from the
218                 // "callsToRemove" set since we want to keep it.
219                 callsToRemove.remove(call.id);
220             }
221         }
222 
223         // Remove calls which no longer exist in the set.
224         for (Integer i : callsToRemove) {
225             if (isDebug()) {
226                 Log.d(TAG, "Removing call from backup: " + i);
227             }
228 
229             removeCallFromBackup(data, i);
230             state.callIds.remove(i);
231         }
232     }
233 
getAllCallLogEntries()234     private Iterable<Call> getAllCallLogEntries() {
235         List<Call> calls = new LinkedList<>();
236 
237         // We use the API here instead of querying ContactsDatabaseHelper directly because
238         // CallLogProvider has special locks in place for sychronizing when to read.  Using the APIs
239         // gives us that for free.
240         ContentResolver resolver = getContentResolver();
241         Cursor cursor = resolver.query(
242                 CallLog.Calls.CONTENT_URI, CALL_LOG_PROJECTION, null, null, null);
243         if (cursor != null) {
244             try {
245                 while (cursor.moveToNext()) {
246                     Call call = readCallFromCursor(cursor);
247                     if (call != null) {
248                         calls.add(call);
249                     }
250                 }
251             } finally {
252                 cursor.close();
253             }
254         }
255 
256         return calls;
257     }
258 
writeCallToProvider(Call call)259     private void writeCallToProvider(Call call) {
260         Long dataUsage = call.dataUsage == 0 ? null : call.dataUsage;
261 
262         PhoneAccountHandle handle = null;
263         if (call.accountComponentName != null && call.accountId != null) {
264             handle = new PhoneAccountHandle(
265                     ComponentName.unflattenFromString(call.accountComponentName), call.accountId);
266         }
267         boolean addForAllUsers = call.addForAllUsers == 1;
268         // We backup the calllog in the user running this backup agent, so write calls to this user.
269         Calls.addCall(null /* CallerInfo */, this, call.number, call.postDialDigits, call.viaNumber,
270                 call.numberPresentation, call.type, call.features, handle, call.date,
271                 (int) call.duration, dataUsage, addForAllUsers, null, true /* is_read */);
272     }
273 
274     @VisibleForTesting
readState(DataInput dataInput)275     CallLogBackupState readState(DataInput dataInput) throws IOException {
276         CallLogBackupState state = new CallLogBackupState();
277         state.callIds = new TreeSet<>();
278 
279         try {
280             // Read the version.
281             state.version = dataInput.readInt();
282 
283             if (state.version >= 1) {
284                 // Read the size.
285                 int size = dataInput.readInt();
286 
287                 // Read all of the call IDs.
288                 for (int i = 0; i < size; i++) {
289                     state.callIds.add(dataInput.readInt());
290                 }
291             }
292         } catch (EOFException e) {
293             state.version = VERSION_NO_PREVIOUS_STATE;
294         }
295 
296         return state;
297     }
298 
299     @VisibleForTesting
writeState(DataOutput dataOutput, CallLogBackupState state)300     void writeState(DataOutput dataOutput, CallLogBackupState state)
301             throws IOException {
302         // Write version first of all
303         dataOutput.writeInt(VERSION);
304 
305         // [Version 1]
306         // size + callIds
307         dataOutput.writeInt(state.callIds.size());
308         for (Integer i : state.callIds) {
309             dataOutput.writeInt(i);
310         }
311     }
312 
313     @VisibleForTesting
readCallFromData(BackupDataInput data)314     Call readCallFromData(BackupDataInput data) {
315         final int callId;
316         try {
317             callId = Integer.parseInt(data.getKey());
318         } catch (NumberFormatException e) {
319             Log.e(TAG, "Unexpected key found in restore: " + data.getKey());
320             return null;
321         }
322 
323         try {
324             byte [] byteArray = new byte[data.getDataSize()];
325             data.readEntityData(byteArray, 0, byteArray.length);
326             DataInputStream dataInput = new DataInputStream(new ByteArrayInputStream(byteArray));
327 
328             Call call = new Call();
329             call.id = callId;
330 
331             int version = dataInput.readInt();
332             if (version >= 1) {
333                 call.date = dataInput.readLong();
334                 call.duration = dataInput.readLong();
335                 call.number = readString(dataInput);
336                 call.type = dataInput.readInt();
337                 call.numberPresentation = dataInput.readInt();
338                 call.accountComponentName = readString(dataInput);
339                 call.accountId = readString(dataInput);
340                 call.accountAddress = readString(dataInput);
341                 call.dataUsage = dataInput.readLong();
342                 call.features = dataInput.readInt();
343             }
344 
345             if (version >= 1002) {
346                 String namespace = dataInput.readUTF();
347                 int length = dataInput.readInt();
348                 byte[] buffer = new byte[length];
349                 dataInput.read(buffer);
350                 readOEMDataForCall(call, new OEMData(namespace, buffer));
351 
352                 int marker = dataInput.readInt();
353                 if (marker != END_OEM_DATA_MARKER) {
354                     Log.e(TAG, "Did not find END-OEM marker for call " + call.id);
355                     // The marker does not match the expected value, ignore this call completely.
356                     return null;
357                 }
358             }
359 
360             if (version >= 1003) {
361                 call.addForAllUsers = dataInput.readInt();
362             }
363 
364             if (version >= 1004) {
365                 call.postDialDigits = readString(dataInput);
366             }
367 
368             if(version >= 1005) {
369                 call.viaNumber = readString(dataInput);
370             }
371 
372             return call;
373         } catch (IOException e) {
374             Log.e(TAG, "Error reading call data for " + callId, e);
375             return null;
376         }
377     }
378 
readCallFromCursor(Cursor cursor)379     private Call readCallFromCursor(Cursor cursor) {
380         Call call = new Call();
381         call.id = cursor.getInt(cursor.getColumnIndex(CallLog.Calls._ID));
382         call.date = cursor.getLong(cursor.getColumnIndex(CallLog.Calls.DATE));
383         call.duration = cursor.getLong(cursor.getColumnIndex(CallLog.Calls.DURATION));
384         call.number = cursor.getString(cursor.getColumnIndex(CallLog.Calls.NUMBER));
385         call.postDialDigits = cursor.getString(
386                 cursor.getColumnIndex(CallLog.Calls.POST_DIAL_DIGITS));
387         call.viaNumber = cursor.getString(cursor.getColumnIndex(CallLog.Calls.VIA_NUMBER));
388         call.type = cursor.getInt(cursor.getColumnIndex(CallLog.Calls.TYPE));
389         call.numberPresentation =
390                 cursor.getInt(cursor.getColumnIndex(CallLog.Calls.NUMBER_PRESENTATION));
391         call.accountComponentName =
392                 cursor.getString(cursor.getColumnIndex(CallLog.Calls.PHONE_ACCOUNT_COMPONENT_NAME));
393         call.accountId =
394                 cursor.getString(cursor.getColumnIndex(CallLog.Calls.PHONE_ACCOUNT_ID));
395         call.accountAddress =
396                 cursor.getString(cursor.getColumnIndex(CallLog.Calls.PHONE_ACCOUNT_ADDRESS));
397         call.dataUsage = cursor.getLong(cursor.getColumnIndex(CallLog.Calls.DATA_USAGE));
398         call.features = cursor.getInt(cursor.getColumnIndex(CallLog.Calls.FEATURES));
399         call.addForAllUsers = cursor.getInt(cursor.getColumnIndex(Calls.ADD_FOR_ALL_USERS));
400         return call;
401     }
402 
addCallToBackup(BackupDataOutput output, Call call)403     private void addCallToBackup(BackupDataOutput output, Call call) {
404         ByteArrayOutputStream baos = new ByteArrayOutputStream();
405         DataOutputStream data = new DataOutputStream(baos);
406 
407         try {
408             data.writeInt(VERSION);
409             data.writeLong(call.date);
410             data.writeLong(call.duration);
411             writeString(data, call.number);
412             data.writeInt(call.type);
413             data.writeInt(call.numberPresentation);
414             writeString(data, call.accountComponentName);
415             writeString(data, call.accountId);
416             writeString(data, call.accountAddress);
417             data.writeLong(call.dataUsage == null ? 0 : call.dataUsage);
418             data.writeInt(call.features);
419 
420             OEMData oemData = getOEMDataForCall(call);
421             data.writeUTF(oemData.namespace);
422             data.writeInt(oemData.bytes.length);
423             data.write(oemData.bytes);
424             data.writeInt(END_OEM_DATA_MARKER);
425 
426             data.writeInt(call.addForAllUsers);
427 
428             writeString(data, call.postDialDigits);
429 
430             writeString(data, call.viaNumber);
431 
432             data.flush();
433 
434             output.writeEntityHeader(Integer.toString(call.id), baos.size());
435             output.writeEntityData(baos.toByteArray(), baos.size());
436 
437             if (isDebug()) {
438                 Log.d(TAG, "Wrote call to backup: " + call + " with byte array: " + baos);
439             }
440         } catch (IOException e) {
441             Log.e(TAG, "Failed to backup call: " + call, e);
442         }
443     }
444 
445     /**
446      * Allows OEMs to provide proprietary data to backup along with the rest of the call log
447      * data. Because there is no way to provide a Backup Transport implementation
448      * nor peek into the data format of backup entries without system-level permissions, it is
449      * not possible (at the time of this writing) to write CTS tests for this piece of code.
450      * It is, therefore, important that if you alter this portion of code that you
451      * test backup and restore of call log is working as expected; ideally this would be tested by
452      * backing up and restoring between two different Android phone devices running M+.
453      */
getOEMDataForCall(Call call)454     private OEMData getOEMDataForCall(Call call) {
455         return new OEMData(NO_OEM_NAMESPACE, ZERO_BYTE_ARRAY);
456 
457         // OEMs that want to add their own proprietary data to call log backup should replace the
458         // code above with their own namespace and add any additional data they need.
459         // Versioning and size-prefixing the data should be done here as needed.
460         //
461         // Example:
462 
463         /*
464         ByteArrayOutputStream baos = new ByteArrayOutputStream();
465         DataOutputStream data = new DataOutputStream(baos);
466 
467         String customData1 = "Generic OEM";
468         int customData2 = 42;
469 
470         // Write a version for the data
471         data.writeInt(OEM_DATA_VERSION);
472 
473         // Write the data and flush
474         data.writeUTF(customData1);
475         data.writeInt(customData2);
476         data.flush();
477 
478         String oemNamespace = "com.oem.namespace";
479         return new OEMData(oemNamespace, baos.toByteArray());
480         */
481     }
482 
483     /**
484      * Allows OEMs to read their own proprietary data when doing a call log restore. It is important
485      * that the implementation verify the namespace of the data matches their expected value before
486      * attempting to read the data or else you may risk reading invalid data.
487      *
488      * See {@link #getOEMDataForCall} for information concerning proper testing of this code.
489      */
readOEMDataForCall(Call call, OEMData oemData)490     private void readOEMDataForCall(Call call, OEMData oemData) {
491         // OEMs that want to read proprietary data from a call log restore should do so here.
492         // Before reading from the data, an OEM should verify that the data matches their
493         // expected namespace.
494         //
495         // Example:
496 
497         /*
498         if ("com.oem.expected.namespace".equals(oemData.namespace)) {
499             ByteArrayInputStream bais = new ByteArrayInputStream(oemData.bytes);
500             DataInputStream data = new DataInputStream(bais);
501 
502             // Check against this version as we read data.
503             int version = data.readInt();
504             String customData1 = data.readUTF();
505             int customData2 = data.readInt();
506             // do something with data
507         }
508         */
509     }
510 
511 
writeString(DataOutputStream data, String str)512     private void writeString(DataOutputStream data, String str) throws IOException {
513         if (str == null) {
514             data.writeBoolean(false);
515         } else {
516             data.writeBoolean(true);
517             data.writeUTF(str);
518         }
519     }
520 
readString(DataInputStream data)521     private String readString(DataInputStream data) throws IOException {
522         if (data.readBoolean()) {
523             return data.readUTF();
524         } else {
525             return null;
526         }
527     }
528 
removeCallFromBackup(BackupDataOutput output, int callId)529     private void removeCallFromBackup(BackupDataOutput output, int callId) {
530         try {
531             output.writeEntityHeader(Integer.toString(callId), -1);
532         } catch (IOException e) {
533             Log.e(TAG, "Failed to remove call: " + callId, e);
534         }
535     }
536 
shouldPreventBackup(Context context)537     static boolean shouldPreventBackup(Context context) {
538         // Check to see that the user is full-data aware before performing calllog backup.
539         return Settings.Secure.getInt(
540                 context.getContentResolver(), USER_FULL_DATA_BACKUP_AWARE, 0) == 0;
541     }
542 
isDebug()543     private static boolean isDebug() {
544         return Log.isLoggable(TAG, Log.DEBUG);
545     }
546 }
547