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;