1 /* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License 15 */ 16 17 package com.android.providers.blockednumber; 18 19 import android.annotation.Nullable; 20 import android.app.backup.BackupAgent; 21 import android.app.backup.BackupDataInput; 22 import android.app.backup.BackupDataOutput; 23 import android.content.ContentResolver; 24 import android.content.ContentValues; 25 import android.database.Cursor; 26 import android.os.ParcelFileDescriptor; 27 import android.provider.BlockedNumberContract; 28 import android.util.Log; 29 30 import libcore.io.IoUtils; 31 32 import java.io.ByteArrayInputStream; 33 import java.io.ByteArrayOutputStream; 34 import java.io.DataInputStream; 35 import java.io.DataOutputStream; 36 import java.io.FileInputStream; 37 import java.io.FileOutputStream; 38 import java.io.IOException; 39 import java.util.ArrayList; 40 import java.util.List; 41 import java.util.SortedSet; 42 import java.util.TreeSet; 43 44 /** 45 * A backup agent to enable backup and restore of blocked numbers. 46 */ 47 public class BlockedNumberBackupAgent extends BackupAgent { 48 private static final String[] BLOCKED_NUMBERS_PROJECTION = new String[] { 49 BlockedNumberContract.BlockedNumbers.COLUMN_ID, 50 BlockedNumberContract.BlockedNumbers.COLUMN_ORIGINAL_NUMBER, 51 BlockedNumberContract.BlockedNumbers.COLUMN_E164_NUMBER, 52 }; 53 private static final String TAG = "BlockedNumberBackup"; 54 private static final int VERSION = 1; 55 private static final boolean DEBUG = false; // DO NOT SUBMIT WITH TRUE. 56 57 @Override onBackup(ParcelFileDescriptor oldState, BackupDataOutput backupDataOutput, ParcelFileDescriptor newState)58 public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput backupDataOutput, 59 ParcelFileDescriptor newState) throws IOException { 60 logV("Backing up blocked numbers."); 61 62 DataInputStream dataInputStream = 63 new DataInputStream(new FileInputStream(oldState.getFileDescriptor())); 64 final BackupState state; 65 try { 66 state = readState(dataInputStream); 67 } finally { 68 IoUtils.closeQuietly(dataInputStream); 69 } 70 71 runBackup(state, backupDataOutput, getAllBlockedNumbers()); 72 73 DataOutputStream dataOutputStream = 74 new DataOutputStream(new FileOutputStream(newState.getFileDescriptor())); 75 try { 76 writeNewState(dataOutputStream, state); 77 } finally { 78 dataOutputStream.close(); 79 } 80 } 81 82 @Override onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState)83 public void onRestore(BackupDataInput data, int appVersionCode, 84 ParcelFileDescriptor newState) throws IOException { 85 logV("Restoring blocked numbers."); 86 87 while (data.readNextHeader()) { 88 BackedUpBlockedNumber blockedNumber = readBlockedNumberFromData(data); 89 if (blockedNumber != null) { 90 writeToProvider(blockedNumber); 91 } 92 } 93 } 94 readState(DataInputStream dataInputStream)95 private BackupState readState(DataInputStream dataInputStream) throws IOException { 96 int version = VERSION; 97 if (dataInputStream.available() > 0) { 98 version = dataInputStream.readInt(); 99 } 100 BackupState state = new BackupState(version, new TreeSet<Integer>()); 101 while (dataInputStream.available() > 0) { 102 state.ids.add(dataInputStream.readInt()); 103 } 104 return state; 105 } 106 runBackup(BackupState state, BackupDataOutput backupDataOutput, Iterable<BackedUpBlockedNumber> allBlockedNumbers)107 private void runBackup(BackupState state, BackupDataOutput backupDataOutput, 108 Iterable<BackedUpBlockedNumber> allBlockedNumbers) throws IOException { 109 SortedSet<Integer> deletedBlockedNumbers = new TreeSet<>(state.ids); 110 111 for (BackedUpBlockedNumber blockedNumber : allBlockedNumbers) { 112 if (state.ids.contains(blockedNumber.id)) { 113 // Existing blocked number: do not delete. 114 deletedBlockedNumbers.remove(blockedNumber.id); 115 } else { 116 logV("Adding blocked number to backup: " + blockedNumber); 117 // New blocked number 118 addToBackup(backupDataOutput, blockedNumber); 119 state.ids.add(blockedNumber.id); 120 } 121 } 122 123 for (int id : deletedBlockedNumbers) { 124 logV("Removing blocked number from backup: " + id); 125 removeFromBackup(backupDataOutput, id); 126 state.ids.remove(id); 127 } 128 } 129 addToBackup(BackupDataOutput output, BackedUpBlockedNumber blockedNumber)130 private void addToBackup(BackupDataOutput output, BackedUpBlockedNumber blockedNumber) 131 throws IOException { 132 ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); 133 DataOutputStream dataOutputStream = new DataOutputStream(outputStream); 134 dataOutputStream.writeInt(VERSION); 135 writeString(dataOutputStream, blockedNumber.originalNumber); 136 writeString(dataOutputStream, blockedNumber.e164Number); 137 dataOutputStream.flush(); 138 139 output.writeEntityHeader(Integer.toString(blockedNumber.id), outputStream.size()); 140 output.writeEntityData(outputStream.toByteArray(), outputStream.size()); 141 } 142 writeString(DataOutputStream dataOutputStream, @Nullable String value)143 private void writeString(DataOutputStream dataOutputStream, @Nullable String value) 144 throws IOException { 145 if (value == null) { 146 dataOutputStream.writeBoolean(false); 147 } else { 148 dataOutputStream.writeBoolean(true); 149 dataOutputStream.writeUTF(value); 150 } 151 } 152 153 @Nullable readString(DataInputStream dataInputStream)154 private String readString(DataInputStream dataInputStream) 155 throws IOException { 156 if (dataInputStream.readBoolean()) { 157 return dataInputStream.readUTF(); 158 } else { 159 return null; 160 } 161 } 162 removeFromBackup(BackupDataOutput output, int id)163 private void removeFromBackup(BackupDataOutput output, int id) throws IOException { 164 output.writeEntityHeader(Integer.toString(id), -1); 165 } 166 getAllBlockedNumbers()167 private Iterable<BackedUpBlockedNumber> getAllBlockedNumbers() { 168 List<BackedUpBlockedNumber> blockedNumbers = new ArrayList<>(); 169 ContentResolver resolver = getContentResolver(); 170 Cursor cursor = resolver.query( 171 BlockedNumberContract.BlockedNumbers.CONTENT_URI, BLOCKED_NUMBERS_PROJECTION, null, 172 null, null); 173 if (cursor != null) { 174 try { 175 while (cursor.moveToNext()) { 176 blockedNumbers.add(createBlockedNumberFromCursor(cursor)); 177 } 178 } finally { 179 cursor.close(); 180 } 181 } 182 return blockedNumbers; 183 } 184 createBlockedNumberFromCursor(Cursor cursor)185 private BackedUpBlockedNumber createBlockedNumberFromCursor(Cursor cursor) { 186 return new BackedUpBlockedNumber( 187 cursor.getInt(0), cursor.getString(1), cursor.getString(2)); 188 } 189 writeNewState(DataOutputStream dataOutputStream, BackupState state)190 private void writeNewState(DataOutputStream dataOutputStream, BackupState state) 191 throws IOException { 192 dataOutputStream.writeInt(VERSION); 193 for (int i : state.ids) { 194 dataOutputStream.writeInt(i); 195 } 196 } 197 198 @Nullable readBlockedNumberFromData(BackupDataInput data)199 private BackedUpBlockedNumber readBlockedNumberFromData(BackupDataInput data) { 200 int id; 201 try { 202 id = Integer.parseInt(data.getKey()); 203 } catch (NumberFormatException e) { 204 Log.e(TAG, "Unexpected key found in restore: " + data.getKey()); 205 return null; 206 } 207 208 try { 209 byte[] byteArray = new byte[data.getDataSize()]; 210 data.readEntityData(byteArray, 0, byteArray.length); 211 DataInputStream dataInput = new DataInputStream(new ByteArrayInputStream(byteArray)); 212 dataInput.readInt(); // Ignore version. 213 BackedUpBlockedNumber blockedNumber = 214 new BackedUpBlockedNumber(id, readString(dataInput), readString(dataInput)); 215 logV("Restoring blocked number: " + blockedNumber); 216 return blockedNumber; 217 } catch (IOException e) { 218 Log.e(TAG, "Error reading blocked number for: " + id + ": " + e.getMessage()); 219 return null; 220 } 221 } 222 writeToProvider(BackedUpBlockedNumber blockedNumber)223 private void writeToProvider(BackedUpBlockedNumber blockedNumber) { 224 ContentValues contentValues = new ContentValues(); 225 contentValues.put(BlockedNumberContract.BlockedNumbers.COLUMN_ORIGINAL_NUMBER, 226 blockedNumber.originalNumber); 227 contentValues.put(BlockedNumberContract.BlockedNumbers.COLUMN_E164_NUMBER, 228 blockedNumber.e164Number); 229 try { 230 getContentResolver().insert( 231 BlockedNumberContract.BlockedNumbers.CONTENT_URI, contentValues); 232 } catch (Exception e) { 233 Log.e(TAG, "Unable to insert blocked number " + blockedNumber + " :" + e.getMessage()); 234 } 235 } 236 isDebug()237 private static boolean isDebug() { 238 return Log.isLoggable(TAG, Log.DEBUG); 239 } 240 logV(String msg)241 private static void logV(String msg) { 242 if (DEBUG) { 243 Log.v(TAG, msg); 244 } 245 } 246 247 private static class BackupState { 248 final int version; 249 final SortedSet<Integer> ids; 250 BackupState(int version, SortedSet<Integer> ids)251 BackupState(int version, SortedSet<Integer> ids) { 252 this.version = version; 253 this.ids = ids; 254 } 255 } 256 257 private static class BackedUpBlockedNumber { 258 final int id; 259 final String originalNumber; 260 final String e164Number; 261 BackedUpBlockedNumber(int id, String originalNumber, String e164Number)262 BackedUpBlockedNumber(int id, String originalNumber, String e164Number) { 263 this.id = id; 264 this.originalNumber = originalNumber; 265 this.e164Number = e164Number; 266 } 267 268 @Override toString()269 public String toString() { 270 if (isDebug()) { 271 return String.format("[%d, original number: %s, e164 number: %s]", 272 id, originalNumber, e164Number); 273 } else { 274 return String.format("[%d]", id); 275 } 276 } 277 } 278 } 279