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