Backup and restore support for Blocked numbers.
BUG: 27619889
Change-Id: Ieed202a994740da3d8b2093fcbe510476c21311c
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 862437c..9bd5ac1 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -23,7 +23,9 @@
android:label="@string/app_label"
android:usesCleartextTraffic="false"
android:defaultToDeviceProtectedStorage="true"
- android:directBootAware="true">
+ android:directBootAware="true"
+ android:allowBackup="true"
+ android:backupAgent=".BlockedNumberBackupAgent">
<provider android:name="BlockedNumberProvider"
android:authorities="com.android.blockednumber"
diff --git a/src/com/android/providers/blockednumber/BlockedNumberBackupAgent.java b/src/com/android/providers/blockednumber/BlockedNumberBackupAgent.java
new file mode 100644
index 0000000..f2e392b
--- /dev/null
+++ b/src/com/android/providers/blockednumber/BlockedNumberBackupAgent.java
@@ -0,0 +1,278 @@
+/*
+ * Copyright (C) 2016 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.blockednumber;
+
+import android.annotation.Nullable;
+import android.app.backup.BackupAgent;
+import android.app.backup.BackupDataInput;
+import android.app.backup.BackupDataOutput;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.os.ParcelFileDescriptor;
+import android.provider.BlockedNumberContract;
+import android.util.Log;
+
+import libcore.io.IoUtils;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.SortedSet;
+import java.util.TreeSet;
+
+/**
+ * A backup agent to enable backup and restore of blocked numbers.
+ */
+public class BlockedNumberBackupAgent extends BackupAgent {
+ private static final String[] BLOCKED_NUMBERS_PROJECTION = new String[] {
+ BlockedNumberContract.BlockedNumbers.COLUMN_ID,
+ BlockedNumberContract.BlockedNumbers.COLUMN_ORIGINAL_NUMBER,
+ BlockedNumberContract.BlockedNumbers.COLUMN_E164_NUMBER,
+ };
+ private static final String TAG = "BlockedNumberBackup";
+ private static final int VERSION = 1;
+ private static final boolean DEBUG = false; // DO NOT SUBMIT WITH TRUE.
+
+ @Override
+ public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput backupDataOutput,
+ ParcelFileDescriptor newState) throws IOException {
+ logV("Backing up blocked numbers.");
+
+ DataInputStream dataInputStream =
+ new DataInputStream(new FileInputStream(oldState.getFileDescriptor()));
+ final BackupState state;
+ try {
+ state = readState(dataInputStream);
+ } finally {
+ IoUtils.closeQuietly(dataInputStream);
+ }
+
+ runBackup(state, backupDataOutput, getAllBlockedNumbers());
+
+ DataOutputStream dataOutputStream =
+ new DataOutputStream(new FileOutputStream(newState.getFileDescriptor()));
+ try {
+ writeNewState(dataOutputStream, state);
+ } finally {
+ dataOutputStream.close();
+ }
+ }
+
+ @Override
+ public void onRestore(BackupDataInput data, int appVersionCode,
+ ParcelFileDescriptor newState) throws IOException {
+ logV("Restoring blocked numbers.");
+
+ while (data.readNextHeader()) {
+ BackedUpBlockedNumber blockedNumber = readBlockedNumberFromData(data);
+ if (blockedNumber != null) {
+ writeToProvider(blockedNumber);
+ }
+ }
+ }
+
+ private BackupState readState(DataInputStream dataInputStream) throws IOException {
+ int version = VERSION;
+ if (dataInputStream.available() > 0) {
+ version = dataInputStream.readInt();
+ }
+ BackupState state = new BackupState(version, new TreeSet<Integer>());
+ while (dataInputStream.available() > 0) {
+ state.ids.add(dataInputStream.readInt());
+ }
+ return state;
+ }
+
+ private void runBackup(BackupState state, BackupDataOutput backupDataOutput,
+ Iterable<BackedUpBlockedNumber> allBlockedNumbers) throws IOException {
+ SortedSet<Integer> deletedBlockedNumbers = new TreeSet<>(state.ids);
+
+ for (BackedUpBlockedNumber blockedNumber : allBlockedNumbers) {
+ if (state.ids.contains(blockedNumber.id)) {
+ // Existing blocked number: do not delete.
+ deletedBlockedNumbers.remove(blockedNumber.id);
+ } else {
+ logV("Adding blocked number to backup: " + blockedNumber);
+ // New blocked number
+ addToBackup(backupDataOutput, blockedNumber);
+ state.ids.add(blockedNumber.id);
+ }
+ }
+
+ for (int id : deletedBlockedNumbers) {
+ logV("Removing blocked number from backup: " + id);
+ removeFromBackup(backupDataOutput, id);
+ state.ids.remove(id);
+ }
+ }
+
+ private void addToBackup(BackupDataOutput output, BackedUpBlockedNumber blockedNumber)
+ throws IOException {
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ DataOutputStream dataOutputStream = new DataOutputStream(outputStream);
+ dataOutputStream.writeInt(VERSION);
+ writeString(dataOutputStream, blockedNumber.originalNumber);
+ writeString(dataOutputStream, blockedNumber.e164Number);
+ dataOutputStream.flush();
+
+ output.writeEntityHeader(Integer.toString(blockedNumber.id), outputStream.size());
+ output.writeEntityData(outputStream.toByteArray(), outputStream.size());
+ }
+
+ private void writeString(DataOutputStream dataOutputStream, @Nullable String value)
+ throws IOException {
+ if (value == null) {
+ dataOutputStream.writeBoolean(false);
+ } else {
+ dataOutputStream.writeBoolean(true);
+ dataOutputStream.writeUTF(value);
+ }
+ }
+
+ @Nullable
+ private String readString(DataInputStream dataInputStream)
+ throws IOException {
+ if (dataInputStream.readBoolean()) {
+ return dataInputStream.readUTF();
+ } else {
+ return null;
+ }
+ }
+
+ private void removeFromBackup(BackupDataOutput output, int id) throws IOException {
+ output.writeEntityHeader(Integer.toString(id), -1);
+ }
+
+ private Iterable<BackedUpBlockedNumber> getAllBlockedNumbers() {
+ List<BackedUpBlockedNumber> blockedNumbers = new ArrayList<>();
+ ContentResolver resolver = getContentResolver();
+ Cursor cursor = resolver.query(
+ BlockedNumberContract.BlockedNumbers.CONTENT_URI, BLOCKED_NUMBERS_PROJECTION, null,
+ null, null);
+ if (cursor != null) {
+ try {
+ while (cursor.moveToNext()) {
+ blockedNumbers.add(createBlockedNumberFromCursor(cursor));
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+ return blockedNumbers;
+ }
+
+ private BackedUpBlockedNumber createBlockedNumberFromCursor(Cursor cursor) {
+ return new BackedUpBlockedNumber(
+ cursor.getInt(0), cursor.getString(1), cursor.getString(2));
+ }
+
+ private void writeNewState(DataOutputStream dataOutputStream, BackupState state)
+ throws IOException {
+ dataOutputStream.writeInt(VERSION);
+ for (int i : state.ids) {
+ dataOutputStream.writeInt(i);
+ }
+ }
+
+ @Nullable
+ private BackedUpBlockedNumber readBlockedNumberFromData(BackupDataInput data) {
+ int id;
+ try {
+ id = 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));
+ dataInput.readInt(); // Ignore version.
+ BackedUpBlockedNumber blockedNumber =
+ new BackedUpBlockedNumber(id, readString(dataInput), readString(dataInput));
+ logV("Restoring blocked number: " + blockedNumber);
+ return blockedNumber;
+ } catch (IOException e) {
+ Log.e(TAG, "Error reading blocked number for: " + id + ": " + e.getMessage());
+ return null;
+ }
+ }
+
+ private void writeToProvider(BackedUpBlockedNumber blockedNumber) {
+ ContentValues contentValues = new ContentValues();
+ contentValues.put(BlockedNumberContract.BlockedNumbers.COLUMN_ORIGINAL_NUMBER,
+ blockedNumber.originalNumber);
+ contentValues.put(BlockedNumberContract.BlockedNumbers.COLUMN_E164_NUMBER,
+ blockedNumber.e164Number);
+ try {
+ getContentResolver().insert(
+ BlockedNumberContract.BlockedNumbers.CONTENT_URI, contentValues);
+ } catch (Exception e) {
+ Log.e(TAG, "Unable to insert blocked number " + blockedNumber + " :" + e.getMessage());
+ }
+ }
+
+ private static boolean isDebug() {
+ return Log.isLoggable(TAG, Log.DEBUG);
+ }
+
+ private static void logV(String msg) {
+ if (DEBUG) {
+ Log.v(TAG, msg);
+ }
+ }
+
+ private static class BackupState {
+ final int version;
+ final SortedSet<Integer> ids;
+
+ BackupState(int version, SortedSet<Integer> ids) {
+ this.version = version;
+ this.ids = ids;
+ }
+ }
+
+ private static class BackedUpBlockedNumber {
+ final int id;
+ final String originalNumber;
+ final String e164Number;
+
+ BackedUpBlockedNumber(int id, String originalNumber, String e164Number) {
+ this.id = id;
+ this.originalNumber = originalNumber;
+ this.e164Number = e164Number;
+ }
+
+ @Override
+ public String toString() {
+ if (isDebug()) {
+ return String.format("[%d, original number: %s, e164 number: %s]",
+ id, originalNumber, e164Number);
+ } else {
+ return String.format("[%d]", id);
+ }
+ }
+ }
+}
diff --git a/src/com/android/providers/blockednumber/BlockedNumberProvider.java b/src/com/android/providers/blockednumber/BlockedNumberProvider.java
index fa07a47..a71a0b1 100644
--- a/src/com/android/providers/blockednumber/BlockedNumberProvider.java
+++ b/src/com/android/providers/blockednumber/BlockedNumberProvider.java
@@ -19,6 +19,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.AppOpsManager;
+import android.app.backup.BackupManager;
import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
@@ -69,6 +70,10 @@
private static final String BLOCK_SUPPRESSION_EXPIRY_TIME_PREF =
"block_suppression_expiry_time_pref";
private static final int MAX_BLOCKING_DISABLED_DURATION_SECONDS = 7 * 24 * 3600; // 1 week
+ // Normally, we allow calls from self, *except* in unit tests, where we clear this flag
+ // to emulate calls from other apps.
+ @VisibleForTesting
+ static boolean ALLOW_SELF_CALL = true;
static {
sUriMatcher = new UriMatcher(0);
@@ -87,10 +92,13 @@
@VisibleForTesting
protected BlockedNumberDatabaseHelper mDbHelper;
+ @VisibleForTesting
+ protected BackupManager mBackupManager;
@Override
public boolean onCreate() {
mDbHelper = BlockedNumberDatabaseHelper.getInstance(getContext());
+ mBackupManager = new BackupManager(getContext());
return true;
}
@@ -116,6 +124,7 @@
case BLOCKED_LIST:
Uri blockedUri = insertBlockedNumber(values);
getContext().getContentResolver().notifyChange(blockedUri, null);
+ mBackupManager.dataChanged();
return blockedUri;
default:
throw new IllegalArgumentException("Unsupported URI: " + uri);
@@ -185,6 +194,7 @@
throw new IllegalArgumentException("Unsupported URI: " + uri);
}
getContext().getContentResolver().notifyChange(uri, null);
+ mBackupManager.dataChanged();
return numRows;
}
@@ -487,7 +497,7 @@
private void checkForPermission(String permission) {
boolean permitted = passesSystemPermissionCheck(permission)
- || checkForPrivilegedApplications();
+ || checkForPrivilegedApplications() || isSelf();
if (!permitted) {
throwSecurityException();
}
@@ -516,6 +526,10 @@
== PackageManager.PERMISSION_GRANTED;
}
+ private boolean isSelf() {
+ return ALLOW_SELF_CALL && Binder.getCallingPid() == Process.myPid();
+ }
+
private void throwSecurityException() {
throw new SecurityException("Caller must be system, default dialer or default SMS app");
}
diff --git a/tests/src/com/android/providers/blockednumber/BlockedNumberProviderTest.java b/tests/src/com/android/providers/blockednumber/BlockedNumberProviderTest.java
index ca7dfb8..217a7d0 100644
--- a/tests/src/com/android/providers/blockednumber/BlockedNumberProviderTest.java
+++ b/tests/src/com/android/providers/blockednumber/BlockedNumberProviderTest.java
@@ -63,6 +63,7 @@
@Override
protected void setUp() throws Exception {
super.setUp();
+ BlockedNumberProvider.ALLOW_SELF_CALL = false;
mMockContext = spy(new MyMockContext(getContext()));
mMockContext.initializeContext();
@@ -175,6 +176,7 @@
Uri uri = insert(cv(BlockedNumbers.COLUMN_ORIGINAL_NUMBER, "14506507000"));
mResolver.delete(uri, null, null);
latch.await(10, TimeUnit.SECONDS);
+ verify(mMockContext.mBackupManager, times(2)).dataChanged();
} catch (Exception e) {
fail(e.toString());
} finally {
@@ -393,6 +395,16 @@
.checkCarrierPrivilegesForPackage(anyString());
}
+ public void testSelfCanAccessApis() {
+ BlockedNumberProvider.ALLOW_SELF_CALL = true;
+ doReturn(PackageManager.PERMISSION_DENIED)
+ .when(mMockContext).checkCallingPermission(anyString());
+
+ mResolver.insert(
+ BlockedNumbers.CONTENT_URI, cv(BlockedNumbers.COLUMN_ORIGINAL_NUMBER, "123"));
+ assertIsBlocked(true, "123");
+ }
+
public void testDefaultDialerCanAccessApis() {
doReturn(PackageManager.PERMISSION_DENIED)
.when(mMockContext).checkCallingPermission(anyString());
diff --git a/tests/src/com/android/providers/blockednumber/BlockedNumberProviderTestable.java b/tests/src/com/android/providers/blockednumber/BlockedNumberProviderTestable.java
index b67b1ab..33fdd64 100644
--- a/tests/src/com/android/providers/blockednumber/BlockedNumberProviderTestable.java
+++ b/tests/src/com/android/providers/blockednumber/BlockedNumberProviderTestable.java
@@ -15,7 +15,13 @@
*/
package com.android.providers.blockednumber;
+import android.app.backup.BackupManager;
+
public class BlockedNumberProviderTestable extends BlockedNumberProvider {
+ BlockedNumberProviderTestable(BackupManager backupManager) {
+ mBackupManager = backupManager;
+ }
+
@Override
public boolean onCreate() {
mDbHelper = BlockedNumberDatabaseHelper.newInstanceForTest(getContext());
diff --git a/tests/src/com/android/providers/blockednumber/MyMockContext.java b/tests/src/com/android/providers/blockednumber/MyMockContext.java
index 6834aca..85617f3 100644
--- a/tests/src/com/android/providers/blockednumber/MyMockContext.java
+++ b/tests/src/com/android/providers/blockednumber/MyMockContext.java
@@ -16,6 +16,7 @@
package com.android.providers.blockednumber;
import android.app.AppOpsManager;
+import android.app.backup.BackupManager;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
@@ -51,6 +52,8 @@
TelephonyManager mTelephonyManager;
@Mock
CarrierConfigManager mCarrierConfigManager;
+ @Mock
+ BackupManager mBackupManager;
private final HashMap<Class<?>, String> mSupportedServiceNamesByClass =
new HashMap<Class<?>, String>();
@@ -116,7 +119,7 @@
registerServices();
mResolver = new MockContentResolver();
- mProvider = new BlockedNumberProviderTestable();
+ mProvider = new BlockedNumberProviderTestable(mBackupManager);
final ProviderInfo info = new ProviderInfo();
info.authority = BlockedNumberContract.AUTHORITY;