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         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
    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 
    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 
    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
    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
    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
    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 
    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 
    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      */
    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      */
    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 
    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 
    521     private String readString(DataInputStream data) throws IOException {
    522         if (data.readBoolean()) {
    523             return data.readUTF();
    524         } else {
    525             return null;
    526         }
    527     }
    528 
    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 
    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 
    543     private static boolean isDebug() {
    544         return Log.isLoggable(TAG, Log.DEBUG);
    545     }
    546 }
    547