Home | History | Annotate | Download | only in calllogbackup
      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
     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 
     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
    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
    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 && call.type != Calls.VOICEMAIL_TYPE) {
    185                 writeCallToProvider(call);
    186                 if (isDebug()) {
    187                     Log.d(TAG, "Restored call: " + call);
    188                 }
    189             }
    190         }
    191     }
    192 
    193     @VisibleForTesting
    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 
    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 && call.type != Calls.VOICEMAIL_TYPE) {
    242                         calls.add(call);
    243                     }
    244                 }
    245             } finally {
    246                 cursor.close();
    247             }
    248         }
    249 
    250         return calls;
    251     }
    252 
    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
    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
    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
    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 
    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 
    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      */
    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      */
    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 
    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 
    515     private String readString(DataInputStream data) throws IOException {
    516         if (data.readBoolean()) {
    517             return data.readUTF();
    518         } else {
    519             return null;
    520         }
    521     }
    522 
    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 
    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 
    537     private static boolean isDebug() {
    538         return Log.isLoggable(TAG, Log.DEBUG);
    539     }
    540 }
    541