blob: a94c91ca4a159749edc9d6ee386e8d9a21df386f [file] [log] [blame]
Santos Cordon61cc9302015-03-06 12:40:43 -08001/*
2 * Copyright (C) 2015 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
17package com.android.providers.calllogbackup;
18
19import android.app.backup.BackupAgent;
20import android.app.backup.BackupDataInput;
21import android.app.backup.BackupDataOutput;
22import android.content.ComponentName;
23import android.content.ContentResolver;
24import android.database.Cursor;
25import android.os.ParcelFileDescriptor;
26import android.os.UserHandle;
27import android.os.UserManager;
28import android.provider.CallLog;
29import android.provider.CallLog.Calls;
30import android.telecom.PhoneAccountHandle;
31import android.util.Log;
32
33import com.android.internal.annotations.VisibleForTesting;
34
35import java.io.BufferedOutputStream;
36import java.io.ByteArrayInputStream;
37import java.io.ByteArrayOutputStream;
38import java.io.DataInput;
39import java.io.DataInputStream;
40import java.io.DataOutput;
41import java.io.DataOutputStream;
42import java.io.EOFException;
43import java.io.FileInputStream;
44import java.io.FileOutputStream;
45import java.io.IOException;
46import java.util.LinkedList;
47import java.util.List;
48import java.util.SortedSet;
49import java.util.TreeSet;
50
51/**
52 * Call log backup agent.
53 */
54public class CallLogBackupAgent extends BackupAgent {
55
56 @VisibleForTesting
57 static class CallLogBackupState {
58 int version;
59 SortedSet<Integer> callIds;
60 }
61
62 @VisibleForTesting
63 static class Call {
64 int id;
65 long date;
66 long duration;
67 String number;
68 int type;
69 int numberPresentation;
70 String accountComponentName;
71 String accountId;
72 String accountAddress;
73 Long dataUsage;
74 int features;
75
76 @Override
77 public String toString() {
78 if (isDebug()) {
79 return "[" + id + ", account: [" + accountComponentName + " : " + accountId +
80 "]," + number + ", " + date + "]";
81 } else {
82 return "[" + id + "]";
83 }
84 }
85 }
86
87 private static final String TAG = "CallLogBackupAgent";
88
89 /** Current version of CallLogBackup. Used to track the backup format. */
90 @VisibleForTesting
91 static final int VERSION = 1001;
92 /** Version indicating that there exists no previous backup entry. */
93 @VisibleForTesting
94 static final int VERSION_NO_PREVIOUS_STATE = 0;
95
96 private static final String[] CALL_LOG_PROJECTION = new String[] {
97 CallLog.Calls._ID,
98 CallLog.Calls.DATE,
99 CallLog.Calls.DURATION,
100 CallLog.Calls.NUMBER,
101 CallLog.Calls.TYPE,
102 CallLog.Calls.COUNTRY_ISO,
103 CallLog.Calls.GEOCODED_LOCATION,
104 CallLog.Calls.NUMBER_PRESENTATION,
105 CallLog.Calls.PHONE_ACCOUNT_COMPONENT_NAME,
106 CallLog.Calls.PHONE_ACCOUNT_ID,
107 CallLog.Calls.PHONE_ACCOUNT_ADDRESS,
108 CallLog.Calls.DATA_USAGE,
109 CallLog.Calls.FEATURES
110 };
111
112 /** ${inheritDoc} */
113 @Override
114 public void onBackup(ParcelFileDescriptor oldStateDescriptor, BackupDataOutput data,
115 ParcelFileDescriptor newStateDescriptor) throws IOException {
116
Santos Cordon61cc9302015-03-06 12:40:43 -0800117 // Get the list of the previous calls IDs which were backed up.
118 DataInputStream dataInput = new DataInputStream(
119 new FileInputStream(oldStateDescriptor.getFileDescriptor()));
120 final CallLogBackupState state;
121 try {
122 state = readState(dataInput);
123 } finally {
124 dataInput.close();
125 }
126
127 // Run the actual backup of data
128 runBackup(state, data, getAllCallLogEntries());
129
130 // Rewrite the backup state.
131 DataOutputStream dataOutput = new DataOutputStream(new BufferedOutputStream(
132 new FileOutputStream(newStateDescriptor.getFileDescriptor())));
133 try {
134 writeState(dataOutput, state);
135 } finally {
136 dataOutput.close();
137 }
138 }
139
140 /** ${inheritDoc} */
141 @Override
142 public void onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState)
143 throws IOException {
Santos Cordon61cc9302015-03-06 12:40:43 -0800144 if (isDebug()) {
145 Log.d(TAG, "Performing Restore");
146 }
147
148 while (data.readNextHeader()) {
149 Call call = readCallFromData(data);
150 if (call != null) {
151 writeCallToProvider(call);
152 if (isDebug()) {
153 Log.d(TAG, "Restored call: " + call);
154 }
155 }
156 }
157 }
158
159 @VisibleForTesting
160 void runBackup(CallLogBackupState state, BackupDataOutput data, Iterable<Call> calls) {
161 SortedSet<Integer> callsToRemove = new TreeSet<>(state.callIds);
162
163 // Loop through all the call log entries to identify:
164 // (1) new calls
165 // (2) calls which have been deleted.
166 for (Call call : calls) {
167 if (!state.callIds.contains(call.id)) {
168
169 if (isDebug()) {
170 Log.d(TAG, "Adding call to backup: " + call);
171 }
172
173 // This call new (not in our list from the last backup), lets back it up.
174 addCallToBackup(data, call);
175 state.callIds.add(call.id);
176 } else {
177 // This call still exists in the current call log so delete it from the
178 // "callsToRemove" set since we want to keep it.
179 callsToRemove.remove(call.id);
180 }
181 }
182
183 // Remove calls which no longer exist in the set.
184 for (Integer i : callsToRemove) {
185 if (isDebug()) {
186 Log.d(TAG, "Removing call from backup: " + i);
187 }
188
189 removeCallFromBackup(data, i);
190 state.callIds.remove(i);
191 }
192 }
193
194 private Iterable<Call> getAllCallLogEntries() {
195 List<Call> calls = new LinkedList<>();
196
197 // We use the API here instead of querying ContactsDatabaseHelper directly because
198 // CallLogProvider has special locks in place for sychronizing when to read. Using the APIs
199 // gives us that for free.
200 ContentResolver resolver = getContentResolver();
201 Cursor cursor = resolver.query(
202 CallLog.Calls.CONTENT_URI, CALL_LOG_PROJECTION, null, null, null);
203 if (cursor != null) {
204 try {
205 while (cursor.moveToNext()) {
206 Call call = readCallFromCursor(cursor);
207 if (call != null) {
208 calls.add(call);
209 }
210 }
211 } finally {
212 cursor.close();
213 }
214 }
215
216 return calls;
217 }
218
219 private void writeCallToProvider(Call call) {
220 Long dataUsage = call.dataUsage == 0 ? null : call.dataUsage;
221
Santos Cordon755cc642015-07-06 12:37:09 -0700222 PhoneAccountHandle handle = null;
223 if (call.accountComponentName != null && call.accountId != null) {
224 handle = new PhoneAccountHandle(
225 ComponentName.unflattenFromString(call.accountComponentName), call.accountId);
226 }
Santos Cordon61cc9302015-03-06 12:40:43 -0800227 Calls.addCall(null /* CallerInfo */, this, call.number, call.numberPresentation, call.type,
228 call.features, handle, call.date, (int) call.duration,
229 dataUsage, true /* addForAllUsers */);
230 }
231
232 @VisibleForTesting
233 CallLogBackupState readState(DataInput dataInput) throws IOException {
234 CallLogBackupState state = new CallLogBackupState();
235 state.callIds = new TreeSet<>();
236
237 try {
238 // Read the version.
239 state.version = dataInput.readInt();
240
241 if (state.version >= 1) {
242 // Read the size.
243 int size = dataInput.readInt();
244
245 // Read all of the call IDs.
246 for (int i = 0; i < size; i++) {
247 state.callIds.add(dataInput.readInt());
248 }
249 }
250 } catch (EOFException e) {
251 state.version = VERSION_NO_PREVIOUS_STATE;
252 }
253
254 return state;
255 }
256
257 @VisibleForTesting
258 void writeState(DataOutput dataOutput, CallLogBackupState state)
259 throws IOException {
260 // Write version first of all
261 dataOutput.writeInt(VERSION);
262
263 // [Version 1]
264 // size + callIds
265 dataOutput.writeInt(state.callIds.size());
266 for (Integer i : state.callIds) {
267 dataOutput.writeInt(i);
268 }
269 }
270
271 @VisibleForTesting
272 Call readCallFromData(BackupDataInput data) {
273 final int callId;
274 try {
275 callId = Integer.parseInt(data.getKey());
276 } catch (NumberFormatException e) {
277 Log.e(TAG, "Unexpected key found in restore: " + data.getKey());
278 return null;
279 }
280
281 try {
282 byte [] byteArray = new byte[data.getDataSize()];
283 data.readEntityData(byteArray, 0, byteArray.length);
284 DataInputStream dataInput = new DataInputStream(new ByteArrayInputStream(byteArray));
285
286 Call call = new Call();
287 call.id = callId;
288
289 int version = dataInput.readInt();
290 if (version >= 1) {
291 call.date = dataInput.readLong();
292 call.duration = dataInput.readLong();
293 call.number = readString(dataInput);
294 call.type = dataInput.readInt();
295 call.numberPresentation = dataInput.readInt();
296 call.accountComponentName = readString(dataInput);
297 call.accountId = readString(dataInput);
298 call.accountAddress = readString(dataInput);
299 call.dataUsage = dataInput.readLong();
300 call.features = dataInput.readInt();
301 }
302
303 return call;
304 } catch (IOException e) {
305 Log.e(TAG, "Error reading call data for " + callId, e);
306 return null;
307 }
308 }
309
310 private Call readCallFromCursor(Cursor cursor) {
311 Call call = new Call();
312 call.id = cursor.getInt(cursor.getColumnIndex(CallLog.Calls._ID));
313 call.date = cursor.getLong(cursor.getColumnIndex(CallLog.Calls.DATE));
314 call.duration = cursor.getLong(cursor.getColumnIndex(CallLog.Calls.DURATION));
315 call.number = cursor.getString(cursor.getColumnIndex(CallLog.Calls.NUMBER));
316 call.type = cursor.getInt(cursor.getColumnIndex(CallLog.Calls.TYPE));
317 call.numberPresentation =
318 cursor.getInt(cursor.getColumnIndex(CallLog.Calls.NUMBER_PRESENTATION));
319 call.accountComponentName =
320 cursor.getString(cursor.getColumnIndex(CallLog.Calls.PHONE_ACCOUNT_COMPONENT_NAME));
321 call.accountId =
322 cursor.getString(cursor.getColumnIndex(CallLog.Calls.PHONE_ACCOUNT_ID));
323 call.accountAddress =
324 cursor.getString(cursor.getColumnIndex(CallLog.Calls.PHONE_ACCOUNT_ADDRESS));
325 call.dataUsage = cursor.getLong(cursor.getColumnIndex(CallLog.Calls.DATA_USAGE));
326 call.features = cursor.getInt(cursor.getColumnIndex(CallLog.Calls.FEATURES));
327 return call;
328 }
329
330 private void addCallToBackup(BackupDataOutput output, Call call) {
331 ByteArrayOutputStream baos = new ByteArrayOutputStream();
332 DataOutputStream data = new DataOutputStream(baos);
333
334 try {
335 data.writeInt(VERSION);
336 data.writeLong(call.date);
337 data.writeLong(call.duration);
338 writeString(data, call.number);
339 data.writeInt(call.type);
340 data.writeInt(call.numberPresentation);
341 writeString(data, call.accountComponentName);
342 writeString(data, call.accountId);
343 writeString(data, call.accountAddress);
344 data.writeLong(call.dataUsage == null ? 0 : call.dataUsage);
345 data.writeInt(call.features);
346 data.flush();
347
348 output.writeEntityHeader(Integer.toString(call.id), baos.size());
349 output.writeEntityData(baos.toByteArray(), baos.size());
350
351 if (isDebug()) {
352 Log.d(TAG, "Wrote call to backup: " + call + " with byte array: " + baos);
353 }
354 } catch (IOException e) {
355 Log.e(TAG, "Failed to backup call: " + call, e);
356 }
357 }
358
359 private void writeString(DataOutputStream data, String str) throws IOException {
360 if (str == null) {
361 data.writeBoolean(false);
362 } else {
363 data.writeBoolean(true);
364 data.writeUTF(str);
365 }
366 }
367
368 private String readString(DataInputStream data) throws IOException {
369 if (data.readBoolean()) {
370 return data.readUTF();
371 } else {
372 return null;
373 }
374 }
375
376 private void removeCallFromBackup(BackupDataOutput output, int callId) {
377 try {
378 output.writeEntityHeader(Integer.toString(callId), -1);
379 } catch (IOException e) {
380 Log.e(TAG, "Failed to remove call: " + callId, e);
381 }
382 }
383
384 private static boolean isDebug() {
385 return Log.isLoggable(TAG, Log.DEBUG);
386 }
387}