• 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 static android.provider.CallLog.Calls.MISSED_REASON_NOT_MISSED;
20 import static com.android.calllogbackup.Flags.callLogRestoreDeduplicationEnabled;
21 import static com.android.calllogbackup.Flags.batchDeduplicationEnabled;
22 
23 import android.app.backup.BackupAgent;
24 import android.app.backup.BackupDataInput;
25 import android.app.backup.BackupDataOutput;
26 import android.app.backup.BackupManager;
27 import android.app.backup.BackupRestoreEventLogger;
28 import android.content.ComponentName;
29 import android.content.ContentResolver;
30 import android.database.Cursor;
31 import android.os.ParcelFileDescriptor;
32 import android.provider.CallLog;
33 import android.provider.CallLog.Calls;
34 import android.telecom.PhoneAccountHandle;
35 import android.telephony.SubscriptionInfo;
36 import android.telephony.SubscriptionManager;
37 import android.util.Log;
38 
39 import com.android.internal.annotations.VisibleForTesting;
40 
41 import java.io.BufferedOutputStream;
42 import java.io.ByteArrayInputStream;
43 import java.io.ByteArrayOutputStream;
44 import java.io.DataInput;
45 import java.io.DataInputStream;
46 import java.io.DataOutput;
47 import java.io.DataOutputStream;
48 import java.io.EOFException;
49 import java.io.FileInputStream;
50 import java.io.FileOutputStream;
51 import java.io.IOException;
52 import java.util.ArrayList;
53 import java.util.LinkedList;
54 import java.util.List;
55 import java.util.HashMap;
56 import java.util.Map;
57 import java.util.SortedSet;
58 import java.util.TreeSet;
59 
60 /**
61  * Call log backup agent.
62  */
63 public class CallLogBackupAgent extends BackupAgent {
64 
65     @VisibleForTesting
66     static class CallLogBackupState {
67         int version;
68         SortedSet<Integer> callIds;
69     }
70 
71     @VisibleForTesting
72     static class Call {
73         int id;
74         long date;
75         long duration;
76         String number;
77         String postDialDigits = "";
78         String viaNumber = "";
79         int type;
80         int numberPresentation;
81         String accountComponentName;
82         String accountId;
83         String accountAddress;
84         Long dataUsage;
85         int features;
86         int addForAllUsers = 1;
87         int callBlockReason = Calls.BLOCK_REASON_NOT_BLOCKED;
88         String callScreeningAppName = null;
89         String callScreeningComponentName = null;
90         long missedReason = MISSED_REASON_NOT_MISSED;
91         int isPhoneAccountMigrationPending;
92         int isBusinessCall;
93         String assertedDisplayName = "";
94 
95         @Override
toString()96         public String toString() {
97             if (isDebug()) {
98                 return  "[" + id + ", account: [" + accountComponentName + " : " + accountId +
99                     "]," + number + ", " + date + "]";
100             } else {
101                 return "[" + id + "]";
102             }
103         }
104     }
105 
106     static class OEMData {
107         String namespace;
108         byte[] bytes;
109 
OEMData(String namespace, byte[] bytes)110         public OEMData(String namespace, byte[] bytes) {
111             this.namespace = namespace;
112             this.bytes = bytes == null ? ZERO_BYTE_ARRAY : bytes;
113         }
114     }
115 
116     private static final String TAG = "CallLogBackupAgent";
117 
118     private static final int CALL_LOG_DEDUPLICATION_BATCH_SIZE = 250;
119 
120     /** Data types and errors used when reporting B&R success rate and errors.  */
121     @BackupRestoreEventLogger.BackupRestoreDataType
122     @VisibleForTesting
123     static final String CALLLOGS = "telecom_call_logs";
124 
125     @BackupRestoreEventLogger.BackupRestoreError
126     static final String ERROR_UNEXPECTED_KEY = "unexpected_key";
127     @BackupRestoreEventLogger.BackupRestoreError
128     static final String ERROR_END_OEM_MARKER_NOT_FOUND = "end_oem_marker_not_found";
129     @BackupRestoreEventLogger.BackupRestoreError
130     static final String ERROR_READING_CALL_DATA = "error_reading_call_data";
131     @BackupRestoreEventLogger.BackupRestoreError
132     static final String ERROR_BACKUP_CALL_FAILED = "backup_call_failed";
133 
134     private BackupRestoreEventLogger mLogger;
135 
136     /** Current version of CallLogBackup. Used to track the backup format. */
137     @VisibleForTesting
138     static final int VERSION = 1010;
139     /** Version indicating that there exists no previous backup entry. */
140     @VisibleForTesting
141     static final int VERSION_NO_PREVIOUS_STATE = 0;
142 
143     static final String NO_OEM_NAMESPACE = "no-oem-namespace";
144 
145     static final byte[] ZERO_BYTE_ARRAY = new byte[0];
146 
147     static final int END_OEM_DATA_MARKER = 0x60061E;
148 
149     static final String TELEPHONY_PHONE_ACCOUNT_HANDLE_COMPONENT_NAME =
150             "com.android.phone/com.android.services.telephony.TelephonyConnectionService";
151 
152     @VisibleForTesting
153     static final String SELECTION_CALL_DATE_AND_NUMBER =
154             CallLog.Calls.DATE + " = ? AND " + CallLog.Calls.NUMBER + " = ?";
155 
156     @VisibleForTesting
157     protected Map<Integer, String> mSubscriptionInfoMap;
158 
159     private static final String[] CALL_LOG_PROJECTION = new String[] {
160         CallLog.Calls._ID,
161         CallLog.Calls.DATE,
162         CallLog.Calls.DURATION,
163         CallLog.Calls.NUMBER,
164         CallLog.Calls.POST_DIAL_DIGITS,
165         CallLog.Calls.VIA_NUMBER,
166         CallLog.Calls.TYPE,
167         CallLog.Calls.COUNTRY_ISO,
168         CallLog.Calls.GEOCODED_LOCATION,
169         CallLog.Calls.NUMBER_PRESENTATION,
170         CallLog.Calls.PHONE_ACCOUNT_COMPONENT_NAME,
171         CallLog.Calls.PHONE_ACCOUNT_ID,
172         CallLog.Calls.PHONE_ACCOUNT_ADDRESS,
173         CallLog.Calls.DATA_USAGE,
174         CallLog.Calls.FEATURES,
175         CallLog.Calls.ADD_FOR_ALL_USERS,
176         CallLog.Calls.BLOCK_REASON,
177         CallLog.Calls.CALL_SCREENING_APP_NAME,
178         CallLog.Calls.CALL_SCREENING_COMPONENT_NAME,
179         CallLog.Calls.MISSED_REASON,
180         CallLog.Calls.IS_PHONE_ACCOUNT_MIGRATION_PENDING,
181         CallLog.Calls.IS_BUSINESS_CALL,
182         CallLog.Calls.ASSERTED_DISPLAY_NAME
183     };
184 
185     /**
186      * BackupRestoreEventLogger Dependencies for testing.
187      */
188     @VisibleForTesting
189     public interface BackupRestoreEventLoggerProxy {
logItemsBackedUp(String dataType, int count)190         void logItemsBackedUp(String dataType, int count);
logItemsBackupFailed(String dataType, int count, String error)191         void logItemsBackupFailed(String dataType, int count, String error);
logItemsRestored(String dataType, int count)192         void logItemsRestored(String dataType, int count);
logItemsRestoreFailed(String dataType, int count, String error)193         void logItemsRestoreFailed(String dataType, int count, String error);
194     }
195 
196     private BackupRestoreEventLoggerProxy mBackupRestoreEventLoggerProxy =
197             new BackupRestoreEventLoggerProxy() {
198         @Override
199         public void logItemsBackedUp(String dataType, int count) {
200             mLogger.logItemsBackedUp(dataType, count);
201         }
202 
203         @Override
204         public void logItemsBackupFailed(String dataType, int count, String error) {
205             mLogger.logItemsBackupFailed(dataType, count, error);
206         }
207 
208         @Override
209         public void logItemsRestored(String dataType, int count) {
210             mLogger.logItemsRestored(dataType, count);
211         }
212 
213         @Override
214         public void logItemsRestoreFailed(String dataType, int count, String error) {
215             mLogger.logItemsRestoreFailed(dataType, count, error);
216         }
217     };
218 
219     /**
220      * Overrides BackupRestoreEventLogger dependencies for testing.
221      */
222     @VisibleForTesting
setBackupRestoreEventLoggerProxy(BackupRestoreEventLoggerProxy proxy)223     public void setBackupRestoreEventLoggerProxy(BackupRestoreEventLoggerProxy proxy) {
224         mBackupRestoreEventLoggerProxy = proxy;
225     }
226 
227     @Override
onCreate()228     public void onCreate() {
229         super.onCreate();
230         Log.d(TAG, "onCreate");
231         BackupManager backupManager = new BackupManager(getApplicationContext());
232         mLogger = backupManager.getBackupRestoreEventLogger(/* backupAgent */ this);
233     }
234 
235     /** ${inheritDoc} */
236     @Override
onBackup(ParcelFileDescriptor oldStateDescriptor, BackupDataOutput data, ParcelFileDescriptor newStateDescriptor)237     public void onBackup(ParcelFileDescriptor oldStateDescriptor, BackupDataOutput data,
238             ParcelFileDescriptor newStateDescriptor) throws IOException {
239         // Get the list of the previous calls IDs which were backed up.
240         DataInputStream dataInput = new DataInputStream(
241                 new FileInputStream(oldStateDescriptor.getFileDescriptor()));
242         final CallLogBackupState state;
243         try {
244             state = readState(dataInput);
245         } finally {
246             dataInput.close();
247         }
248 
249         SubscriptionManager subscriptionManager = getBaseContext().getSystemService(
250                 SubscriptionManager.class);
251         if (subscriptionManager != null) {
252             mSubscriptionInfoMap = new HashMap<>();
253             // Use getAllSubscirptionInfoList() to get the mapping between iccId and subId
254             // from the subscription database
255             List<SubscriptionInfo> subscriptionInfos = subscriptionManager
256                     .getAllSubscriptionInfoList();
257             for (SubscriptionInfo subscriptionInfo : subscriptionInfos) {
258                 mSubscriptionInfoMap.put(
259                         subscriptionInfo.getSubscriptionId(), subscriptionInfo.getIccId());
260             }
261         }
262 
263         // Run the actual backup of data
264         runBackup(state, data, getAllCallLogEntries());
265 
266         // Rewrite the backup state.
267         DataOutputStream dataOutput = new DataOutputStream(new BufferedOutputStream(
268                 new FileOutputStream(newStateDescriptor.getFileDescriptor())));
269         try {
270             writeState(dataOutput, state);
271         } finally {
272             dataOutput.close();
273         }
274     }
275 
276     /**
277      * Restores a call log backup given provided backup data.
278      * @param data the call log backup data; must be in a format written by
279      * {@link #onBackup(ParcelFileDescriptor, BackupDataOutput, ParcelFileDescriptor)}.
280      *
281      * @param appVersionCode The OS version of the data to restore; not used here.
282      * @param newState See parent class; not used here.
283      * @throws IOException Not thrown by the call log backup agent, but required by the underlying
284      * interface -- throwing IOException will cause the existing call log data to be cleared.
285      */
286     @Override
onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState)287     public void onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState)
288             throws IOException {
289         if (isDebug()) {
290             Log.d(TAG, "Performing Restore");
291         }
292 
293         if (callLogRestoreDeduplicationEnabled() && batchDeduplicationEnabled()) {
294             if (hasExistingCallLogs()) {
295                 Map<String, Call> callMap = new HashMap<>();
296 
297                 while (data.readNextHeader()) {
298                     Call call = readCallFromData(data);
299                     if (call != null && call.type != Calls.VOICEMAIL_TYPE) {
300                         String key = getCallKey(call.date, call.number);
301                         callMap.put(key, call);
302 
303                         if (callMap.size() >= getBatchSize()) {
304                             restoreCallBatch(callMap);
305                             // Clear the map for the next batch
306                             callMap.clear();
307                         }
308                     }
309                 }
310 
311                 if (!callMap.isEmpty()) {
312                     restoreCallBatch(callMap);
313                 }
314             } else {
315                 // No existing call logs, so no need for deduplication
316                 performRestoreWithoutDeduplication(data);
317             }
318             return;
319         }
320 
321         while (data.readNextHeader()) {
322             Call call = readCallFromData(data);
323             if (call != null && call.type != Calls.VOICEMAIL_TYPE) {
324                 if (!callLogRestoreDeduplicationEnabled() || !isDuplicateCall(call)) {
325                     writeCallToProvider(call);
326                     mBackupRestoreEventLoggerProxy.logItemsRestored(CALLLOGS, /* count */ 1);
327                     if (isDebug()) {
328                         Log.d(TAG, "Restored call: " + call);
329                     }
330                 }
331             }
332         }
333     }
334 
restoreCallBatch(Map<String, Call> callMap)335     private void restoreCallBatch(Map<String, Call> callMap) {
336         removeDuplicateCalls(callMap);
337 
338         for (Call nonDuplicateCall : callMap.values()) {
339             writeAndLogCall(nonDuplicateCall);
340         }
341     }
342 
removeDuplicateCalls(Map<String, Call> callMap)343     private void removeDuplicateCalls(Map<String, Call> callMap) {
344         // Build the selection clause for the query. This clause will look like:
345         // ((date = ? AND number = ?) OR (date = ? AND number = ?) OR ...)
346         // where the placeholders (?) will be replaced with the date and number of each call
347         // in the callMap.
348         StringBuilder selection = new StringBuilder();
349         selection.append(" (");
350 
351         String[] selectionArgs = new String[callMap.size() * 2];
352         int argIndex = 0;
353 
354         for (Call call : callMap.values()) {
355             if (argIndex > 0) {
356                 selection.append(" OR ");
357             }
358             selection.append("(");
359             selection.append(SELECTION_CALL_DATE_AND_NUMBER);
360             selection.append(")");
361             selectionArgs[argIndex++] = String.valueOf(call.date);
362             selectionArgs[argIndex++] = call.number;
363         }
364         selection.append(")");
365 
366         // Query the call log and check for duplicates
367         try (Cursor cursor = getContentResolver().query(CallLog.Calls.CONTENT_URI,
368                 new String[]{CallLog.Calls.DATE, CallLog.Calls.NUMBER},
369                 selection.toString(), selectionArgs, /* sortOrder */ null)) {
370 
371             if (cursor != null && cursor.moveToFirst()) {
372                 do {
373                     long callLogDate = cursor.getLong(cursor.getColumnIndex(CallLog.Calls.DATE));
374                     String callLogNumber = cursor.getString(
375                             cursor.getColumnIndex(CallLog.Calls.NUMBER));
376                     String key = getCallKey(callLogDate, callLogNumber);
377 
378                     callMap.remove(key);
379                 } while (cursor.moveToNext());
380             }
381         }
382     }
383 
isDuplicateCall(Call call)384     private boolean isDuplicateCall(Call call) {
385         // Build the query selection
386         String[] selectionArgs = new String[]{String.valueOf(call.date), call.number};
387 
388         // Query the call log provider. We only need to check for the existence of a call with
389         // the same date and number, so we only select the _ID column.
390         try (Cursor cursor = getContentResolver().query(CallLog.Calls.CONTENT_URI,
391                 new String[]{CallLog.Calls._ID}, SELECTION_CALL_DATE_AND_NUMBER,
392                 selectionArgs, /* sortOrder */ null)) {
393 
394             return cursor != null && cursor.moveToFirst();
395         }
396     }
397 
performRestoreWithoutDeduplication(BackupDataInput data)398     private void performRestoreWithoutDeduplication(BackupDataInput data) throws IOException {
399         while (data.readNextHeader()) {
400             Call call = readCallFromData(data);
401             if (call != null && call.type != Calls.VOICEMAIL_TYPE) {
402                 writeAndLogCall(call);
403             }
404         }
405     }
406 
writeAndLogCall(Call call)407     private void writeAndLogCall(Call call) {
408         writeCallToProvider(call);
409         mBackupRestoreEventLoggerProxy.logItemsRestored(CALLLOGS, /* count */ 1);
410         if (isDebug()) {
411             Log.d(TAG, "Restored call: " + call);
412         }
413     }
414 
hasExistingCallLogs()415     private boolean hasExistingCallLogs() {
416         try (Cursor cursor = getContentResolver().query(CallLog.Calls.CONTENT_URI,
417                 new String[]{CallLog.Calls._ID}, /* selection */ null, /* selectionArgs */
418                 null, /* sortOrder */ null)) {
419             return cursor != null && cursor.moveToFirst();
420         }
421     }
422 
423     @VisibleForTesting
getBatchSize()424     int getBatchSize() {
425         return CALL_LOG_DEDUPLICATION_BATCH_SIZE;
426     }
427 
getCallKey(long date, String number)428     private String getCallKey(long date, String number) {
429         return date + "_" + number;
430     }
431 
432     @VisibleForTesting
runBackup(CallLogBackupState state, BackupDataOutput data, Iterable<Call> calls)433     void runBackup(CallLogBackupState state, BackupDataOutput data, Iterable<Call> calls) {
434         SortedSet<Integer> callsToRemove = new TreeSet<>(state.callIds);
435 
436         // Loop through all the call log entries to identify:
437         // (1) new calls
438         // (2) calls which have been deleted.
439         for (Call call : calls) {
440             if (!state.callIds.contains(call.id)) {
441 
442                 if (isDebug()) {
443                     Log.d(TAG, "Adding call to backup: " + call);
444                 }
445 
446                 // This call new (not in our list from the last backup), lets back it up.
447                 addCallToBackup(data, call);
448                 state.callIds.add(call.id);
449             } else {
450                 // This call still exists in the current call log so delete it from the
451                 // "callsToRemove" set since we want to keep it.
452                 callsToRemove.remove(call.id);
453                 mBackupRestoreEventLoggerProxy.logItemsBackedUp(CALLLOGS, /* count */ 1);
454             }
455         }
456 
457         // Remove calls which no longer exist in the set.
458         for (Integer i : callsToRemove) {
459             if (isDebug()) {
460                 Log.d(TAG, "Removing call from backup: " + i);
461             }
462 
463             removeCallFromBackup(data, i);
464             state.callIds.remove(i);
465         }
466     }
467 
468     @VisibleForTesting
getAllCallLogEntries()469     Iterable<Call> getAllCallLogEntries() {
470         List<Call> calls = new LinkedList<>();
471 
472         // We use the API here instead of querying ContactsDatabaseHelper directly because
473         // CallLogProvider has special locks in place for sychronizing when to read.  Using the APIs
474         // gives us that for free.
475         ContentResolver resolver = getContentResolver();
476         Cursor cursor = resolver.query(
477                 CallLog.Calls.CONTENT_URI, CALL_LOG_PROJECTION, null, null, null);
478         if (cursor != null) {
479             try {
480                 while (cursor.moveToNext()) {
481                     Call call = readCallFromCursor(cursor);
482                     if (call != null && call.type != Calls.VOICEMAIL_TYPE) {
483                         calls.add(call);
484                     }
485                 }
486             } finally {
487                 cursor.close();
488             }
489         }
490 
491         return calls;
492     }
493 
494     @VisibleForTesting
writeCallToProvider(Call call)495     void writeCallToProvider(Call call) {
496         Long dataUsage = call.dataUsage == 0 ? null : call.dataUsage;
497 
498         PhoneAccountHandle handle = null;
499         if (call.accountComponentName != null && call.accountId != null) {
500             handle = new PhoneAccountHandle(
501                     ComponentName.unflattenFromString(call.accountComponentName), call.accountId);
502         }
503         boolean addForAllUsers = call.addForAllUsers == 1;
504 
505         // We backup the calllog in the user running this backup agent, so write calls to this user.
506         CallLog.AddCallParams.AddCallParametersBuilder builder =
507                 new CallLog.AddCallParams.AddCallParametersBuilder();
508         builder.setCallerInfo(null);
509         builder.setNumber(call.number);
510         builder.setPostDialDigits(call.postDialDigits);
511         builder.setViaNumber(call.viaNumber);
512         builder.setPresentation(call.numberPresentation);
513         builder.setCallType(call.type);
514         builder.setFeatures(call.features);
515         builder.setAccountHandle(handle);
516         builder.setStart(call.date);
517         builder.setDuration((int) call.duration);
518         builder.setDataUsage(dataUsage == null ? Long.MIN_VALUE : dataUsage);
519         builder.setAddForAllUsers(addForAllUsers);
520         builder.setUserToBeInsertedTo(null);
521         builder.setIsRead(true);
522         builder.setCallBlockReason(call.callBlockReason);
523         builder.setCallScreeningAppName(call.callScreeningAppName);
524         builder.setCallScreeningComponentName(call.callScreeningComponentName);
525         builder.setMissedReason(call.missedReason);
526         builder.setIsPhoneAccountMigrationPending(call.isPhoneAccountMigrationPending);
527         builder.setIsBusinessCall(call.isBusinessCall == 1);
528         builder.setAssertedDisplayName(call.assertedDisplayName);
529 
530         Calls.addCall(this, builder.build());
531     }
532 
533     @VisibleForTesting
readState(DataInput dataInput)534     CallLogBackupState readState(DataInput dataInput) throws IOException {
535         CallLogBackupState state = new CallLogBackupState();
536         state.callIds = new TreeSet<>();
537 
538         try {
539             // Read the version.
540             state.version = dataInput.readInt();
541 
542             if (state.version >= 1) {
543                 // Read the size.
544                 int size = dataInput.readInt();
545 
546                 // Read all of the call IDs.
547                 for (int i = 0; i < size; i++) {
548                     state.callIds.add(dataInput.readInt());
549                 }
550             }
551         } catch (EOFException e) {
552             state.version = VERSION_NO_PREVIOUS_STATE;
553         }
554 
555         return state;
556     }
557 
558     @VisibleForTesting
writeState(DataOutput dataOutput, CallLogBackupState state)559     void writeState(DataOutput dataOutput, CallLogBackupState state)
560             throws IOException {
561         // Write version first of all
562         dataOutput.writeInt(VERSION);
563 
564         // [Version 1]
565         // size + callIds
566         dataOutput.writeInt(state.callIds.size());
567         for (Integer i : state.callIds) {
568             dataOutput.writeInt(i);
569         }
570     }
571 
572     @VisibleForTesting
readCallFromData(BackupDataInput data)573     Call readCallFromData(BackupDataInput data) {
574         final int callId;
575         try {
576             callId = Integer.parseInt(data.getKey());
577         } catch (NumberFormatException e) {
578             mBackupRestoreEventLoggerProxy.logItemsRestoreFailed(
579                     CALLLOGS, /* count */ 1, ERROR_UNEXPECTED_KEY);
580             Log.e(TAG, "Unexpected key found in restore: " + data.getKey());
581             return null;
582         }
583 
584         try {
585             byte [] byteArray = new byte[data.getDataSize()];
586             data.readEntityData(byteArray, 0, byteArray.length);
587             DataInputStream dataInput = new DataInputStream(new ByteArrayInputStream(byteArray));
588 
589             Call call = new Call();
590             call.id = callId;
591 
592             int version = dataInput.readInt();
593 
594             // Don't allow downgrades when restoring except when the version is 1010; that version
595             // adds some rather inconsequential columns to the call log database and it is generally
596             // preferable to allow the restore knowing that those new columns will be skipped in the
597             // restore.
598             if (version > VERSION && version != 1010) {
599                 // If somehow we got a backed up row that is newer than the supported file format
600                 // we know of, we will log an error and return null to represent an invalid item.
601                 String errorMessage = "Backup version " + version + " is newer than the current "
602                         + "supported version, " + VERSION;
603                 Log.w(TAG, errorMessage);
604                 mBackupRestoreEventLoggerProxy.logItemsRestoreFailed(CALLLOGS, 1,
605                         errorMessage);
606                 return null;
607             }
608 
609             if (version >= 1) {
610                 call.date = dataInput.readLong();
611                 call.duration = dataInput.readLong();
612                 call.number = readString(dataInput);
613                 call.type = dataInput.readInt();
614                 call.numberPresentation = dataInput.readInt();
615                 call.accountComponentName = readString(dataInput);
616                 call.accountId = readString(dataInput);
617                 call.accountAddress = readString(dataInput);
618                 call.dataUsage = dataInput.readLong();
619                 call.features = dataInput.readInt();
620             }
621 
622             if (version >= 1002) {
623                 String namespace = dataInput.readUTF();
624                 int length = dataInput.readInt();
625                 byte[] buffer = new byte[length];
626                 dataInput.read(buffer);
627                 readOEMDataForCall(call, new OEMData(namespace, buffer));
628 
629                 int marker = dataInput.readInt();
630                 if (marker != END_OEM_DATA_MARKER) {
631                     mBackupRestoreEventLoggerProxy.logItemsRestoreFailed(CALLLOGS, /* count */ 1,
632                             ERROR_END_OEM_MARKER_NOT_FOUND);
633                     Log.e(TAG, "Did not find END-OEM marker for call " + call.id);
634                     // The marker does not match the expected value, ignore this call completely.
635                     return null;
636                 }
637             }
638 
639             if (version >= 1003) {
640                 call.addForAllUsers = dataInput.readInt();
641             }
642 
643             if (version >= 1004) {
644                 call.postDialDigits = readString(dataInput);
645             }
646 
647             if(version >= 1005) {
648                 call.viaNumber = readString(dataInput);
649             }
650 
651             if(version >= 1006) {
652                 call.callBlockReason = dataInput.readInt();
653                 call.callScreeningAppName = readString(dataInput);
654                 call.callScreeningComponentName = readString(dataInput);
655             }
656             if(version >= 1007) {
657                 // Version 1007 had call id columns early in the Q release; they were pulled so we
658                 // will just read the values out here if they exist in a backup and ignore them.
659                 readString(dataInput);
660                 readString(dataInput);
661                 readString(dataInput);
662                 readString(dataInput);
663                 readString(dataInput);
664                 readInteger(dataInput);
665             }
666             if (version >= 1008) {
667                 call.missedReason = dataInput.readLong();
668             }
669             if (version >= 1009) {
670                 call.isPhoneAccountMigrationPending = dataInput.readInt();
671             }
672             if (version >= 1010) {
673                 call.isBusinessCall = dataInput.readInt();
674                 call.assertedDisplayName = readString(dataInput);
675             }
676             /**
677              * In >=T Android, Telephony PhoneAccountHandle must use SubId as the ID (the unique
678              * identifier). Any version of Telephony call logs that are restored in >=T Android
679              * should set pending migration status as true and migrate to the subId later because
680              * different devices have different mappings between SubId and IccId.
681              *
682              * In <T Android, call log PhoneAccountHandle ID uses IccId, and backup with IccId;
683              * in >=T Android, call log PhoneAccountHandle ID uses SubId, and IccId is decided to
684              * use for backup for the reason mentioned above. Every time a call log is restored,
685              * the on-devie sub Id can be determined based on its IccId. The pending migration
686              * from IccId to SubId will be complete after the PhoneAccountHandle is registrated by
687              * Telecom and before CallLogProvider unhides it.
688              */
689             if (call.accountComponentName != null && call.accountComponentName.equals(
690                     TELEPHONY_PHONE_ACCOUNT_HANDLE_COMPONENT_NAME)) {
691                 call.isPhoneAccountMigrationPending = 1;
692             }
693             return call;
694         } catch (IOException e) {
695             mBackupRestoreEventLoggerProxy.logItemsRestoreFailed(
696                     CALLLOGS, /* count */ 1, ERROR_READING_CALL_DATA);
697             Log.e(TAG, "Error reading call data for " + callId, e);
698             return null;
699         }
700     }
701 
702     /**
703      * We need to use IccId for the PHONE_ACCOUNT_ID and set it as pending in backup when:
704      * 1) the phone account component name is telephony; AND
705      * 2) IS_PHONE_ACCOUNT_MIGRATION_PENDING status is not 1 ("1" means the ID is already IccId).
706      */
shouldConvertSubIdToIccIdForBackup( String accountComponentName, int isPhoneAccountMigrationPending)707     private boolean shouldConvertSubIdToIccIdForBackup(
708             String accountComponentName, int isPhoneAccountMigrationPending) {
709         if (mSubscriptionInfoMap == null) {
710             Log.e(TAG, "Subscription database is not available.");
711             return false;
712         }
713         if (accountComponentName != null
714                 && accountComponentName.equals(TELEPHONY_PHONE_ACCOUNT_HANDLE_COMPONENT_NAME)
715                 && isPhoneAccountMigrationPending != 1) {
716             return true;
717         }
718         return false;
719     }
720 
721     @VisibleForTesting
readCallFromCursor(Cursor cursor)722     Call readCallFromCursor(Cursor cursor) {
723         Call call = new Call();
724         call.id = cursor.getInt(cursor.getColumnIndex(CallLog.Calls._ID));
725         call.date = cursor.getLong(cursor.getColumnIndex(CallLog.Calls.DATE));
726         call.duration = cursor.getLong(cursor.getColumnIndex(CallLog.Calls.DURATION));
727         call.number = cursor.getString(cursor.getColumnIndex(CallLog.Calls.NUMBER));
728         call.postDialDigits = cursor.getString(
729                 cursor.getColumnIndex(CallLog.Calls.POST_DIAL_DIGITS));
730         call.viaNumber = cursor.getString(cursor.getColumnIndex(CallLog.Calls.VIA_NUMBER));
731         call.type = cursor.getInt(cursor.getColumnIndex(CallLog.Calls.TYPE));
732         call.numberPresentation =
733                 cursor.getInt(cursor.getColumnIndex(CallLog.Calls.NUMBER_PRESENTATION));
734         call.accountComponentName =
735                 cursor.getString(cursor.getColumnIndex(CallLog.Calls.PHONE_ACCOUNT_COMPONENT_NAME));
736         call.accountId =
737                 cursor.getString(cursor.getColumnIndex(CallLog.Calls.PHONE_ACCOUNT_ID));
738         call.accountAddress =
739                 cursor.getString(cursor.getColumnIndex(CallLog.Calls.PHONE_ACCOUNT_ADDRESS));
740         call.dataUsage = cursor.getLong(cursor.getColumnIndex(CallLog.Calls.DATA_USAGE));
741         call.features = cursor.getInt(cursor.getColumnIndex(CallLog.Calls.FEATURES));
742         call.addForAllUsers = cursor.getInt(cursor.getColumnIndex(Calls.ADD_FOR_ALL_USERS));
743         call.callBlockReason = cursor.getInt(cursor.getColumnIndex(CallLog.Calls.BLOCK_REASON));
744         call.callScreeningAppName = cursor
745             .getString(cursor.getColumnIndex(CallLog.Calls.CALL_SCREENING_APP_NAME));
746         call.callScreeningComponentName = cursor
747             .getString(cursor.getColumnIndex(CallLog.Calls.CALL_SCREENING_COMPONENT_NAME));
748         call.missedReason = cursor
749             .getInt(cursor.getColumnIndex(CallLog.Calls.MISSED_REASON));
750         call.isPhoneAccountMigrationPending = cursor.getInt(
751                 cursor.getColumnIndex(CallLog.Calls.IS_PHONE_ACCOUNT_MIGRATION_PENDING));
752         call.isBusinessCall = cursor.getInt(cursor.getColumnIndex(CallLog.Calls.IS_BUSINESS_CALL));
753         call.assertedDisplayName =
754                 cursor.getString(cursor.getColumnIndex(CallLog.Calls.ASSERTED_DISPLAY_NAME));
755         /*
756          * Starting Android T, the ID of Telephony PhoneAccountHandle need to migrate from IccId
757          * to SubId. Because the mapping between IccId and SubId in different devices is different,
758          * the Backup need to use IccId for the ID and set it as pending migration, and when the
759          * ID is restored, ID need migrated to SubId after the corresponding PhoneAccountHandle
760          * is registrated by Telecom and before CallLogProvider unhides them.
761          */
762         if (shouldConvertSubIdToIccIdForBackup(call.accountComponentName,
763                 call.isPhoneAccountMigrationPending)) {
764             Log.i(TAG, "Processing PhoneAccountMigration Backup accountId: " + call.accountId);
765             String iccId = null;
766             try {
767                 iccId = mSubscriptionInfoMap.get(Integer.parseInt(call.accountId));
768             } catch (NullPointerException e) {
769                 // Ignore, iccId will be null;
770             } catch(NumberFormatException e) {
771                 // Ignore, iccId will be null;
772             }
773 
774             if (iccId != null) {
775                 Log.i(TAG, "processing PhoneAccountMigration Found Subid during Backup: "
776                         + call.accountId);
777                 call.accountId = iccId;
778                 call.isPhoneAccountMigrationPending = 1;
779             }
780         }
781         return call;
782     }
783 
addCallToBackup(BackupDataOutput output, Call call)784     private void addCallToBackup(BackupDataOutput output, Call call) {
785         ByteArrayOutputStream baos = new ByteArrayOutputStream();
786         DataOutputStream data = new DataOutputStream(baos);
787 
788         try {
789             data.writeInt(VERSION);
790             data.writeLong(call.date);
791             data.writeLong(call.duration);
792             writeString(data, call.number);
793             data.writeInt(call.type);
794             data.writeInt(call.numberPresentation);
795             writeString(data, call.accountComponentName);
796             writeString(data, call.accountId);
797             writeString(data, call.accountAddress);
798             data.writeLong(call.dataUsage == null ? 0 : call.dataUsage);
799             data.writeInt(call.features);
800 
801             OEMData oemData = getOEMDataForCall(call);
802             data.writeUTF(oemData.namespace);
803             data.writeInt(oemData.bytes.length);
804             data.write(oemData.bytes);
805             data.writeInt(END_OEM_DATA_MARKER);
806 
807             data.writeInt(call.addForAllUsers);
808 
809             writeString(data, call.postDialDigits);
810 
811             writeString(data, call.viaNumber);
812 
813             data.writeInt(call.callBlockReason);
814             writeString(data, call.callScreeningAppName);
815             writeString(data, call.callScreeningComponentName);
816 
817             // Step 1007 used to write caller ID data; those were pulled.  Keeping that in here
818             // to maintain compatibility for backups which had this data.
819             writeString(data, "");
820             writeString(data, "");
821             writeString(data, "");
822             writeString(data, "");
823             writeString(data, "");
824             writeInteger(data, null);
825 
826             data.writeLong(call.missedReason);
827             data.writeInt(call.isPhoneAccountMigrationPending);
828 
829             data.writeInt(call.isBusinessCall);
830             writeString(data, call.assertedDisplayName);
831 
832             data.flush();
833 
834             output.writeEntityHeader(Integer.toString(call.id), baos.size());
835             output.writeEntityData(baos.toByteArray(), baos.size());
836 
837             mBackupRestoreEventLoggerProxy.logItemsBackedUp(CALLLOGS, /* count */ 1);
838 
839             if (isDebug()) {
840                 Log.d(TAG, "Wrote call to backup: " + call + " with byte array: " + baos);
841             }
842         } catch (Exception e) {
843             mBackupRestoreEventLoggerProxy.logItemsBackupFailed(
844                     CALLLOGS, /* count */ 1, ERROR_BACKUP_CALL_FAILED);
845             Log.e(TAG, "Failed to backup call: " + call, e);
846         }
847     }
848 
849     /**
850      * Allows OEMs to provide proprietary data to backup along with the rest of the call log
851      * data. Because there is no way to provide a Backup Transport implementation
852      * nor peek into the data format of backup entries without system-level permissions, it is
853      * not possible (at the time of this writing) to write CTS tests for this piece of code.
854      * It is, therefore, important that if you alter this portion of code that you
855      * test backup and restore of call log is working as expected; ideally this would be tested by
856      * backing up and restoring between two different Android phone devices running M+.
857      */
getOEMDataForCall(Call call)858     private OEMData getOEMDataForCall(Call call) {
859         return new OEMData(NO_OEM_NAMESPACE, ZERO_BYTE_ARRAY);
860 
861         // OEMs that want to add their own proprietary data to call log backup should replace the
862         // code above with their own namespace and add any additional data they need.
863         // Versioning and size-prefixing the data should be done here as needed.
864         //
865         // Example:
866 
867         /*
868         ByteArrayOutputStream baos = new ByteArrayOutputStream();
869         DataOutputStream data = new DataOutputStream(baos);
870 
871         String customData1 = "Generic OEM";
872         int customData2 = 42;
873 
874         // Write a version for the data
875         data.writeInt(OEM_DATA_VERSION);
876 
877         // Write the data and flush
878         data.writeUTF(customData1);
879         data.writeInt(customData2);
880         data.flush();
881 
882         String oemNamespace = "com.oem.namespace";
883         return new OEMData(oemNamespace, baos.toByteArray());
884         */
885     }
886 
887     /**
888      * Allows OEMs to read their own proprietary data when doing a call log restore. It is important
889      * that the implementation verify the namespace of the data matches their expected value before
890      * attempting to read the data or else you may risk reading invalid data.
891      *
892      * See {@link #getOEMDataForCall} for information concerning proper testing of this code.
893      */
readOEMDataForCall(Call call, OEMData oemData)894     private void readOEMDataForCall(Call call, OEMData oemData) {
895         // OEMs that want to read proprietary data from a call log restore should do so here.
896         // Before reading from the data, an OEM should verify that the data matches their
897         // expected namespace.
898         //
899         // Example:
900 
901         /*
902         if ("com.oem.expected.namespace".equals(oemData.namespace)) {
903             ByteArrayInputStream bais = new ByteArrayInputStream(oemData.bytes);
904             DataInputStream data = new DataInputStream(bais);
905 
906             // Check against this version as we read data.
907             int version = data.readInt();
908             String customData1 = data.readUTF();
909             int customData2 = data.readInt();
910             // do something with data
911         }
912         */
913     }
914 
915 
writeString(DataOutputStream data, String str)916     private void writeString(DataOutputStream data, String str) throws IOException {
917         if (str == null) {
918             data.writeBoolean(false);
919         } else {
920             data.writeBoolean(true);
921             data.writeUTF(str);
922         }
923     }
924 
readString(DataInputStream data)925     private String readString(DataInputStream data) throws IOException {
926         if (data.readBoolean()) {
927             return data.readUTF();
928         } else {
929             return null;
930         }
931     }
932 
writeInteger(DataOutputStream data, Integer num)933     private void writeInteger(DataOutputStream data, Integer num) throws IOException {
934         if (num == null) {
935             data.writeBoolean(false);
936         } else {
937             data.writeBoolean(true);
938             data.writeInt(num);
939         }
940     }
941 
readInteger(DataInputStream data)942     private Integer readInteger(DataInputStream data) throws IOException {
943         if (data.readBoolean()) {
944             return data.readInt();
945         } else {
946             return null;
947         }
948     }
949 
removeCallFromBackup(BackupDataOutput output, int callId)950     private void removeCallFromBackup(BackupDataOutput output, int callId) {
951         try {
952             output.writeEntityHeader(Integer.toString(callId), -1);
953         } catch (IOException e) {
954             Log.e(TAG, "Failed to remove call: " + callId, e);
955         }
956     }
957 
isDebug()958     private static boolean isDebug() {
959         return Log.isLoggable(TAG, Log.DEBUG);
960     }
961 }
962