/* * 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()); while (dataInputStream.available() > 0) { state.ids.add(dataInputStream.readInt()); } return state; } private void runBackup(BackupState state, BackupDataOutput backupDataOutput, Iterable allBlockedNumbers) throws IOException { SortedSet 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 getAllBlockedNumbers() { List 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 ids; BackupState(int version, SortedSet 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); } } } }