| /* |
| * 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.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.content.Context; |
| import android.database.Cursor; |
| import android.os.ParcelFileDescriptor; |
| import android.provider.CallLog; |
| import android.provider.CallLog.Calls; |
| import android.provider.Settings; |
| 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<Integer> callIds; |
| } |
| |
| @VisibleForTesting |
| static class Call { |
| int id; |
| long date; |
| long duration; |
| String number; |
| String postDialDigits = ""; |
| String viaNumber = ""; |
| int type; |
| int numberPresentation; |
| String accountComponentName; |
| String accountId; |
| String accountAddress; |
| Long dataUsage; |
| int features; |
| int addForAllUsers = 1; |
| int callBlockReason = Calls.BLOCK_REASON_NOT_BLOCKED; |
| String callScreeningAppName = null; |
| String callScreeningComponentName = null; |
| |
| @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"; |
| |
| private static final String USER_FULL_DATA_BACKUP_AWARE = "user_full_data_backup_aware"; |
| |
| /** Current version of CallLogBackup. Used to track the backup format. */ |
| @VisibleForTesting |
| static final int VERSION = 1007; |
| /** 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.POST_DIAL_DIGITS, |
| CallLog.Calls.VIA_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, |
| CallLog.Calls.ADD_FOR_ALL_USERS, |
| CallLog.Calls.BLOCK_REASON, |
| CallLog.Calls.CALL_SCREENING_APP_NAME, |
| CallLog.Calls.CALL_SCREENING_COMPONENT_NAME |
| }; |
| |
| /** ${inheritDoc} */ |
| @Override |
| public void onBackup(ParcelFileDescriptor oldStateDescriptor, BackupDataOutput data, |
| ParcelFileDescriptor newStateDescriptor) throws IOException { |
| |
| if (shouldPreventBackup(this)) { |
| if (isDebug()) { |
| Log.d(TAG, "Skipping onBackup"); |
| } |
| return; |
| } |
| |
| // 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 && call.type != Calls.VOICEMAIL_TYPE) { |
| writeCallToProvider(call); |
| if (isDebug()) { |
| Log.d(TAG, "Restored call: " + call); |
| } |
| } |
| } |
| } |
| |
| @VisibleForTesting |
| void runBackup(CallLogBackupState state, BackupDataOutput data, Iterable<Call> calls) { |
| SortedSet<Integer> 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<Call> getAllCallLogEntries() { |
| List<Call> 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 && call.type != Calls.VOICEMAIL_TYPE) { |
| 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); |
| } |
| boolean addForAllUsers = call.addForAllUsers == 1; |
| // We backup the calllog in the user running this backup agent, so write calls to this user. |
| Calls.addCall(null /* CallerInfo */, this, call.number, call.postDialDigits, call.viaNumber, |
| call.numberPresentation, call.type, call.features, handle, call.date, |
| (int) call.duration, dataUsage, addForAllUsers, null, true /* isRead */, |
| call.callBlockReason /*callBlockReason*/, |
| call.callScreeningAppName /*callScreeningAppName*/, |
| call.callScreeningComponentName /*callScreeningComponentName*/); |
| } |
| |
| @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; |
| } |
| } |
| |
| if (version >= 1003) { |
| call.addForAllUsers = dataInput.readInt(); |
| } |
| |
| if (version >= 1004) { |
| call.postDialDigits = readString(dataInput); |
| } |
| |
| if(version >= 1005) { |
| call.viaNumber = readString(dataInput); |
| } |
| |
| if(version >= 1006) { |
| call.callBlockReason = dataInput.readInt(); |
| call.callScreeningAppName = readString(dataInput); |
| call.callScreeningComponentName = readString(dataInput); |
| } |
| if(version >= 1007) { |
| // Version 1007 had call id columns early in the Q release; they were pulled so we |
| // will just read the values out here if they exist in a backup and ignore them. |
| readString(dataInput); |
| readString(dataInput); |
| readString(dataInput); |
| readString(dataInput); |
| readString(dataInput); |
| readInteger(dataInput); |
| } |
| 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.postDialDigits = cursor.getString( |
| cursor.getColumnIndex(CallLog.Calls.POST_DIAL_DIGITS)); |
| call.viaNumber = cursor.getString(cursor.getColumnIndex(CallLog.Calls.VIA_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)); |
| call.addForAllUsers = cursor.getInt(cursor.getColumnIndex(Calls.ADD_FOR_ALL_USERS)); |
| call.callBlockReason = cursor.getInt(cursor.getColumnIndex(CallLog.Calls.BLOCK_REASON)); |
| call.callScreeningAppName = cursor |
| .getString(cursor.getColumnIndex(CallLog.Calls.CALL_SCREENING_APP_NAME)); |
| call.callScreeningComponentName = cursor |
| .getString(cursor.getColumnIndex(CallLog.Calls.CALL_SCREENING_COMPONENT_NAME)); |
| 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.writeInt(call.addForAllUsers); |
| |
| writeString(data, call.postDialDigits); |
| |
| writeString(data, call.viaNumber); |
| |
| data.writeInt(call.callBlockReason); |
| writeString(data, call.callScreeningAppName); |
| writeString(data, call.callScreeningComponentName); |
| |
| // Step 1007 used to write caller ID data; those were pulled. Keeping that in here |
| // to maintain compatibility for backups which had this data. |
| writeString(data, ""); |
| writeString(data, ""); |
| writeString(data, ""); |
| writeString(data, ""); |
| writeString(data, ""); |
| writeInteger(data, null); |
| |
| 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 writeInteger(DataOutputStream data, Integer num) throws IOException { |
| if (num == null) { |
| data.writeBoolean(false); |
| } else { |
| data.writeBoolean(true); |
| data.writeInt(num); |
| } |
| } |
| |
| private Integer readInteger(DataInputStream data) throws IOException { |
| if (data.readBoolean()) { |
| return data.readInt(); |
| } 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); |
| } |
| } |
| |
| static boolean shouldPreventBackup(Context context) { |
| // Check to see that the user is full-data aware before performing calllog backup. |
| return Settings.Secure.getInt( |
| context.getContentResolver(), USER_FULL_DATA_BACKUP_AWARE, 0) == 0; |
| } |
| |
| private static boolean isDebug() { |
| return Log.isLoggable(TAG, Log.DEBUG); |
| } |
| } |