summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Android.mk4
-rw-r--r--AndroidManifest.xml18
-rw-r--r--res/values/strings.xml4
-rw-r--r--src/com/android/providers/calllogbackup/CallLogBackupAgent.java392
-rw-r--r--src/com/android/providers/calllogbackup/CallLogChangeReceiver.java41
-rw-r--r--tests/Android.mk21
-rw-r--r--tests/AndroidManifest.xml37
-rw-r--r--tests/src/com/android/providers/calllogbackup/CallLogBackupAgentTest.java230
-rw-r--r--tests/src/com/android/providers/calllogbackup/MockitoHelper.java53
9 files changed, 792 insertions, 8 deletions
diff --git a/Android.mk b/Android.mk
index 83f8f26..b2681b2 100644
--- a/Android.mk
+++ b/Android.mk
@@ -11,9 +11,9 @@ 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.calllog.*
+LOCAL_EMMA_COVERAGE_FILTER := +com.android.providers.calllogbackup.*
-LOCAL_PACKAGE_NAME := CallLogProvider
+LOCAL_PACKAGE_NAME := CallLogBackup
LOCAL_CERTIFICATE := shared
LOCAL_PRIVILEGED_MODULE := true
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 0654e39..bf486de 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -1,12 +1,22 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="com.android.providers.calllog"
+ package="com.android.providers.calllogbackup"
android:sharedUserId="android.uid.shared"
android:sharedUserLabel="@string/sharedUserLabel">
- <application android:process="android.process.acore"
- android:label="@string/app_label"
+ <application android:label="@string/app_label"
android:icon="@drawable/app_icon"
- android:allowBackup="true">
+ android:allowBackup="true"
+ android:backupAgent="CallLogBackupAgent">
+ <meta-data android:name="com.google.android.backup.api_key"
+ android:value="AEdPqrEAAAAISbHhhUji6KZyyjz4I8-MdBqlnoiTJoFAEUHHzA" />
+
+ <receiver android:name="CallLogChangeReceiver"
+ android:permission="android.permission.SEND_CALL_LOG_CHANGE">
+ <!-- Sent when the call log changes. We use it to trigger a backup request. -->
+ <intent-filter>
+ <action android:name="android.intent.action.CALL_LOG_CHANGE" />
+ </intent-filter>
+ </receiver>
</application>
</manifest>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 09d70f9..0adbb03 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -20,7 +20,7 @@
apps sharing the uid). -->
<string name="sharedUserLabel">Android Core Apps</string>
- <!-- This is the label for the application that stores call-log data. -->
- <string name="app_label">Call Log Storage</string>
+ <!-- This is the label for the application that stores backs up and restores the calllog. -->
+ <string name="app_label">Call Log Backup/Restore</string>
</resources>
diff --git a/src/com/android/providers/calllogbackup/CallLogBackupAgent.java b/src/com/android/providers/calllogbackup/CallLogBackupAgent.java
new file mode 100644
index 0000000..24ab98e
--- /dev/null
+++ b/src/com/android/providers/calllogbackup/CallLogBackupAgent.java
@@ -0,0 +1,392 @@
+/*
+ * 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<Integer> 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 + "]";
+ }
+ }
+ }
+
+ private static final String TAG = "CallLogBackupAgent";
+
+ /** Current version of CallLogBackup. Used to track the backup format. */
+ @VisibleForTesting
+ static final int VERSION = 1001;
+ /** Version indicating that there exists no previous backup entry. */
+ @VisibleForTesting
+ static final int VERSION_NO_PREVIOUS_STATE = 0;
+
+ 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 (UserManager.get(this).getUserHandle() == UserHandle.USER_OWNER) {
+ 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 (UserManager.get(this).getUserHandle() == UserHandle.USER_OWNER) {
+ 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<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) {
+ calls.add(call);
+ }
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+
+ return calls;
+ }
+
+ private void writeCallToProvider(Call call) {
+ Long dataUsage = call.dataUsage == 0 ? null : call.dataUsage;
+
+ PhoneAccountHandle 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();
+ }
+
+ 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);
+ 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);
+ }
+ }
+
+ 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
new file mode 100644
index 0000000..dbd7b00
--- /dev/null
+++ b/src/com/android/providers/calllogbackup/CallLogChangeReceiver.java
@@ -0,0 +1,41 @@
+/*
+ * 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/Android.mk b/tests/Android.mk
new file mode 100644
index 0000000..7acc7be
--- /dev/null
+++ b/tests/Android.mk
@@ -0,0 +1,21 @@
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+# We only want this apk build for tests.
+LOCAL_MODULE_TAGS := tests
+
+LOCAL_STATIC_JAVA_LIBRARIES := mockito-target
+
+LOCAL_JAVA_LIBRARIES := android.test.runner
+
+# Only compile source java files in this apk.
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_PACKAGE_NAME := CallLogBackupTests
+
+LOCAL_INSTRUMENTATION_FOR := CallLogBackup
+LOCAL_CERTIFICATE := shared
+
+LOCAL_PROGUARD_ENABLED := disabled
+
+include $(BUILD_PACKAGE)
diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml
new file mode 100644
index 0000000..285bb46
--- /dev/null
+++ b/tests/AndroidManifest.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2008 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.providers.calllogbackup.tests"
+ android:sharedUserId="android.uid.shared">
+
+ <application>
+ <uses-library android:name="android.test.runner" />
+ </application>
+
+ <!--
+ The test declared in this instrumentation will be run along with tests declared by
+ 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"
+ -->
+ <instrumentation android:name="android.test.InstrumentationTestRunner"
+ android:targetPackage="com.android.providers.calllogbackup"
+ android:label="CallLog Backup Tests">
+ </instrumentation>
+
+</manifest>
diff --git a/tests/src/com/android/providers/calllogbackup/CallLogBackupAgentTest.java b/tests/src/com/android/providers/calllogbackup/CallLogBackupAgentTest.java
new file mode 100644
index 0000000..53ad162
--- /dev/null
+++ b/tests/src/com/android/providers/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.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<Call> 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<Call> 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<Call> 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<Call> 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
new file mode 100644
index 0000000..778f142
--- /dev/null
+++ b/tests/src/com/android/providers/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.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);
+ }
+}