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.dialer.backup;
18 
19 import android.annotation.TargetApi;
20 import android.content.ContentResolver;
21 import android.content.ContentValues;
22 import android.content.Context;
23 import android.database.Cursor;
24 import android.net.Uri;
25 import android.os.Build.VERSION_CODES;
26 import android.provider.VoicemailContract;
27 import android.provider.VoicemailContract.Voicemails;
28 import android.support.annotation.NonNull;
29 import android.support.annotation.Nullable;
30 import android.telecom.PhoneAccountHandle;
31 import android.telecom.TelecomManager;
32 import android.util.Pair;
33 import com.android.dialer.common.Assert;
34 import com.android.dialer.common.ConfigProviderBindings;
35 import com.android.dialer.common.LogUtil;
36 import com.android.voicemail.VoicemailComponent;
37 import com.google.common.io.ByteStreams;
38 import com.google.common.io.Files;
39 import com.google.protobuf.ByteString;
40 import java.io.ByteArrayInputStream;
41 import java.io.File;
42 import java.io.IOException;
43 import java.io.InputStream;
44 import java.io.OutputStream;
45 import java.util.ArrayList;
46 import java.util.List;
47 
48 /** Helper functions for DialerBackupAgent */
49 public class DialerBackupUtils {
50   // Backup voicemails up to 20MB
51   static long maxVoicemailSizeToBackup = 20000000L;
52   static final String RESTORED_COLUMN = "restored";
53 
DialerBackupUtils()54   private DialerBackupUtils() {}
55 
copyAudioBytesToContentUri( @onNull byte[] audioBytesArray, @NonNull OutputStream restoreStream)56   public static void copyAudioBytesToContentUri(
57       @NonNull byte[] audioBytesArray, @NonNull OutputStream restoreStream) throws IOException {
58     LogUtil.i("DialerBackupUtils.copyStream", "audioByteArray length: " + audioBytesArray.length);
59 
60     ByteArrayInputStream decodedStream = new ByteArrayInputStream(audioBytesArray);
61     LogUtil.i(
62         "DialerBackupUtils.copyStream", "decodedStream.available: " + decodedStream.available());
63 
64     ByteStreams.copy(decodedStream, restoreStream);
65   }
66 
audioStreamToByteString(@onNull InputStream stream)67   public static @Nullable ByteString audioStreamToByteString(@NonNull InputStream stream)
68       throws IOException {
69     if (stream.available() > 0) {
70       return ByteString.readFrom(stream);
71     } else {
72       LogUtil.i("DialerBackupUtils.audioStreamToByteArray", "no audio stream to backup");
73     }
74     return ByteString.EMPTY;
75   }
76 
writeProtoToFile(@onNull File file, @NonNull VoicemailInfo voicemailInfo)77   public static void writeProtoToFile(@NonNull File file, @NonNull VoicemailInfo voicemailInfo)
78       throws IOException {
79     LogUtil.i(
80         "DialerBackupUtils.writeProtoToFile",
81         "backup " + voicemailInfo + " to " + file.getAbsolutePath());
82 
83     byte[] bytes = voicemailInfo.toByteArray();
84     Files.write(bytes, file);
85   }
86 
87   /** Only restore voicemails that have the restored column in calllog (NMR2+ builds) */
88   @TargetApi(VERSION_CODES.M)
canRestoreVoicemails(ContentResolver contentResolver, Context context)89   public static boolean canRestoreVoicemails(ContentResolver contentResolver, Context context) {
90     try (Cursor cursor = contentResolver.query(Voicemails.CONTENT_URI, null, null, null, null)) {
91       // Restored column only exists in NMR2 and above builds.
92       if (cursor.getColumnIndex(RESTORED_COLUMN) != -1) {
93         LogUtil.i("DialerBackupUtils.canRestoreVoicemails", "Build supports restore");
94         return true;
95       } else {
96         LogUtil.i("DialerBackupUtils.canRestoreVoicemails", "Build does not support restore");
97         return false;
98       }
99     }
100   }
101 
protoFileToVoicemailInfo(@onNull File file)102   public static VoicemailInfo protoFileToVoicemailInfo(@NonNull File file) throws IOException {
103     byte[] byteArray = Files.toByteArray(file);
104     return VoicemailInfo.parseFrom(byteArray);
105   }
106 
107   @TargetApi(VERSION_CODES.M)
convertVoicemailCursorRowToProto( @onNull Cursor cursor, @NonNull ContentResolver contentResolver)108   public static VoicemailInfo convertVoicemailCursorRowToProto(
109       @NonNull Cursor cursor, @NonNull ContentResolver contentResolver) throws IOException {
110 
111     VoicemailInfo.Builder voicemailInfo = VoicemailInfo.newBuilder();
112 
113     for (int i = 0; i < cursor.getColumnCount(); ++i) {
114       String name = cursor.getColumnName(i);
115       String value = cursor.getString(i);
116 
117       LogUtil.i(
118           "DialerBackupUtils.convertVoicemailCursorRowToProto",
119           "column index: %d, column name: %s, column value: %s",
120           i,
121           name,
122           value);
123 
124       switch (name) {
125         case Voicemails.DATE:
126           voicemailInfo.setDate(value);
127           break;
128         case Voicemails.DELETED:
129           voicemailInfo.setDeleted(value);
130           break;
131         case Voicemails.DIRTY:
132           voicemailInfo.setDirty(value);
133           break;
134         case Voicemails.DIR_TYPE:
135           voicemailInfo.setDirType(value);
136           break;
137         case Voicemails.DURATION:
138           voicemailInfo.setDuration(value);
139           break;
140         case Voicemails.HAS_CONTENT:
141           voicemailInfo.setHasContent(value);
142           break;
143         case Voicemails.IS_READ:
144           voicemailInfo.setIsRead(value);
145           break;
146         case Voicemails.ITEM_TYPE:
147           voicemailInfo.setItemType(value);
148           break;
149         case Voicemails.LAST_MODIFIED:
150           voicemailInfo.setLastModified(value);
151           break;
152         case Voicemails.MIME_TYPE:
153           voicemailInfo.setMimeType(value);
154           break;
155         case Voicemails.NUMBER:
156           voicemailInfo.setNumber(value);
157           break;
158         case Voicemails.PHONE_ACCOUNT_COMPONENT_NAME:
159           voicemailInfo.setPhoneAccountComponentName(value);
160           break;
161         case Voicemails.PHONE_ACCOUNT_ID:
162           voicemailInfo.setPhoneAccountId(value);
163           break;
164         case Voicemails.SOURCE_DATA:
165           voicemailInfo.setSourceData(value);
166           break;
167         case Voicemails.SOURCE_PACKAGE:
168           voicemailInfo.setSourcePackage(value);
169           break;
170         case Voicemails.TRANSCRIPTION:
171           voicemailInfo.setTranscription(value);
172           break;
173         case DialerBackupAgent.VOICEMAIL_URI:
174           try (InputStream audioStream = contentResolver.openInputStream(Uri.parse(value))) {
175             voicemailInfo.setEncodedVoicemailKey(audioStreamToByteString(audioStream));
176           }
177           break;
178         default:
179           LogUtil.i(
180               "DialerBackupUtils.convertVoicemailCursorRowToProto",
181               "Not backing up column: %s, with value: %s",
182               name,
183               value);
184           break;
185       }
186     }
187     return voicemailInfo.build();
188   }
189 
convertVoicemailProtoFileToContentValueAndAudioBytes( @onNull File file, Context context)190   public static Pair<ContentValues, byte[]> convertVoicemailProtoFileToContentValueAndAudioBytes(
191       @NonNull File file, Context context) throws IOException {
192 
193     VoicemailInfo voicemailInfo = DialerBackupUtils.protoFileToVoicemailInfo(file);
194     LogUtil.i(
195         "DialerBackupUtils.convertVoicemailProtoFileToContentValueAndEncodedAudio",
196         "file name: "
197             + file.getName()
198             + " voicemailInfo size: "
199             + voicemailInfo.getSerializedSize());
200 
201     if (isDuplicate(context, voicemailInfo)) {
202       LogUtil.i(
203           "DialerBackupUtils.convertVoicemailProtoFileToContentValueAndEncodedAudio",
204           "voicemail already exists");
205       return null;
206     } else {
207       ContentValues contentValues = new ContentValues();
208 
209       if (voicemailInfo.hasDate()) {
210         contentValues.put(Voicemails.DATE, voicemailInfo.getDate());
211       }
212       if (voicemailInfo.hasDeleted()) {
213         contentValues.put(Voicemails.DELETED, voicemailInfo.getDeleted());
214       }
215       if (!voicemailInfo.hasDirty()) {
216         contentValues.put(Voicemails.DIRTY, voicemailInfo.getDirty());
217       }
218       if (!voicemailInfo.hasDuration()) {
219         contentValues.put(Voicemails.DURATION, voicemailInfo.getDuration());
220       }
221       if (!voicemailInfo.hasIsRead()) {
222         contentValues.put(Voicemails.IS_READ, voicemailInfo.getIsRead());
223       }
224       if (!voicemailInfo.hasLastModified()) {
225         contentValues.put(Voicemails.LAST_MODIFIED, voicemailInfo.getLastModified());
226       }
227       if (!voicemailInfo.hasMimeType()) {
228         contentValues.put(Voicemails.MIME_TYPE, voicemailInfo.getMimeType());
229       }
230       if (!voicemailInfo.hasNumber()) {
231         contentValues.put(Voicemails.NUMBER, voicemailInfo.getNumber());
232       }
233       if (!voicemailInfo.hasPhoneAccountComponentName()) {
234         contentValues.put(
235             Voicemails.PHONE_ACCOUNT_COMPONENT_NAME, voicemailInfo.getPhoneAccountComponentName());
236       }
237       if (!voicemailInfo.hasPhoneAccountId()) {
238         contentValues.put(Voicemails.PHONE_ACCOUNT_ID, voicemailInfo.getPhoneAccountId());
239       }
240       if (!voicemailInfo.hasSourceData()) {
241         contentValues.put(Voicemails.SOURCE_DATA, voicemailInfo.getSourceData());
242       }
243       if (!voicemailInfo.hasSourcePackage()) {
244         contentValues.put(Voicemails.SOURCE_PACKAGE, voicemailInfo.getSourcePackage());
245       }
246       if (!voicemailInfo.hasTranscription()) {
247         contentValues.put(Voicemails.TRANSCRIPTION, voicemailInfo.getTranscription());
248       }
249       contentValues.put(VoicemailContract.Voicemails.HAS_CONTENT, 1);
250       contentValues.put(RESTORED_COLUMN, "1");
251       contentValues.put(Voicemails.SOURCE_PACKAGE, getSourcePackage(context, voicemailInfo));
252 
253       LogUtil.i(
254           "DialerBackupUtils.convertVoicemailProtoFileToContentValueAndEncodedAudio",
255           "cv: " + contentValues);
256 
257       return Pair.create(contentValues, voicemailInfo.getEncodedVoicemailKey().toByteArray());
258     }
259   }
260 
261   /**
262    * We should be using the system package name as the source package if there is no endless VM/VM
263    * archive present on the device. This is to separate pre-O (no endless VM) and O+ (endless VM)
264    * devices. This ensures that the source of truth for VMs is the VM server when endless VM is not
265    * enabled, and when endless VM/archived VMs is present, the source of truth for VMs is the device
266    * itself.
267    */
getSourcePackage(Context context, VoicemailInfo voicemailInfo)268   private static String getSourcePackage(Context context, VoicemailInfo voicemailInfo) {
269     if (ConfigProviderBindings.get(context)
270         .getBoolean("voicemail_restore_force_system_source_package", false)) {
271       LogUtil.i("DialerBackupUtils.getSourcePackage", "forcing system source package");
272       return "com.android.phone";
273     }
274     if (ConfigProviderBindings.get(context)
275         .getBoolean("voicemail_restore_check_archive_for_source_package", true)) {
276       if ("1".equals(voicemailInfo.getArchived())) {
277         LogUtil.i(
278             "DialerBackupUtils.getSourcePackage",
279             "voicemail was archived, using app source package");
280         // Using our app's source package will prevent the archived voicemail from being deleted by
281         // the system when it syncs with the voicemail server. In most cases the user will not see
282         // duplicate voicemails because this voicemail was archived and likely deleted from the
283         // voicemail server.
284         return context.getPackageName();
285       } else {
286         // Use the system source package. This means that if the voicemail is not present on the
287         // voicemail server then the system will delete it when it syncs.
288         LogUtil.i(
289             "DialerBackupUtils.getSourcePackage",
290             "voicemail was not archived, using system source package");
291         return "com.android.phone";
292       }
293     }
294     // Use our app's source package. This means that if the system syncs voicemail from the server
295     // the user could potentially get duplicate voicemails.
296     LogUtil.i("DialerBackupUtils.getSourcePackage", "defaulting to using app source package");
297     return context.getPackageName();
298   }
299 
300   @TargetApi(VERSION_CODES.M)
isDuplicate(Context context, VoicemailInfo voicemailInfo)301   private static boolean isDuplicate(Context context, VoicemailInfo voicemailInfo) {
302     // This checks for VM that might already exist, and doesn't restore them
303     try (Cursor cursor =
304         context
305             .getContentResolver()
306             .query(
307                 VoicemailContract.Voicemails.CONTENT_URI,
308                 null,
309                 String.format(
310                     "(%s = ? AND %s = ? AND %s = ?)",
311                     Voicemails.NUMBER, Voicemails.DATE, Voicemails.DURATION),
312                 new String[] {
313                   voicemailInfo.getNumber(), voicemailInfo.getDate(), voicemailInfo.getDuration()
314                 },
315                 null,
316                 null)) {
317       if (cursor.moveToFirst()
318           && ConfigProviderBindings.get(context)
319               .getBoolean("enable_vm_restore_no_duplicate", true)) {
320         return true;
321       }
322     }
323     return false;
324   }
325 
getPhoneAccountClause(List<PhoneAccountHandle> phoneAccountsToArchive)326   public static String getPhoneAccountClause(List<PhoneAccountHandle> phoneAccountsToArchive) {
327     Assert.checkArgument(!phoneAccountsToArchive.isEmpty());
328     StringBuilder whereQuery = new StringBuilder();
329 
330     whereQuery.append("(");
331 
332     for (int i = 0; i < phoneAccountsToArchive.size(); i++) {
333       whereQuery.append(
334           Voicemails.PHONE_ACCOUNT_ID + " = " + phoneAccountsToArchive.get(i).getId());
335 
336       if (phoneAccountsToArchive.size() > 1 && i < phoneAccountsToArchive.size() - 1) {
337         whereQuery.append(" OR ");
338       }
339     }
340     whereQuery.append(")");
341     return whereQuery.toString();
342   }
343 
getPhoneAccountsToArchive(Context context)344   public static List<PhoneAccountHandle> getPhoneAccountsToArchive(Context context) {
345     List<PhoneAccountHandle> phoneAccountsToBackUp = new ArrayList<>();
346 
347     for (PhoneAccountHandle handle :
348         context.getSystemService(TelecomManager.class).getCallCapablePhoneAccounts()) {
349 
350       if (VoicemailComponent.get(context)
351           .getVoicemailClient()
352           .isVoicemailArchiveEnabled(context, handle)) {
353         phoneAccountsToBackUp.add(handle);
354         LogUtil.i(
355             "DialerBackupUtils.getPhoneAccountsToArchive", "enabled for: " + handle.toString());
356       } else {
357         LogUtil.i(
358             "DialerBackupUtils.getPhoneAccountsToArchive", "not enabled for: " + handle.toString());
359       }
360     }
361     return phoneAccountsToBackUp;
362   }
363 }
364