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