/* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License */ package com.android.providers.calllogbackup; import android.app.backup.BackupAgent; import android.app.backup.BackupDataInput; import android.app.backup.BackupDataOutput; import android.content.ComponentName; import android.content.ContentResolver; import android.database.Cursor; import android.os.ParcelFileDescriptor; import android.os.UserHandle; import android.os.UserManager; import android.provider.CallLog; import android.provider.CallLog.Calls; import android.telecom.PhoneAccountHandle; import android.util.Log; import com.android.internal.annotations.VisibleForTesting; import java.io.BufferedOutputStream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.DataInput; import java.io.DataInputStream; import java.io.DataOutput; import java.io.DataOutputStream; import java.io.EOFException; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.util.LinkedList; import java.util.List; import java.util.SortedSet; import java.util.TreeSet; /** * Call log backup agent. */ public class CallLogBackupAgent extends BackupAgent { @VisibleForTesting static class CallLogBackupState { int version; SortedSet callIds; } @VisibleForTesting static class Call { int id; long date; long duration; String number; int type; int numberPresentation; String accountComponentName; String accountId; String accountAddress; Long dataUsage; int features; @Override public String toString() { if (isDebug()) { return "[" + id + ", account: [" + accountComponentName + " : " + accountId + "]," + number + ", " + date + "]"; } else { return "[" + id + "]"; } } } static class OEMData { String namespace; byte[] bytes; public OEMData(String namespace, byte[] bytes) { this.namespace = namespace; this.bytes = bytes == null ? ZERO_BYTE_ARRAY : bytes; } } private static final String TAG = "CallLogBackupAgent"; /** Current version of CallLogBackup. Used to track the backup format. */ @VisibleForTesting static final int VERSION = 1002; /** Version indicating that there exists no previous backup entry. */ @VisibleForTesting static final int VERSION_NO_PREVIOUS_STATE = 0; static final String NO_OEM_NAMESPACE = "no-oem-namespace"; static final byte[] ZERO_BYTE_ARRAY = new byte[0]; static final int END_OEM_DATA_MARKER = 0x60061E; private static final String[] CALL_LOG_PROJECTION = new String[] { CallLog.Calls._ID, CallLog.Calls.DATE, CallLog.Calls.DURATION, CallLog.Calls.NUMBER, CallLog.Calls.TYPE, CallLog.Calls.COUNTRY_ISO, CallLog.Calls.GEOCODED_LOCATION, CallLog.Calls.NUMBER_PRESENTATION, CallLog.Calls.PHONE_ACCOUNT_COMPONENT_NAME, CallLog.Calls.PHONE_ACCOUNT_ID, CallLog.Calls.PHONE_ACCOUNT_ADDRESS, CallLog.Calls.DATA_USAGE, CallLog.Calls.FEATURES }; /** ${inheritDoc} */ @Override public void onBackup(ParcelFileDescriptor oldStateDescriptor, BackupDataOutput data, ParcelFileDescriptor newStateDescriptor) throws IOException { // Get the list of the previous calls IDs which were backed up. DataInputStream dataInput = new DataInputStream( new FileInputStream(oldStateDescriptor.getFileDescriptor())); final CallLogBackupState state; try { state = readState(dataInput); } finally { dataInput.close(); } // Run the actual backup of data runBackup(state, data, getAllCallLogEntries()); // Rewrite the backup state. DataOutputStream dataOutput = new DataOutputStream(new BufferedOutputStream( new FileOutputStream(newStateDescriptor.getFileDescriptor()))); try { writeState(dataOutput, state); } finally { dataOutput.close(); } } /** ${inheritDoc} */ @Override public void onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState) throws IOException { if (isDebug()) { Log.d(TAG, "Performing Restore"); } while (data.readNextHeader()) { Call call = readCallFromData(data); if (call != null) { writeCallToProvider(call); if (isDebug()) { Log.d(TAG, "Restored call: " + call); } } } } @VisibleForTesting void runBackup(CallLogBackupState state, BackupDataOutput data, Iterable calls) { SortedSet callsToRemove = new TreeSet<>(state.callIds); // Loop through all the call log entries to identify: // (1) new calls // (2) calls which have been deleted. for (Call call : calls) { if (!state.callIds.contains(call.id)) { if (isDebug()) { Log.d(TAG, "Adding call to backup: " + call); } // This call new (not in our list from the last backup), lets back it up. addCallToBackup(data, call); state.callIds.add(call.id); } else { // This call still exists in the current call log so delete it from the // "callsToRemove" set since we want to keep it. callsToRemove.remove(call.id); } } // Remove calls which no longer exist in the set. for (Integer i : callsToRemove) { if (isDebug()) { Log.d(TAG, "Removing call from backup: " + i); } removeCallFromBackup(data, i); state.callIds.remove(i); } } private Iterable getAllCallLogEntries() { List calls = new LinkedList<>(); // We use the API here instead of querying ContactsDatabaseHelper directly because // CallLogProvider has special locks in place for sychronizing when to read. Using the APIs // gives us that for free. ContentResolver resolver = getContentResolver(); Cursor cursor = resolver.query( CallLog.Calls.CONTENT_URI, CALL_LOG_PROJECTION, null, null, null); if (cursor != null) { try { while (cursor.moveToNext()) { Call call = readCallFromCursor(cursor); if (call != null) { calls.add(call); } } } finally { cursor.close(); } } return calls; } private void writeCallToProvider(Call call) { Long dataUsage = call.dataUsage == 0 ? null : call.dataUsage; PhoneAccountHandle handle = null; if (call.accountComponentName != null && call.accountId != null) { handle = new PhoneAccountHandle( ComponentName.unflattenFromString(call.accountComponentName), call.accountId); } Calls.addCall(null /* CallerInfo */, this, call.number, call.numberPresentation, call.type, call.features, handle, call.date, (int) call.duration, dataUsage, true /* addForAllUsers */); } @VisibleForTesting CallLogBackupState readState(DataInput dataInput) throws IOException { CallLogBackupState state = new CallLogBackupState(); state.callIds = new TreeSet<>(); try { // Read the version. state.version = dataInput.readInt(); if (state.version >= 1) { // Read the size. int size = dataInput.readInt(); // Read all of the call IDs. for (int i = 0; i < size; i++) { state.callIds.add(dataInput.readInt()); } } } catch (EOFException e) { state.version = VERSION_NO_PREVIOUS_STATE; } return state; } @VisibleForTesting void writeState(DataOutput dataOutput, CallLogBackupState state) throws IOException { // Write version first of all dataOutput.writeInt(VERSION); // [Version 1] // size + callIds dataOutput.writeInt(state.callIds.size()); for (Integer i : state.callIds) { dataOutput.writeInt(i); } } @VisibleForTesting Call readCallFromData(BackupDataInput data) { final int callId; try { callId = Integer.parseInt(data.getKey()); } catch (NumberFormatException e) { Log.e(TAG, "Unexpected key found in restore: " + data.getKey()); return null; } try { byte [] byteArray = new byte[data.getDataSize()]; data.readEntityData(byteArray, 0, byteArray.length); DataInputStream dataInput = new DataInputStream(new ByteArrayInputStream(byteArray)); Call call = new Call(); call.id = callId; int version = dataInput.readInt(); if (version >= 1) { call.date = dataInput.readLong(); call.duration = dataInput.readLong(); call.number = readString(dataInput); call.type = dataInput.readInt(); call.numberPresentation = dataInput.readInt(); call.accountComponentName = readString(dataInput); call.accountId = readString(dataInput); call.accountAddress = readString(dataInput); call.dataUsage = dataInput.readLong(); call.features = dataInput.readInt(); } if (version >= 1002) { String namespace = dataInput.readUTF(); int length = dataInput.readInt(); byte[] buffer = new byte[length]; dataInput.read(buffer); readOEMDataForCall(call, new OEMData(namespace, buffer)); int marker = dataInput.readInt(); if (marker != END_OEM_DATA_MARKER) { Log.e(TAG, "Did not find END-OEM marker for call " + call.id); // The marker does not match the expected value, ignore this call completely. return null; } } return call; } catch (IOException e) { Log.e(TAG, "Error reading call data for " + callId, e); return null; } } private Call readCallFromCursor(Cursor cursor) { Call call = new Call(); call.id = cursor.getInt(cursor.getColumnIndex(CallLog.Calls._ID)); call.date = cursor.getLong(cursor.getColumnIndex(CallLog.Calls.DATE)); call.duration = cursor.getLong(cursor.getColumnIndex(CallLog.Calls.DURATION)); call.number = cursor.getString(cursor.getColumnIndex(CallLog.Calls.NUMBER)); call.type = cursor.getInt(cursor.getColumnIndex(CallLog.Calls.TYPE)); call.numberPresentation = cursor.getInt(cursor.getColumnIndex(CallLog.Calls.NUMBER_PRESENTATION)); call.accountComponentName = cursor.getString(cursor.getColumnIndex(CallLog.Calls.PHONE_ACCOUNT_COMPONENT_NAME)); call.accountId = cursor.getString(cursor.getColumnIndex(CallLog.Calls.PHONE_ACCOUNT_ID)); call.accountAddress = cursor.getString(cursor.getColumnIndex(CallLog.Calls.PHONE_ACCOUNT_ADDRESS)); call.dataUsage = cursor.getLong(cursor.getColumnIndex(CallLog.Calls.DATA_USAGE)); call.features = cursor.getInt(cursor.getColumnIndex(CallLog.Calls.FEATURES)); return call; } private void addCallToBackup(BackupDataOutput output, Call call) { ByteArrayOutputStream baos = new ByteArrayOutputStream(); DataOutputStream data = new DataOutputStream(baos); try { data.writeInt(VERSION); data.writeLong(call.date); data.writeLong(call.duration); writeString(data, call.number); data.writeInt(call.type); data.writeInt(call.numberPresentation); writeString(data, call.accountComponentName); writeString(data, call.accountId); writeString(data, call.accountAddress); data.writeLong(call.dataUsage == null ? 0 : call.dataUsage); data.writeInt(call.features); OEMData oemData = getOEMDataForCall(call); data.writeUTF(oemData.namespace); data.writeInt(oemData.bytes.length); data.write(oemData.bytes); data.writeInt(END_OEM_DATA_MARKER); data.flush(); output.writeEntityHeader(Integer.toString(call.id), baos.size()); output.writeEntityData(baos.toByteArray(), baos.size()); if (isDebug()) { Log.d(TAG, "Wrote call to backup: " + call + " with byte array: " + baos); } } catch (IOException e) { Log.e(TAG, "Failed to backup call: " + call, e); } } /** * Allows OEMs to provide proprietary data to backup along with the rest of the call log * data. Because there is no way to provide a Backup Transport implementation * nor peek into the data format of backup entries without system-level permissions, it is * not possible (at the time of this writing) to write CTS tests for this piece of code. * It is, therefore, important that if you alter this portion of code that you * test backup and restore of call log is working as expected; ideally this would be tested by * backing up and restoring between two different Android phone devices running M+. */ private OEMData getOEMDataForCall(Call call) { return new OEMData(NO_OEM_NAMESPACE, ZERO_BYTE_ARRAY); // OEMs that want to add their own proprietary data to call log backup should replace the // code above with their own namespace and add any additional data they need. // Versioning and size-prefixing the data should be done here as needed. // // Example: /* ByteArrayOutputStream baos = new ByteArrayOutputStream(); DataOutputStream data = new DataOutputStream(baos); String customData1 = "Generic OEM"; int customData2 = 42; // Write a version for the data data.writeInt(OEM_DATA_VERSION); // Write the data and flush data.writeUTF(customData1); data.writeInt(customData2); data.flush(); String oemNamespace = "com.oem.namespace"; return new OEMData(oemNamespace, baos.toByteArray()); */ } /** * Allows OEMs to read their own proprietary data when doing a call log restore. It is important * that the implementation verify the namespace of the data matches their expected value before * attempting to read the data or else you may risk reading invalid data. * * See {@link #getOEMDataForCall} for information concerning proper testing of this code. */ private void readOEMDataForCall(Call call, OEMData oemData) { // OEMs that want to read proprietary data from a call log restore should do so here. // Before reading from the data, an OEM should verify that the data matches their // expected namespace. // // Example: /* if ("com.oem.expected.namespace".equals(oemData.namespace)) { ByteArrayInputStream bais = new ByteArrayInputStream(oemData.bytes); DataInputStream data = new DataInputStream(bais); // Check against this version as we read data. int version = data.readInt(); String customData1 = data.readUTF(); int customData2 = data.readInt(); // do something with data } */ } private void writeString(DataOutputStream data, String str) throws IOException { if (str == null) { data.writeBoolean(false); } else { data.writeBoolean(true); data.writeUTF(str); } } private String readString(DataInputStream data) throws IOException { if (data.readBoolean()) { return data.readUTF(); } else { return null; } } private void removeCallFromBackup(BackupDataOutput output, int callId) { try { output.writeEntityHeader(Integer.toString(callId), -1); } catch (IOException e) { Log.e(TAG, "Failed to remove call: " + callId, e); } } private static boolean isDebug() { return Log.isLoggable(TAG, Log.DEBUG); } }