From 349e0f4b527d73ed575d4c33eed1e99599e5acf6 Mon Sep 17 00:00:00 2001 From: Santos Cordon Date: Fri, 9 Oct 2015 12:50:27 -0700 Subject: Add a check for full-memory-backup user awareness. Also, update the package name of CallLogBackupAgent. Bug: 24783945 Change-Id: Iccf88af058260bc94ac6837868c4da692427f464 --- Android.mk | 2 +- AndroidManifest.xml | 4 +- .../android/calllogbackup/CallLogBackupAgent.java | 517 +++++++++++++++++++++ .../calllogbackup/CallLogChangeReceiver.java | 52 +++ .../calllogbackup/CallLogBackupAgent.java | 492 -------------------- .../calllogbackup/CallLogChangeReceiver.java | 41 -- tests/AndroidManifest.xml | 6 +- .../calllogbackup/CallLogBackupAgentTest.java | 230 +++++++++ .../com/android/calllogbackup/MockitoHelper.java | 53 +++ .../calllogbackup/CallLogBackupAgentTest.java | 230 --------- .../providers/calllogbackup/MockitoHelper.java | 53 --- 11 files changed, 858 insertions(+), 822 deletions(-) create mode 100644 src/com/android/calllogbackup/CallLogBackupAgent.java create mode 100644 src/com/android/calllogbackup/CallLogChangeReceiver.java delete mode 100644 src/com/android/providers/calllogbackup/CallLogBackupAgent.java delete mode 100644 src/com/android/providers/calllogbackup/CallLogChangeReceiver.java create mode 100644 tests/src/com/android/calllogbackup/CallLogBackupAgentTest.java create mode 100644 tests/src/com/android/calllogbackup/MockitoHelper.java delete mode 100644 tests/src/com/android/providers/calllogbackup/CallLogBackupAgentTest.java delete mode 100644 tests/src/com/android/providers/calllogbackup/MockitoHelper.java diff --git a/Android.mk b/Android.mk index b2681b2..356e001 100644 --- a/Android.mk +++ b/Android.mk @@ -11,7 +11,7 @@ LOCAL_SRC_FILES := $(call all-java-files-under, src) # leaving out code which is tested by other means (e.g. static libraries) that # would dilute the coverage results. These options do not affect regular # production builds. -LOCAL_EMMA_COVERAGE_FILTER := +com.android.providers.calllogbackup.* +LOCAL_EMMA_COVERAGE_FILTER := +com.android.calllogbackup.* LOCAL_PACKAGE_NAME := CallLogBackup LOCAL_CERTIFICATE := shared diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 1ee54f0..2a1aadb 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -1,5 +1,5 @@ @@ -10,7 +10,7 @@ android:usesCleartextTraffic="false"> + android:value="AEdPqrEAAAAIVhVYJjcc4bozis7qBfzzgREFk3nIkWGNc5VaRg" /> diff --git a/src/com/android/calllogbackup/CallLogBackupAgent.java b/src/com/android/calllogbackup/CallLogBackupAgent.java new file mode 100644 index 0000000..a6f7c85 --- /dev/null +++ b/src/com/android/calllogbackup/CallLogBackupAgent.java @@ -0,0 +1,517 @@ +/* + * 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.os.UserHandle; +import android.os.UserManager; +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 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"; + + 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 = 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 { + + 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 (shouldPreventBackup(this)) { + if (isDebug()) { + Log.d(TAG, "Skipping restore"); + } + return; + } + + 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 */, true /* is_read */); + } + + @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); + } + } + + 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); + } +} diff --git a/src/com/android/calllogbackup/CallLogChangeReceiver.java b/src/com/android/calllogbackup/CallLogChangeReceiver.java new file mode 100644 index 0000000..eaebe5f --- /dev/null +++ b/src/com/android/calllogbackup/CallLogChangeReceiver.java @@ -0,0 +1,52 @@ +/* + * 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.BackupManager; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.provider.Settings; +import android.util.Log; + +/** + * Call Log Change Broadcast Receiver. Receives an intent when the call log provider changes + * so that it triggers backup accordingly. + */ +public class CallLogChangeReceiver extends BroadcastReceiver { + private static final String TAG = "CallLogChangeReceiver"; + private static final boolean VDBG = Log.isLoggable(TAG, Log.VERBOSE); + private static final String ACTION_CALL_LOG_CHANGE = "android.intent.action.CALL_LOG_CHANGE"; + + /** ${inheritDoc} */ + @Override + public void onReceive(Context context, Intent intent) { + if (ACTION_CALL_LOG_CHANGE.equals(intent.getAction())) { + + if (CallLogBackupAgent.shouldPreventBackup(context)) { + // User is not full-backup-data aware so we skip calllog backup until they are. + if (VDBG) { + Log.v(TAG, "Skipping call log backup due to lack of full-data check."); + } + } else { + BackupManager bm = new BackupManager(context); + bm.dataChanged(); + } + } + } + +} diff --git a/src/com/android/providers/calllogbackup/CallLogBackupAgent.java b/src/com/android/providers/calllogbackup/CallLogBackupAgent.java deleted file mode 100644 index f939d8c..0000000 --- a/src/com/android/providers/calllogbackup/CallLogBackupAgent.java +++ /dev/null @@ -1,492 +0,0 @@ -/* - * 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 */, true /* is_read */); - } - - @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); - } -} diff --git a/src/com/android/providers/calllogbackup/CallLogChangeReceiver.java b/src/com/android/providers/calllogbackup/CallLogChangeReceiver.java deleted file mode 100644 index dbd7b00..0000000 --- a/src/com/android/providers/calllogbackup/CallLogChangeReceiver.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * 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.BackupManager; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; - -/** - * Call Log Change Broadcast Receiver. Receives an intent when the call log provider changes - * so that it triggers backup accordingly. - */ -public class CallLogChangeReceiver extends BroadcastReceiver { - - private static final String ACTION_CALL_LOG_CHANGE = "android.intent.action.CALL_LOG_CHANGE"; - - /** ${inheritDoc} */ - @Override - public void onReceive(Context context, Intent intent) { - if (ACTION_CALL_LOG_CHANGE.equals(intent.getAction())) { - BackupManager bm = new BackupManager(context); - bm.dataChanged(); - } - } - -} diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml index 285bb46..357df4f 100644 --- a/tests/AndroidManifest.xml +++ b/tests/AndroidManifest.xml @@ -15,7 +15,7 @@ --> @@ -27,10 +27,10 @@ all other applications via the command: "adb shell itr". The "itr" command will find all tests declared by all applications. If you want to run just these tests on their own then use the command: - "adb shell am instrument -w com.android.providers.calllogbackup.tests/android.test.InstrumentationTestRunner" + "adb shell am instrument -w com.android.calllogbackup.tests/android.test.InstrumentationTestRunner" --> diff --git a/tests/src/com/android/calllogbackup/CallLogBackupAgentTest.java b/tests/src/com/android/calllogbackup/CallLogBackupAgentTest.java new file mode 100644 index 0000000..4564195 --- /dev/null +++ b/tests/src/com/android/calllogbackup/CallLogBackupAgentTest.java @@ -0,0 +1,230 @@ +/* + * 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 static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.eq; + +import android.app.backup.BackupDataOutput; +import android.test.AndroidTestCase; +import android.test.suitebuilder.annotation.SmallTest; + +import com.android.calllogbackup.CallLogBackupAgent.Call; +import com.android.calllogbackup.CallLogBackupAgent.CallLogBackupState; + +import org.mockito.InOrder; +import org.mockito.Matchers; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.EOFException; +import java.util.LinkedList; +import java.util.List; +import java.util.TreeSet; + +/** + * Test cases for {@link com.android.providers.contacts.CallLogBackupAgent} + */ +@SmallTest +public class CallLogBackupAgentTest extends AndroidTestCase { + + @Mock DataInput mDataInput; + @Mock DataOutput mDataOutput; + @Mock BackupDataOutput mBackupDataOutput; + + CallLogBackupAgent mCallLogBackupAgent; + + MockitoHelper mMockitoHelper = new MockitoHelper(); + + @Override + public void setUp() throws Exception { + super.setUp(); + + mMockitoHelper.setUp(getClass()); + // Since we're testing a system app, AppDataDirGuesser doesn't find our + // cache dir, so set it explicitly. + System.setProperty("dexmaker.dexcache", getContext().getCacheDir().toString()); + + MockitoAnnotations.initMocks(this); + + mCallLogBackupAgent = new CallLogBackupAgent(); + } + + @Override + public void tearDown() throws Exception { + mMockitoHelper.tearDown(); + } + + public void testReadState_NoCall() throws Exception { + when(mDataInput.readInt()).thenThrow(new EOFException()); + + CallLogBackupState state = mCallLogBackupAgent.readState(mDataInput); + + assertEquals(state.version, CallLogBackupAgent.VERSION_NO_PREVIOUS_STATE); + assertEquals(state.callIds.size(), 0); + } + + public void testReadState_OneCall() throws Exception { + when(mDataInput.readInt()).thenReturn( + 1 /* version */, + 1 /* size */, + 101 /* call-ID */ ); + + CallLogBackupState state = mCallLogBackupAgent.readState(mDataInput); + + assertEquals(1, state.version); + assertEquals(1, state.callIds.size()); + assertTrue(state.callIds.contains(101)); + } + + public void testReadState_MultipleCalls() throws Exception { + when(mDataInput.readInt()).thenReturn( + 1 /* version */, + 2 /* size */, + 101 /* call-ID */, + 102 /* call-ID */); + + CallLogBackupState state = mCallLogBackupAgent.readState(mDataInput); + + assertEquals(1, state.version); + assertEquals(2, state.callIds.size()); + assertTrue(state.callIds.contains(101)); + assertTrue(state.callIds.contains(102)); + } + + public void testWriteState_NoCalls() throws Exception { + CallLogBackupState state = new CallLogBackupState(); + state.version = CallLogBackupAgent.VERSION; + state.callIds = new TreeSet<>(); + + mCallLogBackupAgent.writeState(mDataOutput, state); + + InOrder inOrder = Mockito.inOrder(mDataOutput); + inOrder.verify(mDataOutput).writeInt(CallLogBackupAgent.VERSION); + inOrder.verify(mDataOutput).writeInt(0 /* size */); + } + + public void testWriteState_OneCall() throws Exception { + CallLogBackupState state = new CallLogBackupState(); + state.version = CallLogBackupAgent.VERSION; + state.callIds = new TreeSet<>(); + state.callIds.add(101); + + mCallLogBackupAgent.writeState(mDataOutput, state); + + InOrder inOrder = Mockito.inOrder(mDataOutput); + inOrder.verify(mDataOutput).writeInt(CallLogBackupAgent.VERSION); + inOrder.verify(mDataOutput).writeInt(1); + inOrder.verify(mDataOutput).writeInt(101 /* call-ID */); + } + + public void testWriteState_MultipleCalls() throws Exception { + CallLogBackupState state = new CallLogBackupState(); + state.version = CallLogBackupAgent.VERSION; + state.callIds = new TreeSet<>(); + state.callIds.add(101); + state.callIds.add(102); + state.callIds.add(103); + + mCallLogBackupAgent.writeState(mDataOutput, state); + + InOrder inOrder = Mockito.inOrder(mDataOutput); + inOrder.verify(mDataOutput).writeInt(CallLogBackupAgent.VERSION); + inOrder.verify(mDataOutput).writeInt(3 /* size */); + inOrder.verify(mDataOutput).writeInt(101 /* call-ID */); + inOrder.verify(mDataOutput).writeInt(102 /* call-ID */); + inOrder.verify(mDataOutput).writeInt(103 /* call-ID */); + } + + public void testRunBackup_NoCalls() throws Exception { + CallLogBackupState state = new CallLogBackupState(); + state.version = CallLogBackupAgent.VERSION; + state.callIds = new TreeSet<>(); + List calls = new LinkedList<>(); + + mCallLogBackupAgent.runBackup(state, mBackupDataOutput, calls); + + Mockito.verifyNoMoreInteractions(mBackupDataOutput); + } + + public void testRunBackup_OneNewCall() throws Exception { + CallLogBackupState state = new CallLogBackupState(); + state.version = CallLogBackupAgent.VERSION; + state.callIds = new TreeSet<>(); + List calls = new LinkedList<>(); + calls.add(makeCall(101, 0L, 0L, "555-5555")); + mCallLogBackupAgent.runBackup(state, mBackupDataOutput, calls); + + verify(mBackupDataOutput).writeEntityHeader(eq("101"), Matchers.anyInt()); + verify(mBackupDataOutput).writeEntityData((byte[]) Matchers.any(), Matchers.anyInt()); + } + + public void testRunBackup_MultipleCall() throws Exception { + CallLogBackupState state = new CallLogBackupState(); + state.version = CallLogBackupAgent.VERSION; + state.callIds = new TreeSet<>(); + List calls = new LinkedList<>(); + calls.add(makeCall(101, 0L, 0L, "555-1234")); + calls.add(makeCall(102, 0L, 0L, "555-5555")); + + mCallLogBackupAgent.runBackup(state, mBackupDataOutput, calls); + + InOrder inOrder = Mockito.inOrder(mBackupDataOutput); + inOrder.verify(mBackupDataOutput).writeEntityHeader(eq("101"), Matchers.anyInt()); + inOrder.verify(mBackupDataOutput). + writeEntityData((byte[]) Matchers.any(), Matchers.anyInt()); + inOrder.verify(mBackupDataOutput).writeEntityHeader(eq("102"), Matchers.anyInt()); + inOrder.verify(mBackupDataOutput). + writeEntityData((byte[]) Matchers.any(), Matchers.anyInt()); + } + + public void testRunBackup_PartialMultipleCall() throws Exception { + CallLogBackupState state = new CallLogBackupState(); + + state.version = CallLogBackupAgent.VERSION; + state.callIds = new TreeSet<>(); + state.callIds.add(101); + + List calls = new LinkedList<>(); + calls.add(makeCall(101, 0L, 0L, "555-1234")); + calls.add(makeCall(102, 0L, 0L, "555-5555")); + + mCallLogBackupAgent.runBackup(state, mBackupDataOutput, calls); + + InOrder inOrder = Mockito.inOrder(mBackupDataOutput); + inOrder.verify(mBackupDataOutput).writeEntityHeader(eq("102"), Matchers.anyInt()); + inOrder.verify(mBackupDataOutput). + writeEntityData((byte[]) Matchers.any(), Matchers.anyInt()); + } + + private static Call makeCall(int id, long date, long duration, String number) { + Call c = new Call(); + c.id = id; + c.date = date; + c.duration = duration; + c.number = number; + c.accountComponentName = "account-component"; + c.accountId = "account-id"; + return c; + } + +} diff --git a/tests/src/com/android/calllogbackup/MockitoHelper.java b/tests/src/com/android/calllogbackup/MockitoHelper.java new file mode 100644 index 0000000..25407d0 --- /dev/null +++ b/tests/src/com/android/calllogbackup/MockitoHelper.java @@ -0,0 +1,53 @@ +/* + * 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.util.Log; + +/** + * Helper for Mockito-based test cases. + */ +public final class MockitoHelper { + private static final String TAG = "MockitoHelper"; + + private ClassLoader mOriginalClassLoader; + private Thread mContextThread; + + /** + * Creates a new helper, which in turn will set the context classloader so + * it can load Mockito resources. + * + * @param packageClass test case class + */ + public void setUp(Class packageClass) throws Exception { + // makes a copy of the context classloader + mContextThread = Thread.currentThread(); + mOriginalClassLoader = mContextThread.getContextClassLoader(); + ClassLoader newClassLoader = packageClass.getClassLoader(); + Log.v(TAG, "Changing context classloader from " + mOriginalClassLoader + + " to " + newClassLoader); + mContextThread.setContextClassLoader(newClassLoader); + } + + /** + * Restores the context classloader to the previous value. + */ + public void tearDown() throws Exception { + Log.v(TAG, "Restoring context classloader to " + mOriginalClassLoader); + mContextThread.setContextClassLoader(mOriginalClassLoader); + } +} diff --git a/tests/src/com/android/providers/calllogbackup/CallLogBackupAgentTest.java b/tests/src/com/android/providers/calllogbackup/CallLogBackupAgentTest.java deleted file mode 100644 index 53ad162..0000000 --- a/tests/src/com/android/providers/calllogbackup/CallLogBackupAgentTest.java +++ /dev/null @@ -1,230 +0,0 @@ -/* - * 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 static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; -import static org.mockito.Mockito.eq; - -import android.app.backup.BackupDataOutput; -import android.test.AndroidTestCase; -import android.test.suitebuilder.annotation.SmallTest; - -import com.android.providers.calllogbackup.CallLogBackupAgent.Call; -import com.android.providers.calllogbackup.CallLogBackupAgent.CallLogBackupState; - -import org.mockito.InOrder; -import org.mockito.Matchers; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.MockitoAnnotations; - -import java.io.DataInput; -import java.io.DataOutput; -import java.io.EOFException; -import java.util.LinkedList; -import java.util.List; -import java.util.TreeSet; - -/** - * Test cases for {@link com.android.providers.contacts.CallLogBackupAgent} - */ -@SmallTest -public class CallLogBackupAgentTest extends AndroidTestCase { - - @Mock DataInput mDataInput; - @Mock DataOutput mDataOutput; - @Mock BackupDataOutput mBackupDataOutput; - - CallLogBackupAgent mCallLogBackupAgent; - - MockitoHelper mMockitoHelper = new MockitoHelper(); - - @Override - public void setUp() throws Exception { - super.setUp(); - - mMockitoHelper.setUp(getClass()); - // Since we're testing a system app, AppDataDirGuesser doesn't find our - // cache dir, so set it explicitly. - System.setProperty("dexmaker.dexcache", getContext().getCacheDir().toString()); - - MockitoAnnotations.initMocks(this); - - mCallLogBackupAgent = new CallLogBackupAgent(); - } - - @Override - public void tearDown() throws Exception { - mMockitoHelper.tearDown(); - } - - public void testReadState_NoCall() throws Exception { - when(mDataInput.readInt()).thenThrow(new EOFException()); - - CallLogBackupState state = mCallLogBackupAgent.readState(mDataInput); - - assertEquals(state.version, CallLogBackupAgent.VERSION_NO_PREVIOUS_STATE); - assertEquals(state.callIds.size(), 0); - } - - public void testReadState_OneCall() throws Exception { - when(mDataInput.readInt()).thenReturn( - 1 /* version */, - 1 /* size */, - 101 /* call-ID */ ); - - CallLogBackupState state = mCallLogBackupAgent.readState(mDataInput); - - assertEquals(1, state.version); - assertEquals(1, state.callIds.size()); - assertTrue(state.callIds.contains(101)); - } - - public void testReadState_MultipleCalls() throws Exception { - when(mDataInput.readInt()).thenReturn( - 1 /* version */, - 2 /* size */, - 101 /* call-ID */, - 102 /* call-ID */); - - CallLogBackupState state = mCallLogBackupAgent.readState(mDataInput); - - assertEquals(1, state.version); - assertEquals(2, state.callIds.size()); - assertTrue(state.callIds.contains(101)); - assertTrue(state.callIds.contains(102)); - } - - public void testWriteState_NoCalls() throws Exception { - CallLogBackupState state = new CallLogBackupState(); - state.version = CallLogBackupAgent.VERSION; - state.callIds = new TreeSet<>(); - - mCallLogBackupAgent.writeState(mDataOutput, state); - - InOrder inOrder = Mockito.inOrder(mDataOutput); - inOrder.verify(mDataOutput).writeInt(CallLogBackupAgent.VERSION); - inOrder.verify(mDataOutput).writeInt(0 /* size */); - } - - public void testWriteState_OneCall() throws Exception { - CallLogBackupState state = new CallLogBackupState(); - state.version = CallLogBackupAgent.VERSION; - state.callIds = new TreeSet<>(); - state.callIds.add(101); - - mCallLogBackupAgent.writeState(mDataOutput, state); - - InOrder inOrder = Mockito.inOrder(mDataOutput); - inOrder.verify(mDataOutput).writeInt(CallLogBackupAgent.VERSION); - inOrder.verify(mDataOutput).writeInt(1); - inOrder.verify(mDataOutput).writeInt(101 /* call-ID */); - } - - public void testWriteState_MultipleCalls() throws Exception { - CallLogBackupState state = new CallLogBackupState(); - state.version = CallLogBackupAgent.VERSION; - state.callIds = new TreeSet<>(); - state.callIds.add(101); - state.callIds.add(102); - state.callIds.add(103); - - mCallLogBackupAgent.writeState(mDataOutput, state); - - InOrder inOrder = Mockito.inOrder(mDataOutput); - inOrder.verify(mDataOutput).writeInt(CallLogBackupAgent.VERSION); - inOrder.verify(mDataOutput).writeInt(3 /* size */); - inOrder.verify(mDataOutput).writeInt(101 /* call-ID */); - inOrder.verify(mDataOutput).writeInt(102 /* call-ID */); - inOrder.verify(mDataOutput).writeInt(103 /* call-ID */); - } - - public void testRunBackup_NoCalls() throws Exception { - CallLogBackupState state = new CallLogBackupState(); - state.version = CallLogBackupAgent.VERSION; - state.callIds = new TreeSet<>(); - List calls = new LinkedList<>(); - - mCallLogBackupAgent.runBackup(state, mBackupDataOutput, calls); - - Mockito.verifyNoMoreInteractions(mBackupDataOutput); - } - - public void testRunBackup_OneNewCall() throws Exception { - CallLogBackupState state = new CallLogBackupState(); - state.version = CallLogBackupAgent.VERSION; - state.callIds = new TreeSet<>(); - List calls = new LinkedList<>(); - calls.add(makeCall(101, 0L, 0L, "555-5555")); - mCallLogBackupAgent.runBackup(state, mBackupDataOutput, calls); - - verify(mBackupDataOutput).writeEntityHeader(eq("101"), Matchers.anyInt()); - verify(mBackupDataOutput).writeEntityData((byte[]) Matchers.any(), Matchers.anyInt()); - } - - public void testRunBackup_MultipleCall() throws Exception { - CallLogBackupState state = new CallLogBackupState(); - state.version = CallLogBackupAgent.VERSION; - state.callIds = new TreeSet<>(); - List calls = new LinkedList<>(); - calls.add(makeCall(101, 0L, 0L, "555-1234")); - calls.add(makeCall(102, 0L, 0L, "555-5555")); - - mCallLogBackupAgent.runBackup(state, mBackupDataOutput, calls); - - InOrder inOrder = Mockito.inOrder(mBackupDataOutput); - inOrder.verify(mBackupDataOutput).writeEntityHeader(eq("101"), Matchers.anyInt()); - inOrder.verify(mBackupDataOutput). - writeEntityData((byte[]) Matchers.any(), Matchers.anyInt()); - inOrder.verify(mBackupDataOutput).writeEntityHeader(eq("102"), Matchers.anyInt()); - inOrder.verify(mBackupDataOutput). - writeEntityData((byte[]) Matchers.any(), Matchers.anyInt()); - } - - public void testRunBackup_PartialMultipleCall() throws Exception { - CallLogBackupState state = new CallLogBackupState(); - - state.version = CallLogBackupAgent.VERSION; - state.callIds = new TreeSet<>(); - state.callIds.add(101); - - List calls = new LinkedList<>(); - calls.add(makeCall(101, 0L, 0L, "555-1234")); - calls.add(makeCall(102, 0L, 0L, "555-5555")); - - mCallLogBackupAgent.runBackup(state, mBackupDataOutput, calls); - - InOrder inOrder = Mockito.inOrder(mBackupDataOutput); - inOrder.verify(mBackupDataOutput).writeEntityHeader(eq("102"), Matchers.anyInt()); - inOrder.verify(mBackupDataOutput). - writeEntityData((byte[]) Matchers.any(), Matchers.anyInt()); - } - - private static Call makeCall(int id, long date, long duration, String number) { - Call c = new Call(); - c.id = id; - c.date = date; - c.duration = duration; - c.number = number; - c.accountComponentName = "account-component"; - c.accountId = "account-id"; - return c; - } - -} diff --git a/tests/src/com/android/providers/calllogbackup/MockitoHelper.java b/tests/src/com/android/providers/calllogbackup/MockitoHelper.java deleted file mode 100644 index 778f142..0000000 --- a/tests/src/com/android/providers/calllogbackup/MockitoHelper.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * 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.util.Log; - -/** - * Helper for Mockito-based test cases. - */ -public final class MockitoHelper { - private static final String TAG = "MockitoHelper"; - - private ClassLoader mOriginalClassLoader; - private Thread mContextThread; - - /** - * Creates a new helper, which in turn will set the context classloader so - * it can load Mockito resources. - * - * @param packageClass test case class - */ - public void setUp(Class packageClass) throws Exception { - // makes a copy of the context classloader - mContextThread = Thread.currentThread(); - mOriginalClassLoader = mContextThread.getContextClassLoader(); - ClassLoader newClassLoader = packageClass.getClassLoader(); - Log.v(TAG, "Changing context classloader from " + mOriginalClassLoader - + " to " + newClassLoader); - mContextThread.setContextClassLoader(newClassLoader); - } - - /** - * Restores the context classloader to the previous value. - */ - public void tearDown() throws Exception { - Log.v(TAG, "Restoring context classloader to " + mOriginalClassLoader); - mContextThread.setContextClassLoader(mOriginalClassLoader); - } -} -- cgit v1.2.3