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.telephony; 18 19 import android.annotation.NonNull; 20 import android.annotation.TargetApi; 21 import android.app.AlarmManager; 22 import android.app.IntentService; 23 import android.app.backup.BackupAgent; 24 import android.app.backup.BackupDataInput; 25 import android.app.backup.BackupDataOutput; 26 import android.app.backup.FullBackupDataOutput; 27 import android.content.ContentResolver; 28 import android.content.ContentUris; 29 import android.content.ContentValues; 30 import android.content.Context; 31 import android.content.Intent; 32 import android.content.SharedPreferences; 33 import android.database.Cursor; 34 import android.database.DatabaseUtils; 35 import android.net.Uri; 36 import android.os.Build; 37 import android.os.ParcelFileDescriptor; 38 import android.os.PowerManager; 39 import android.preference.PreferenceManager; 40 import android.provider.BaseColumns; 41 import android.provider.Telephony; 42 import android.telephony.PhoneNumberUtils; 43 import android.telephony.SubscriptionInfo; 44 import android.telephony.SubscriptionManager; 45 import android.text.TextUtils; 46 import android.util.ArrayMap; 47 import android.util.ArraySet; 48 import android.util.JsonReader; 49 import android.util.JsonWriter; 50 import android.util.Log; 51 import android.util.SparseArray; 52 53 import com.android.internal.annotations.VisibleForTesting; 54 import com.android.internal.telephony.PhoneFactory; 55 56 import com.google.android.mms.ContentType; 57 import com.google.android.mms.pdu.CharacterSets; 58 59 import java.io.BufferedWriter; 60 import java.io.File; 61 import java.io.FileDescriptor; 62 import java.io.FileFilter; 63 import java.io.FileInputStream; 64 import java.io.IOException; 65 import java.io.InputStreamReader; 66 import java.io.OutputStreamWriter; 67 import java.util.ArrayList; 68 import java.util.Arrays; 69 import java.util.Comparator; 70 import java.util.HashMap; 71 import java.util.List; 72 import java.util.Locale; 73 import java.util.Map; 74 import java.util.Set; 75 import java.util.concurrent.TimeUnit; 76 import java.util.zip.DeflaterOutputStream; 77 import java.util.zip.InflaterInputStream; 78 79 /*** 80 * Backup agent for backup and restore SMS's and text MMS's. 81 * 82 * This backup agent stores SMS's into "sms_backup" file as a JSON array. Example below. 83 * [{"self_phone":"+1234567891011","address":"+1234567891012","body":"Example sms", 84 * "date":"1450893518140","date_sent":"1450893514000","status":"-1","type":"1"}, 85 * {"self_phone":"+1234567891011","address":"12345","body":"Example 2","date":"1451328022316", 86 * "date_sent":"1451328018000","status":"-1","type":"1"}] 87 * 88 * Text MMS's are stored into "mms_backup" file as a JSON array. Example below. 89 * [{"self_phone":"+1234567891011","date":"1451322716","date_sent":"0","m_type":"128","v":"18", 90 * "msg_box":"2","mms_addresses":[{"type":137,"address":"+1234567891011","charset":106}, 91 * {"type":151,"address":"example@example.com","charset":106}],"mms_body":"Mms to email", 92 * "mms_charset":106}, 93 * {"self_phone":"+1234567891011","sub":"MMS subject","date":"1451322955","date_sent":"0", 94 * "m_type":"132","v":"17","msg_box":"1","ct_l":"http://promms/servlets/NOK5BBqgUHAqugrQNM", 95 * "mms_addresses":[{"type":151,"address":"+1234567891011","charset":106}], 96 * "mms_body":"Mms\nBody\r\n", 97 * "attachments":[{"mime_type":"image/jpeg","filename":"image000000.jpg"}], 98 * "smil":"<smil><head><layout><root-layout/><region id='Image' fit='meet' top='0' left='0' 99 * height='100%' width='100%'/></layout></head><body><par dur='5000ms'><img src='image000000.jpg' 100 * region='Image' /></par></body></smil>", 101 * "mms_charset":106,"sub_cs":"106"}] 102 * 103 * It deflates the files on the flight. 104 * Every 1000 messages it backs up file, deletes it and creates a new one with the same name. 105 * 106 * It stores how many bytes we are over the quota and don't backup the oldest messages. 107 * 108 * NOTE: presently, only MMS's with text are backed up. However, MMS's with attachments are 109 * restored. In other words, this code can restore MMS attachments if the attachment data 110 * is in the json, but it doesn't currently backup the attachment data in the json. 111 */ 112 113 @TargetApi(Build.VERSION_CODES.M) 114 public class TelephonyBackupAgent extends BackupAgent { 115 private static final String TAG = "TelephonyBackupAgent"; 116 private static final boolean DEBUG = false; 117 private static volatile boolean sIsRestoring; 118 119 // SharedPreferences keys 120 private static final String NUM_SMS_RESTORED = "num_sms_restored"; 121 private static final String NUM_SMS_EXCEPTIONS = "num_sms_exceptions"; 122 private static final String NUM_SMS_FILES_STORED = "num_sms_files_restored"; 123 private static final String NUM_SMS_FILES_WITH_EXCEPTIONS = "num_sms_files_with_exceptions"; 124 private static final String NUM_MMS_RESTORED = "num_mms_restored"; 125 private static final String NUM_MMS_EXCEPTIONS = "num_mms_exceptions"; 126 private static final String NUM_MMS_FILES_STORED = "num_mms_files_restored"; 127 private static final String NUM_MMS_FILES_WITH_EXCEPTIONS = "num_mms_files_with_exceptions"; 128 129 // Copied from packages/apps/Messaging/src/com/android/messaging/sms/MmsUtils.java. 130 private static final int DEFAULT_DURATION = 5000; //ms 131 132 // Copied from packages/apps/Messaging/src/com/android/messaging/sms/MmsUtils.java. 133 @VisibleForTesting 134 static final String sSmilTextOnly = 135 "<smil>" + 136 "<head>" + 137 "<layout>" + 138 "<root-layout/>" + 139 "<region id=\"Text\" top=\"0\" left=\"0\" " 140 + "height=\"100%%\" width=\"100%%\"/>" + 141 "</layout>" + 142 "</head>" + 143 "<body>" + 144 "%s" + // constructed body goes here 145 "</body>" + 146 "</smil>"; 147 148 // Copied from packages/apps/Messaging/src/com/android/messaging/sms/MmsUtils.java. 149 @VisibleForTesting 150 static final String sSmilTextPart = 151 "<par dur=\"" + DEFAULT_DURATION + "ms\">" + 152 "<text src=\"%s\" region=\"Text\" />" + 153 "</par>"; 154 155 156 // JSON key for phone number a message was sent from or received to. 157 private static final String SELF_PHONE_KEY = "self_phone"; 158 // JSON key for list of addresses of MMS message. 159 private static final String MMS_ADDRESSES_KEY = "mms_addresses"; 160 // JSON key for list of attachments of MMS message. 161 private static final String MMS_ATTACHMENTS_KEY = "attachments"; 162 // JSON key for SMIL part of the MMS. 163 private static final String MMS_SMIL_KEY = "smil"; 164 // JSON key for list of recipients of the message. 165 private static final String RECIPIENTS = "recipients"; 166 // JSON key for MMS body. 167 private static final String MMS_BODY_KEY = "mms_body"; 168 // JSON key for MMS charset. 169 private static final String MMS_BODY_CHARSET_KEY = "mms_charset"; 170 // JSON key for mime type. 171 private static final String MMS_MIME_TYPE = "mime_type"; 172 // JSON key for attachment filename. 173 private static final String MMS_ATTACHMENT_FILENAME = "filename"; 174 175 // File names suffixes for backup/restore. 176 private static final String SMS_BACKUP_FILE_SUFFIX = "_sms_backup"; 177 private static final String MMS_BACKUP_FILE_SUFFIX = "_mms_backup"; 178 179 // File name formats for backup. It looks like 000000_sms_backup, 000001_sms_backup, etc. 180 private static final String SMS_BACKUP_FILE_FORMAT = "%06d"+SMS_BACKUP_FILE_SUFFIX; 181 private static final String MMS_BACKUP_FILE_FORMAT = "%06d"+MMS_BACKUP_FILE_SUFFIX; 182 183 // Charset being used for reading/writing backup files. 184 private static final String CHARSET_UTF8 = "UTF-8"; 185 186 // Order by ID entries from database. 187 private static final String ORDER_BY_ID = BaseColumns._ID + " ASC"; 188 189 // Order by Date entries from database. We start backup from the oldest. 190 private static final String ORDER_BY_DATE = "date ASC"; 191 192 // This is a hard coded string rather than a localized one because we don't want it to 193 // change when you change locale. 194 @VisibleForTesting 195 static final String UNKNOWN_SENDER = "\u02BCUNKNOWN_SENDER!\u02BC"; 196 197 private static String ATTACHMENT_DATA_PATH = "/app_parts/"; 198 199 // Thread id for UNKNOWN_SENDER. 200 private long mUnknownSenderThreadId; 201 202 // Columns from SMS database for backup/restore. 203 @VisibleForTesting 204 static final String[] SMS_PROJECTION = new String[] { 205 Telephony.Sms._ID, 206 Telephony.Sms.SUBSCRIPTION_ID, 207 Telephony.Sms.ADDRESS, 208 Telephony.Sms.BODY, 209 Telephony.Sms.SUBJECT, 210 Telephony.Sms.DATE, 211 Telephony.Sms.DATE_SENT, 212 Telephony.Sms.STATUS, 213 Telephony.Sms.TYPE, 214 Telephony.Sms.THREAD_ID, 215 Telephony.Sms.READ 216 }; 217 218 // Columns to fetch recepients of SMS. 219 private static final String[] SMS_RECIPIENTS_PROJECTION = { 220 Telephony.Threads._ID, 221 Telephony.Threads.RECIPIENT_IDS 222 }; 223 224 // Columns from MMS database for backup/restore. 225 @VisibleForTesting 226 static final String[] MMS_PROJECTION = new String[] { 227 Telephony.Mms._ID, 228 Telephony.Mms.SUBSCRIPTION_ID, 229 Telephony.Mms.SUBJECT, 230 Telephony.Mms.SUBJECT_CHARSET, 231 Telephony.Mms.DATE, 232 Telephony.Mms.DATE_SENT, 233 Telephony.Mms.MESSAGE_TYPE, 234 Telephony.Mms.MMS_VERSION, 235 Telephony.Mms.MESSAGE_BOX, 236 Telephony.Mms.CONTENT_LOCATION, 237 Telephony.Mms.THREAD_ID, 238 Telephony.Mms.TRANSACTION_ID, 239 Telephony.Mms.READ 240 }; 241 242 // Columns from addr database for backup/restore. This database is used for fetching addresses 243 // for MMS message. 244 @VisibleForTesting 245 static final String[] MMS_ADDR_PROJECTION = new String[] { 246 Telephony.Mms.Addr.TYPE, 247 Telephony.Mms.Addr.ADDRESS, 248 Telephony.Mms.Addr.CHARSET 249 }; 250 251 // Columns from part database for backup/restore. This database is used for fetching body text 252 // and charset for MMS message. 253 @VisibleForTesting 254 static final String[] MMS_TEXT_PROJECTION = new String[] { 255 Telephony.Mms.Part.TEXT, 256 Telephony.Mms.Part.CHARSET 257 }; 258 static final int MMS_TEXT_IDX = 0; 259 static final int MMS_TEXT_CHARSET_IDX = 1; 260 261 // Buffer size for Json writer. 262 public static final int WRITER_BUFFER_SIZE = 32*1024; //32Kb 263 264 // We increase how many bytes backup size over quota by 10%, so we will fit into quota on next 265 // backup 266 public static final double BYTES_OVER_QUOTA_MULTIPLIER = 1.1; 267 268 // Maximum messages for one backup file. After reaching the limit the agent backs up the file, 269 // deletes it and creates a new one with the same name. 270 // Not final for the testing. 271 @VisibleForTesting 272 int mMaxMsgPerFile = 1000; 273 274 // Default values for SMS, MMS, Addresses restore. 275 private static ContentValues sDefaultValuesSms = new ContentValues(5); 276 private static ContentValues sDefaultValuesMms = new ContentValues(6); 277 private static final ContentValues sDefaultValuesAddr = new ContentValues(2); 278 private static final ContentValues sDefaultValuesAttachments = new ContentValues(2); 279 280 // Shared preferences for the backup agent. 281 private static final String BACKUP_PREFS = "backup_shared_prefs"; 282 // Key for storing quota bytes. 283 private static final String QUOTA_BYTES = "backup_quota_bytes"; 284 // Key for storing backup data size. 285 private static final String BACKUP_DATA_BYTES = "backup_data_bytes"; 286 // Key for storing timestamp when backup agent resets quota. It does that to get onQuotaExceeded 287 // call so it could get the new quota if it changed. 288 private static final String QUOTA_RESET_TIME = "reset_quota_time"; 289 private static final long QUOTA_RESET_INTERVAL = 30 * AlarmManager.INTERVAL_DAY; // 30 days. 290 291 292 static { 293 // Consider restored messages read and seen by default. The actual data can override 294 // these values. sDefaultValuesSms.put(Telephony.Sms.READ, 1)295 sDefaultValuesSms.put(Telephony.Sms.READ, 1); sDefaultValuesSms.put(Telephony.Sms.SEEN, 1)296 sDefaultValuesSms.put(Telephony.Sms.SEEN, 1); sDefaultValuesSms.put(Telephony.Sms.ADDRESS, UNKNOWN_SENDER)297 sDefaultValuesSms.put(Telephony.Sms.ADDRESS, UNKNOWN_SENDER); 298 // If there is no sub_id with self phone number on restore set it to -1. sDefaultValuesSms.put(Telephony.Sms.SUBSCRIPTION_ID, -1)299 sDefaultValuesSms.put(Telephony.Sms.SUBSCRIPTION_ID, -1); 300 sDefaultValuesMms.put(Telephony.Mms.READ, 1)301 sDefaultValuesMms.put(Telephony.Mms.READ, 1); sDefaultValuesMms.put(Telephony.Mms.SEEN, 1)302 sDefaultValuesMms.put(Telephony.Mms.SEEN, 1); sDefaultValuesMms.put(Telephony.Mms.SUBSCRIPTION_ID, -1)303 sDefaultValuesMms.put(Telephony.Mms.SUBSCRIPTION_ID, -1); sDefaultValuesMms.put(Telephony.Mms.MESSAGE_BOX, Telephony.Mms.MESSAGE_BOX_ALL)304 sDefaultValuesMms.put(Telephony.Mms.MESSAGE_BOX, Telephony.Mms.MESSAGE_BOX_ALL); sDefaultValuesMms.put(Telephony.Mms.TEXT_ONLY, 1)305 sDefaultValuesMms.put(Telephony.Mms.TEXT_ONLY, 1); 306 sDefaultValuesAddr.put(Telephony.Mms.Addr.TYPE, 0)307 sDefaultValuesAddr.put(Telephony.Mms.Addr.TYPE, 0); sDefaultValuesAddr.put(Telephony.Mms.Addr.CHARSET, CharacterSets.DEFAULT_CHARSET)308 sDefaultValuesAddr.put(Telephony.Mms.Addr.CHARSET, CharacterSets.DEFAULT_CHARSET); 309 } 310 311 312 private SparseArray<String> mSubId2phone = new SparseArray<String>(); 313 private Map<String, Integer> mPhone2subId = new ArrayMap<String, Integer>(); 314 private Map<Long, Boolean> mThreadArchived = new HashMap<>(); 315 316 private ContentResolver mContentResolver; 317 // How many bytes we can backup to fit into quota. 318 private long mBytesOverQuota; 319 320 // Cache list of recipients by threadId. It reduces db requests heavily. Used during backup. 321 @VisibleForTesting 322 Map<Long, List<String>> mCacheRecipientsByThread = null; 323 // Cache threadId by list of recipients. Used during restore. 324 @VisibleForTesting 325 Map<Set<String>, Long> mCacheGetOrCreateThreadId = null; 326 327 @Override onCreate()328 public void onCreate() { 329 super.onCreate(); 330 Log.d(TAG, "onCreate"); 331 final SubscriptionManager subscriptionManager = SubscriptionManager.from(this); 332 if (subscriptionManager != null) { 333 final List<SubscriptionInfo> subInfo = 334 subscriptionManager.getCompleteActiveSubscriptionInfoList(); 335 if (subInfo != null) { 336 for (SubscriptionInfo sub : subInfo) { 337 final String phoneNumber = getNormalizedNumber(sub); 338 mSubId2phone.append(sub.getSubscriptionId(), phoneNumber); 339 mPhone2subId.put(phoneNumber, sub.getSubscriptionId()); 340 } 341 } 342 } 343 mContentResolver = getContentResolver(); 344 initUnknownSender(); 345 } 346 347 @VisibleForTesting setContentResolver(ContentResolver contentResolver)348 void setContentResolver(ContentResolver contentResolver) { 349 mContentResolver = contentResolver; 350 } 351 @VisibleForTesting setSubId(SparseArray<String> subId2Phone, Map<String, Integer> phone2subId)352 void setSubId(SparseArray<String> subId2Phone, Map<String, Integer> phone2subId) { 353 mSubId2phone = subId2Phone; 354 mPhone2subId = phone2subId; 355 } 356 357 @VisibleForTesting initUnknownSender()358 void initUnknownSender() { 359 mUnknownSenderThreadId = getOrCreateThreadId(null); 360 sDefaultValuesSms.put(Telephony.Sms.THREAD_ID, mUnknownSenderThreadId); 361 sDefaultValuesMms.put(Telephony.Mms.THREAD_ID, mUnknownSenderThreadId); 362 } 363 364 @Override onFullBackup(FullBackupDataOutput data)365 public void onFullBackup(FullBackupDataOutput data) throws IOException { 366 SharedPreferences sharedPreferences = getSharedPreferences(BACKUP_PREFS, MODE_PRIVATE); 367 if (sharedPreferences.getLong(QUOTA_RESET_TIME, Long.MAX_VALUE) < 368 System.currentTimeMillis()) { 369 clearSharedPreferences(); 370 } 371 372 mBytesOverQuota = sharedPreferences.getLong(BACKUP_DATA_BYTES, 0) - 373 sharedPreferences.getLong(QUOTA_BYTES, Long.MAX_VALUE); 374 if (mBytesOverQuota > 0) { 375 mBytesOverQuota *= BYTES_OVER_QUOTA_MULTIPLIER; 376 } 377 378 try ( 379 Cursor smsCursor = mContentResolver.query(Telephony.Sms.CONTENT_URI, SMS_PROJECTION, 380 null, null, ORDER_BY_DATE); 381 Cursor mmsCursor = mContentResolver.query(Telephony.Mms.CONTENT_URI, MMS_PROJECTION, 382 null, null, ORDER_BY_DATE)) { 383 384 if (smsCursor != null) { 385 smsCursor.moveToFirst(); 386 } 387 if (mmsCursor != null) { 388 mmsCursor.moveToFirst(); 389 } 390 391 // It backs up messages from the oldest to newest. First it looks at the timestamp of 392 // the next SMS messages and MMS message. If the SMS is older it backs up 1000 SMS 393 // messages, otherwise 1000 MMS messages. Repeat until out of SMS's or MMS's. 394 // It ensures backups are incremental. 395 int fileNum = 0; 396 while (smsCursor != null && !smsCursor.isAfterLast() && 397 mmsCursor != null && !mmsCursor.isAfterLast()) { 398 final long smsDate = TimeUnit.MILLISECONDS.toSeconds(getMessageDate(smsCursor)); 399 final long mmsDate = getMessageDate(mmsCursor); 400 if (smsDate < mmsDate) { 401 backupAll(data, smsCursor, 402 String.format(Locale.US, SMS_BACKUP_FILE_FORMAT, fileNum++)); 403 } else { 404 backupAll(data, mmsCursor, String.format(Locale.US, 405 MMS_BACKUP_FILE_FORMAT, fileNum++)); 406 } 407 } 408 409 while (smsCursor != null && !smsCursor.isAfterLast()) { 410 backupAll(data, smsCursor, 411 String.format(Locale.US, SMS_BACKUP_FILE_FORMAT, fileNum++)); 412 } 413 414 while (mmsCursor != null && !mmsCursor.isAfterLast()) { 415 backupAll(data, mmsCursor, 416 String.format(Locale.US, MMS_BACKUP_FILE_FORMAT, fileNum++)); 417 } 418 } 419 420 mThreadArchived = new HashMap<>(); 421 } 422 423 @VisibleForTesting clearSharedPreferences()424 void clearSharedPreferences() { 425 getSharedPreferences(BACKUP_PREFS, MODE_PRIVATE).edit() 426 .remove(BACKUP_DATA_BYTES) 427 .remove(QUOTA_BYTES) 428 .remove(QUOTA_RESET_TIME) 429 .apply(); 430 } 431 getMessageDate(Cursor cursor)432 private static long getMessageDate(Cursor cursor) { 433 return cursor.getLong(cursor.getColumnIndex(Telephony.Sms.DATE)); 434 } 435 436 @Override onQuotaExceeded(long backupDataBytes, long quotaBytes)437 public void onQuotaExceeded(long backupDataBytes, long quotaBytes) { 438 SharedPreferences sharedPreferences = getSharedPreferences(BACKUP_PREFS, MODE_PRIVATE); 439 if (sharedPreferences.contains(BACKUP_DATA_BYTES) 440 && sharedPreferences.contains(QUOTA_BYTES)) { 441 // Increase backup size by the size we skipped during previous backup. 442 backupDataBytes += (sharedPreferences.getLong(BACKUP_DATA_BYTES, 0) 443 - sharedPreferences.getLong(QUOTA_BYTES, 0)) * BYTES_OVER_QUOTA_MULTIPLIER; 444 } 445 sharedPreferences.edit() 446 .putLong(BACKUP_DATA_BYTES, backupDataBytes) 447 .putLong(QUOTA_BYTES, quotaBytes) 448 .putLong(QUOTA_RESET_TIME, System.currentTimeMillis() + QUOTA_RESET_INTERVAL) 449 .apply(); 450 } 451 backupAll(FullBackupDataOutput data, Cursor cursor, String fileName)452 private void backupAll(FullBackupDataOutput data, Cursor cursor, String fileName) 453 throws IOException { 454 if (cursor == null || cursor.isAfterLast()) { 455 return; 456 } 457 458 // Backups consist of multiple chunks; each chunk consists of a set of messages 459 // of the same type in a chronological order. 460 BackupChunkInformation chunk; 461 try (JsonWriter jsonWriter = getJsonWriter(fileName)) { 462 if (fileName.endsWith(SMS_BACKUP_FILE_SUFFIX)) { 463 chunk = putSmsMessagesToJson(cursor, jsonWriter); 464 } else { 465 chunk = putMmsMessagesToJson(cursor, jsonWriter); 466 } 467 } 468 backupFile(chunk, fileName, data); 469 } 470 471 @VisibleForTesting 472 @NonNull putMmsMessagesToJson(Cursor cursor, JsonWriter jsonWriter)473 BackupChunkInformation putMmsMessagesToJson(Cursor cursor, 474 JsonWriter jsonWriter) throws IOException { 475 BackupChunkInformation results = new BackupChunkInformation(); 476 jsonWriter.beginArray(); 477 for (; results.count < mMaxMsgPerFile && !cursor.isAfterLast(); 478 cursor.moveToNext()) { 479 writeMmsToWriter(jsonWriter, cursor, results); 480 } 481 jsonWriter.endArray(); 482 return results; 483 } 484 485 @VisibleForTesting 486 @NonNull putSmsMessagesToJson(Cursor cursor, JsonWriter jsonWriter)487 BackupChunkInformation putSmsMessagesToJson(Cursor cursor, JsonWriter jsonWriter) 488 throws IOException { 489 BackupChunkInformation results = new BackupChunkInformation(); 490 jsonWriter.beginArray(); 491 for (; results.count < mMaxMsgPerFile && !cursor.isAfterLast(); 492 ++results.count, cursor.moveToNext()) { 493 writeSmsToWriter(jsonWriter, cursor, results); 494 } 495 jsonWriter.endArray(); 496 return results; 497 } 498 backupFile(BackupChunkInformation chunkInformation, String fileName, FullBackupDataOutput data)499 private void backupFile(BackupChunkInformation chunkInformation, String fileName, 500 FullBackupDataOutput data) 501 throws IOException { 502 final File file = new File(getFilesDir().getPath() + "/" + fileName); 503 file.setLastModified(chunkInformation.timestamp); 504 try { 505 if (chunkInformation.count > 0) { 506 if (mBytesOverQuota > 0) { 507 mBytesOverQuota -= file.length(); 508 return; 509 } 510 super.fullBackupFile(file, data); 511 } 512 } finally { 513 file.delete(); 514 } 515 } 516 517 public static class DeferredSmsMmsRestoreService extends IntentService { 518 private static final String TAG = "DeferredSmsMmsRestoreService"; 519 private static boolean sSharedPrefsAddedToLocalLogs = false; 520 addAllSharedPrefToLocalLog(Context context)521 public static void addAllSharedPrefToLocalLog(Context context) { 522 if (sSharedPrefsAddedToLocalLogs) return; 523 localLog("addAllSharedPrefToLocalLog"); 524 SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context); 525 Map<String, ?> allPref = sp.getAll(); 526 if (allPref.keySet() == null || allPref.keySet().size() == 0) return; 527 for (String key : allPref.keySet()) { 528 try { 529 localLog(key + ":" + allPref.get(key).toString()); 530 } catch (Exception e) { 531 localLog("Skipping over key " + key + " due to exception " + e); 532 } 533 } 534 sSharedPrefsAddedToLocalLogs = true; 535 } 536 localLog(String logMsg)537 public static void localLog(String logMsg) { 538 Log.d(TAG, logMsg); 539 PhoneFactory.localLog(TAG, logMsg); 540 } 541 542 private final Comparator<File> mFileComparator = new Comparator<File>() { 543 @Override 544 public int compare(File lhs, File rhs) { 545 return rhs.getName().compareTo(lhs.getName()); 546 } 547 }; 548 DeferredSmsMmsRestoreService()549 public DeferredSmsMmsRestoreService() { 550 super(TAG); 551 Log.d(TAG, "DeferredSmsMmsRestoreService"); 552 setIntentRedelivery(true); 553 } 554 555 private TelephonyBackupAgent mTelephonyBackupAgent; 556 private PowerManager.WakeLock mWakeLock; 557 558 @Override onHandleIntent(Intent intent)559 protected void onHandleIntent(Intent intent) { 560 Log.d(TAG, "onHandleIntent"); 561 try { 562 mWakeLock.acquire(); 563 sIsRestoring = true; 564 565 File[] files = getFilesToRestore(this); 566 567 if (files == null || files.length == 0) { 568 return; 569 } 570 Arrays.sort(files, mFileComparator); 571 572 boolean didRestore = false; 573 574 for (File file : files) { 575 final String fileName = file.getName(); 576 Log.d(TAG, "onHandleIntent restoring file " + fileName); 577 try (FileInputStream fileInputStream = new FileInputStream(file)) { 578 mTelephonyBackupAgent.doRestoreFile(fileName, fileInputStream.getFD()); 579 didRestore = true; 580 } catch (Exception e) { 581 // Either IOException or RuntimeException. 582 Log.e(TAG, "onHandleIntent", e); 583 localLog("onHandleIntent: Exception " + e); 584 } finally { 585 file.delete(); 586 } 587 } 588 if (didRestore) { 589 // Tell the default sms app to do a full sync now that the messages have been 590 // restored. 591 localLog("onHandleIntent: done - notifying default sms app"); 592 ProviderUtil.notifyIfNotDefaultSmsApp(null /*uri*/, null /*calling package*/, 593 this); 594 } 595 } finally { 596 addAllSharedPrefToLocalLog(this); 597 sIsRestoring = false; 598 mWakeLock.release(); 599 } 600 } 601 602 @Override onCreate()603 public void onCreate() { 604 super.onCreate(); 605 Log.d(TAG, "onCreate"); 606 try { 607 PhoneFactory.addLocalLog(TAG, 32); 608 } catch (IllegalArgumentException e) { 609 // ignore 610 } 611 mTelephonyBackupAgent = new TelephonyBackupAgent(); 612 mTelephonyBackupAgent.attach(this); 613 mTelephonyBackupAgent.onCreate(); 614 615 PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); 616 mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG); 617 } 618 619 @Override onDestroy()620 public void onDestroy() { 621 if (mTelephonyBackupAgent != null) { 622 mTelephonyBackupAgent.onDestroy(); 623 mTelephonyBackupAgent = null; 624 } 625 super.onDestroy(); 626 } 627 startIfFilesExist(Context context)628 static void startIfFilesExist(Context context) { 629 try { 630 PhoneFactory.addLocalLog(TAG, 32); 631 } catch (IllegalArgumentException e) { 632 // ignore 633 } 634 File[] files = getFilesToRestore(context); 635 if (files == null || files.length == 0) { 636 Log.d(TAG, "startIfFilesExist: no files to restore"); 637 addAllSharedPrefToLocalLog(context); 638 return; 639 } 640 context.startService(new Intent(context, DeferredSmsMmsRestoreService.class)); 641 } 642 getFilesToRestore(Context context)643 private static File[] getFilesToRestore(Context context) { 644 return context.getFilesDir().listFiles(new FileFilter() { 645 @Override 646 public boolean accept(File file) { 647 return file.getName().endsWith(SMS_BACKUP_FILE_SUFFIX) || 648 file.getName().endsWith(MMS_BACKUP_FILE_SUFFIX); 649 } 650 }); 651 } 652 } 653 654 @Override 655 public void onRestoreFinished() { 656 super.onRestoreFinished(); 657 DeferredSmsMmsRestoreService.startIfFilesExist(this); 658 } 659 660 private void doRestoreFile(String fileName, FileDescriptor fd) throws IOException { 661 Log.d(TAG, "Restoring file " + fileName); 662 663 try (JsonReader jsonReader = getJsonReader(fd)) { 664 if (fileName.endsWith(SMS_BACKUP_FILE_SUFFIX)) { 665 Log.d(TAG, "Restoring SMS"); 666 putSmsMessagesToProvider(jsonReader); 667 } else if (fileName.endsWith(MMS_BACKUP_FILE_SUFFIX)) { 668 Log.d(TAG, "Restoring text MMS"); 669 putMmsMessagesToProvider(jsonReader); 670 } else { 671 DeferredSmsMmsRestoreService.localLog("Unknown file to restore:" + fileName); 672 } 673 } 674 } 675 676 @VisibleForTesting 677 void putSmsMessagesToProvider(JsonReader jsonReader) throws IOException { 678 jsonReader.beginArray(); 679 int msgCount = 0; 680 int numExceptions = 0; 681 final int bulkInsertSize = mMaxMsgPerFile; 682 ContentValues[] values = new ContentValues[bulkInsertSize]; 683 while (jsonReader.hasNext()) { 684 ContentValues cv = readSmsValuesFromReader(jsonReader); 685 try { 686 if (mSmsProviderQuery.doesSmsExist(cv)) { 687 continue; 688 } 689 values[(msgCount++) % bulkInsertSize] = cv; 690 if (msgCount % bulkInsertSize == 0) { 691 mContentResolver.bulkInsert(Telephony.Sms.CONTENT_URI, values); 692 } 693 } catch (RuntimeException e) { 694 Log.e(TAG, "putSmsMessagesToProvider", e); 695 DeferredSmsMmsRestoreService.localLog("putSmsMessagesToProvider: Exception " + e); 696 numExceptions++; 697 } 698 } 699 if (msgCount % bulkInsertSize > 0) { 700 mContentResolver.bulkInsert(Telephony.Sms.CONTENT_URI, 701 Arrays.copyOf(values, msgCount % bulkInsertSize)); 702 } 703 jsonReader.endArray(); 704 incremenentSharedPref(true, msgCount, numExceptions); 705 } 706 707 void incremenentSharedPref(boolean sms, int msgCount, int numExceptions) { 708 SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this); 709 SharedPreferences.Editor editor = sp.edit(); 710 if (sms) { 711 editor.putInt(NUM_SMS_RESTORED, sp.getInt(NUM_SMS_RESTORED, 0) + msgCount); 712 editor.putInt(NUM_SMS_EXCEPTIONS, sp.getInt(NUM_SMS_EXCEPTIONS, 0) + numExceptions); 713 editor.putInt(NUM_SMS_FILES_STORED, sp.getInt(NUM_SMS_FILES_STORED, 0) + 1); 714 if (numExceptions > 0) { 715 editor.putInt(NUM_SMS_FILES_WITH_EXCEPTIONS, 716 sp.getInt(NUM_SMS_FILES_WITH_EXCEPTIONS, 0) + 1); 717 } 718 } else { 719 editor.putInt(NUM_MMS_RESTORED, sp.getInt(NUM_MMS_RESTORED, 0) + msgCount); 720 editor.putInt(NUM_MMS_EXCEPTIONS, sp.getInt(NUM_MMS_EXCEPTIONS, 0) + numExceptions); 721 editor.putInt(NUM_MMS_FILES_STORED, sp.getInt(NUM_MMS_FILES_STORED, 0) + 1); 722 if (numExceptions > 0) { 723 editor.putInt(NUM_MMS_FILES_WITH_EXCEPTIONS, 724 sp.getInt(NUM_MMS_FILES_WITH_EXCEPTIONS, 0) + 1); 725 } 726 } 727 editor.commit(); 728 } 729 730 @VisibleForTesting 731 void putMmsMessagesToProvider(JsonReader jsonReader) throws IOException { 732 jsonReader.beginArray(); 733 int total = 0; 734 int numExceptions = 0; 735 while (jsonReader.hasNext()) { 736 final Mms mms = readMmsFromReader(jsonReader); 737 if (DEBUG) { 738 Log.d(TAG, "putMmsMessagesToProvider " + mms); 739 } 740 try { 741 if (doesMmsExist(mms)) { 742 if (DEBUG) { 743 Log.e(TAG, String.format("Mms: %s already exists", mms.toString())); 744 } else { 745 Log.w(TAG, "Mms: Found duplicate MMS"); 746 } 747 continue; 748 } 749 total++; 750 addMmsMessage(mms); 751 } catch (Exception e) { 752 Log.e(TAG, "putMmsMessagesToProvider", e); 753 numExceptions++; 754 DeferredSmsMmsRestoreService.localLog("putMmsMessagesToProvider: Exception " + e); 755 } 756 } 757 Log.d(TAG, "putMmsMessagesToProvider handled " + total + " new messages."); 758 incremenentSharedPref(false, total, numExceptions); 759 } 760 761 @VisibleForTesting 762 static final String[] PROJECTION_ID = {BaseColumns._ID}; 763 private static final int ID_IDX = 0; 764 765 /** 766 * Interface to allow mocking method for testing. 767 */ 768 public interface SmsProviderQuery { 769 boolean doesSmsExist(ContentValues smsValues); 770 } 771 772 private SmsProviderQuery mSmsProviderQuery = new SmsProviderQuery() { 773 @Override 774 public boolean doesSmsExist(ContentValues smsValues) { 775 // The SMS body might contain '\0' characters (U+0000) such as in the case of 776 // http://b/160801497 . SQLite does not allow '\0' in String literals, but as of SQLite 777 // version 3.32.2 2020-06-04, it does allow them as selectionArgs; therefore, we're 778 // using the latter approach here. 779 final String selection = String.format(Locale.US, "%s=%d AND %s=?", 780 Telephony.Sms.DATE, smsValues.getAsLong(Telephony.Sms.DATE), 781 Telephony.Sms.BODY); 782 String[] selectionArgs = new String[] { smsValues.getAsString(Telephony.Sms.BODY)}; 783 try (Cursor cursor = mContentResolver.query(Telephony.Sms.CONTENT_URI, PROJECTION_ID, 784 selection, selectionArgs, null)) { 785 return cursor != null && cursor.getCount() > 0; 786 } 787 } 788 }; 789 790 /** 791 * Sets a temporary {@code SmsProviderQuery} for testing; note that this method 792 * is not thread safe. 793 * 794 * @return the previous {@code SmsProviderQuery} 795 */ 796 @VisibleForTesting 797 public SmsProviderQuery getAndSetSmsProviderQuery(SmsProviderQuery smsProviderQuery) { 798 SmsProviderQuery result = mSmsProviderQuery; 799 mSmsProviderQuery = smsProviderQuery; 800 return result; 801 } 802 803 private boolean doesMmsExist(Mms mms) { 804 final String where = String.format(Locale.US, "%s = %d", 805 Telephony.Sms.DATE, mms.values.getAsLong(Telephony.Mms.DATE)); 806 try (Cursor cursor = mContentResolver.query(Telephony.Mms.CONTENT_URI, PROJECTION_ID, where, 807 null, null)) { 808 if (cursor != null && cursor.moveToFirst()) { 809 do { 810 final int mmsId = cursor.getInt(ID_IDX); 811 final MmsBody body = getMmsBody(mmsId); 812 if (body != null && body.equals(mms.body)) { 813 return true; 814 } 815 } while (cursor.moveToNext()); 816 } 817 } 818 return false; 819 } 820 821 private static String getNormalizedNumber(SubscriptionInfo subscriptionInfo) { 822 if (subscriptionInfo == null) { 823 return null; 824 } 825 // country iso might not be always available in some corner cases (e.g. mis-configured SIM, 826 // carrier config, or test SIM has incorrect IMSI, etc...). In that case, just return the 827 // unformatted number. 828 if (!TextUtils.isEmpty(subscriptionInfo.getCountryIso())) { 829 return PhoneNumberUtils.formatNumberToE164(subscriptionInfo.getNumber(), 830 subscriptionInfo.getCountryIso().toUpperCase(Locale.US)); 831 } else { 832 return subscriptionInfo.getNumber(); 833 } 834 } 835 836 private void writeSmsToWriter(JsonWriter jsonWriter, Cursor cursor, 837 BackupChunkInformation chunk) throws IOException { 838 jsonWriter.beginObject(); 839 840 for (int i=0; i<cursor.getColumnCount(); ++i) { 841 final String name = cursor.getColumnName(i); 842 final String value = cursor.getString(i); 843 if (value == null) { 844 continue; 845 } 846 switch (name) { 847 case Telephony.Sms.SUBSCRIPTION_ID: 848 final int subId = cursor.getInt(i); 849 final String selfNumber = mSubId2phone.get(subId); 850 if (selfNumber != null) { 851 jsonWriter.name(SELF_PHONE_KEY).value(selfNumber); 852 } 853 break; 854 case Telephony.Sms.THREAD_ID: 855 final long threadId = cursor.getLong(i); 856 handleThreadId(jsonWriter, threadId); 857 break; 858 case Telephony.Sms._ID: 859 break; 860 case Telephony.Sms.DATE: 861 case Telephony.Sms.DATE_SENT: 862 chunk.timestamp = findNewestValue(chunk.timestamp, value); 863 jsonWriter.name(name).value(value); 864 break; 865 default: 866 jsonWriter.name(name).value(value); 867 break; 868 } 869 } 870 jsonWriter.endObject(); 871 } 872 873 private long findNewestValue(long current, String latest) { 874 if(latest == null) { 875 return current; 876 } 877 878 try { 879 long latestLong = Long.valueOf(latest); 880 return Math.max(current, latestLong); 881 } catch (NumberFormatException e) { 882 Log.d(TAG, "Unable to parse value "+latest); 883 return current; 884 } 885 886 } 887 888 private void handleThreadId(JsonWriter jsonWriter, long threadId) throws IOException { 889 final List<String> recipients = getRecipientsByThread(threadId); 890 if (recipients == null || recipients.isEmpty()) { 891 return; 892 } 893 894 writeRecipientsToWriter(jsonWriter.name(RECIPIENTS), recipients); 895 if (!mThreadArchived.containsKey(threadId)) { 896 boolean isArchived = isThreadArchived(threadId); 897 if (isArchived) { 898 jsonWriter.name(Telephony.Threads.ARCHIVED).value(true); 899 } 900 mThreadArchived.put(threadId, isArchived); 901 } 902 } 903 904 private static String[] THREAD_ARCHIVED_PROJECTION = 905 new String[] { Telephony.Threads.ARCHIVED }; 906 private static int THREAD_ARCHIVED_IDX = 0; 907 908 private boolean isThreadArchived(long threadId) { 909 Uri.Builder builder = Telephony.Threads.CONTENT_URI.buildUpon(); 910 builder.appendPath(String.valueOf(threadId)).appendPath("recipients"); 911 Uri uri = builder.build(); 912 913 try (Cursor cursor = getContentResolver().query(uri, THREAD_ARCHIVED_PROJECTION, null, null, 914 null)) { 915 if (cursor != null && cursor.moveToFirst()) { 916 return cursor.getInt(THREAD_ARCHIVED_IDX) == 1; 917 } 918 } 919 return false; 920 } 921 922 private static void writeRecipientsToWriter(JsonWriter jsonWriter, List<String> recipients) 923 throws IOException { 924 jsonWriter.beginArray(); 925 if (recipients != null) { 926 for (String s : recipients) { 927 jsonWriter.value(s); 928 } 929 } 930 jsonWriter.endArray(); 931 } 932 933 private ContentValues readSmsValuesFromReader(JsonReader jsonReader) 934 throws IOException { 935 ContentValues values = new ContentValues(6+sDefaultValuesSms.size()); 936 values.putAll(sDefaultValuesSms); 937 long threadId = -1; 938 boolean isArchived = false; 939 jsonReader.beginObject(); 940 while (jsonReader.hasNext()) { 941 String name = jsonReader.nextName(); 942 switch (name) { 943 case Telephony.Sms.BODY: 944 case Telephony.Sms.DATE: 945 case Telephony.Sms.DATE_SENT: 946 case Telephony.Sms.STATUS: 947 case Telephony.Sms.TYPE: 948 case Telephony.Sms.SUBJECT: 949 case Telephony.Sms.ADDRESS: 950 case Telephony.Sms.READ: 951 values.put(name, jsonReader.nextString()); 952 break; 953 case RECIPIENTS: 954 threadId = getOrCreateThreadId(getRecipients(jsonReader)); 955 values.put(Telephony.Sms.THREAD_ID, threadId); 956 break; 957 case Telephony.Threads.ARCHIVED: 958 isArchived = jsonReader.nextBoolean(); 959 break; 960 case SELF_PHONE_KEY: 961 final String selfPhone = jsonReader.nextString(); 962 if (mPhone2subId.containsKey(selfPhone)) { 963 values.put(Telephony.Sms.SUBSCRIPTION_ID, mPhone2subId.get(selfPhone)); 964 } 965 break; 966 default: 967 if (DEBUG) { 968 Log.w(TAG, "readSmsValuesFromReader Unknown name:" + name); 969 } else { 970 Log.w(TAG, "readSmsValuesFromReader encountered unknown name."); 971 } 972 jsonReader.skipValue(); 973 break; 974 } 975 } 976 jsonReader.endObject(); 977 archiveThread(threadId, isArchived); 978 return values; 979 } 980 981 private static Set<String> getRecipients(JsonReader jsonReader) throws IOException { 982 Set<String> recipients = new ArraySet<String>(); 983 jsonReader.beginArray(); 984 while (jsonReader.hasNext()) { 985 recipients.add(jsonReader.nextString()); 986 } 987 jsonReader.endArray(); 988 return recipients; 989 } 990 991 private void writeMmsToWriter(JsonWriter jsonWriter, Cursor cursor, 992 BackupChunkInformation chunk) throws IOException { 993 final int mmsId = cursor.getInt(ID_IDX); 994 final MmsBody body = getMmsBody(mmsId); 995 // We backup any message that contains text, but only backup the text part. 996 if (body == null || body.text == null) { 997 return; 998 } 999 1000 boolean subjectNull = true; 1001 jsonWriter.beginObject(); 1002 for (int i=0; i<cursor.getColumnCount(); ++i) { 1003 final String name = cursor.getColumnName(i); 1004 final String value = cursor.getString(i); 1005 if (DEBUG) { 1006 Log.d(TAG, "writeMmsToWriter name: " + name + " value: " + value); 1007 } 1008 if (value == null) { 1009 continue; 1010 } 1011 switch (name) { 1012 case Telephony.Mms.SUBSCRIPTION_ID: 1013 final int subId = cursor.getInt(i); 1014 final String selfNumber = mSubId2phone.get(subId); 1015 if (selfNumber != null) { 1016 jsonWriter.name(SELF_PHONE_KEY).value(selfNumber); 1017 } 1018 break; 1019 case Telephony.Mms.THREAD_ID: 1020 final long threadId = cursor.getLong(i); 1021 handleThreadId(jsonWriter, threadId); 1022 break; 1023 case Telephony.Mms._ID: 1024 case Telephony.Mms.SUBJECT_CHARSET: 1025 break; 1026 case Telephony.Mms.DATE: 1027 case Telephony.Mms.DATE_SENT: 1028 chunk.timestamp = findNewestValue(chunk.timestamp, value); 1029 jsonWriter.name(name).value(value); 1030 break; 1031 case Telephony.Mms.SUBJECT: 1032 subjectNull = false; 1033 default: 1034 jsonWriter.name(name).value(value); 1035 break; 1036 } 1037 } 1038 // Addresses. 1039 writeMmsAddresses(jsonWriter.name(MMS_ADDRESSES_KEY), mmsId); 1040 // Body (text of the message). 1041 jsonWriter.name(MMS_BODY_KEY).value(body.text); 1042 // Charset of the body text. 1043 jsonWriter.name(MMS_BODY_CHARSET_KEY).value(body.charSet); 1044 1045 if (!subjectNull) { 1046 // Subject charset. 1047 writeStringToWriter(jsonWriter, cursor, Telephony.Mms.SUBJECT_CHARSET); 1048 } 1049 jsonWriter.endObject(); 1050 chunk.count++; 1051 } 1052 1053 private Mms readMmsFromReader(JsonReader jsonReader) throws IOException { 1054 Mms mms = new Mms(); 1055 mms.values = new ContentValues(5+sDefaultValuesMms.size()); 1056 mms.values.putAll(sDefaultValuesMms); 1057 jsonReader.beginObject(); 1058 String bodyText = null; 1059 long threadId = -1; 1060 boolean isArchived = false; 1061 int bodyCharset = CharacterSets.DEFAULT_CHARSET; 1062 while (jsonReader.hasNext()) { 1063 String name = jsonReader.nextName(); 1064 if (DEBUG) { 1065 Log.d(TAG, "readMmsFromReader " + name); 1066 } 1067 switch (name) { 1068 case SELF_PHONE_KEY: 1069 final String selfPhone = jsonReader.nextString(); 1070 if (mPhone2subId.containsKey(selfPhone)) { 1071 mms.values.put(Telephony.Mms.SUBSCRIPTION_ID, mPhone2subId.get(selfPhone)); 1072 } 1073 break; 1074 case MMS_ADDRESSES_KEY: 1075 getMmsAddressesFromReader(jsonReader, mms); 1076 break; 1077 case MMS_ATTACHMENTS_KEY: 1078 getMmsAttachmentsFromReader(jsonReader, mms); 1079 break; 1080 case MMS_SMIL_KEY: 1081 mms.smil = jsonReader.nextString(); 1082 break; 1083 case MMS_BODY_KEY: 1084 bodyText = jsonReader.nextString(); 1085 break; 1086 case MMS_BODY_CHARSET_KEY: 1087 bodyCharset = jsonReader.nextInt(); 1088 break; 1089 case RECIPIENTS: 1090 threadId = getOrCreateThreadId(getRecipients(jsonReader)); 1091 mms.values.put(Telephony.Sms.THREAD_ID, threadId); 1092 break; 1093 case Telephony.Threads.ARCHIVED: 1094 isArchived = jsonReader.nextBoolean(); 1095 break; 1096 case Telephony.Mms.SUBJECT: 1097 case Telephony.Mms.SUBJECT_CHARSET: 1098 case Telephony.Mms.DATE: 1099 case Telephony.Mms.DATE_SENT: 1100 case Telephony.Mms.MESSAGE_TYPE: 1101 case Telephony.Mms.MMS_VERSION: 1102 case Telephony.Mms.MESSAGE_BOX: 1103 case Telephony.Mms.CONTENT_LOCATION: 1104 case Telephony.Mms.TRANSACTION_ID: 1105 case Telephony.Mms.READ: 1106 mms.values.put(name, jsonReader.nextString()); 1107 break; 1108 default: 1109 Log.d(TAG, "Unknown JSON element name:" + name); 1110 jsonReader.skipValue(); 1111 break; 1112 } 1113 } 1114 jsonReader.endObject(); 1115 1116 if (bodyText != null) { 1117 mms.body = new MmsBody(bodyText, bodyCharset); 1118 } 1119 // Set the text_only flag 1120 mms.values.put(Telephony.Mms.TEXT_ONLY, (mms.attachments == null 1121 || mms.attachments.size() == 0) && bodyText != null ? 1 : 0); 1122 1123 // Set default charset for subject. 1124 if (mms.values.get(Telephony.Mms.SUBJECT) != null && 1125 mms.values.get(Telephony.Mms.SUBJECT_CHARSET) == null) { 1126 mms.values.put(Telephony.Mms.SUBJECT_CHARSET, CharacterSets.DEFAULT_CHARSET); 1127 } 1128 1129 archiveThread(threadId, isArchived); 1130 1131 return mms; 1132 } 1133 1134 private static final String ARCHIVE_THREAD_SELECTION = Telephony.Threads._ID + "=?"; 1135 1136 private void archiveThread(long threadId, boolean isArchived) { 1137 if (threadId < 0 || !isArchived) { 1138 return; 1139 } 1140 final ContentValues values = new ContentValues(1); 1141 values.put(Telephony.Threads.ARCHIVED, 1); 1142 if (mContentResolver.update( 1143 Telephony.Threads.CONTENT_URI, 1144 values, 1145 ARCHIVE_THREAD_SELECTION, 1146 new String[] { Long.toString(threadId)}) != 1) { 1147 Log.e(TAG, "archiveThread: failed to update database"); 1148 } 1149 } 1150 1151 private MmsBody getMmsBody(int mmsId) { 1152 Uri MMS_PART_CONTENT_URI = Telephony.Mms.CONTENT_URI.buildUpon() 1153 .appendPath(String.valueOf(mmsId)).appendPath("part").build(); 1154 1155 String body = null; 1156 int charSet = 0; 1157 1158 try (Cursor cursor = mContentResolver.query(MMS_PART_CONTENT_URI, MMS_TEXT_PROJECTION, 1159 Telephony.Mms.Part.CONTENT_TYPE + "=?", new String[]{ContentType.TEXT_PLAIN}, 1160 ORDER_BY_ID)) { 1161 if (cursor != null && cursor.moveToFirst()) { 1162 do { 1163 String text = cursor.getString(MMS_TEXT_IDX); 1164 if (text != null) { 1165 body = (body == null ? text : body.concat(text)); 1166 charSet = cursor.getInt(MMS_TEXT_CHARSET_IDX); 1167 } 1168 } while (cursor.moveToNext()); 1169 } 1170 } 1171 return (body == null ? null : new MmsBody(body, charSet)); 1172 } 1173 1174 private void writeMmsAddresses(JsonWriter jsonWriter, int mmsId) throws IOException { 1175 Uri.Builder builder = Telephony.Mms.CONTENT_URI.buildUpon(); 1176 builder.appendPath(String.valueOf(mmsId)).appendPath("addr"); 1177 Uri uriAddrPart = builder.build(); 1178 1179 jsonWriter.beginArray(); 1180 try (Cursor cursor = mContentResolver.query(uriAddrPart, MMS_ADDR_PROJECTION, 1181 null/*selection*/, null/*selectionArgs*/, ORDER_BY_ID)) { 1182 if (cursor != null && cursor.moveToFirst()) { 1183 do { 1184 if (cursor.getString(cursor.getColumnIndex(Telephony.Mms.Addr.ADDRESS)) 1185 != null) { 1186 jsonWriter.beginObject(); 1187 writeIntToWriter(jsonWriter, cursor, Telephony.Mms.Addr.TYPE); 1188 writeStringToWriter(jsonWriter, cursor, Telephony.Mms.Addr.ADDRESS); 1189 writeIntToWriter(jsonWriter, cursor, Telephony.Mms.Addr.CHARSET); 1190 jsonWriter.endObject(); 1191 } 1192 } while (cursor.moveToNext()); 1193 } 1194 } 1195 jsonWriter.endArray(); 1196 } 1197 1198 private static void getMmsAddressesFromReader(JsonReader jsonReader, Mms mms) 1199 throws IOException { 1200 mms.addresses = new ArrayList<ContentValues>(); 1201 jsonReader.beginArray(); 1202 while (jsonReader.hasNext()) { 1203 jsonReader.beginObject(); 1204 ContentValues addrValues = new ContentValues(sDefaultValuesAddr); 1205 while (jsonReader.hasNext()) { 1206 final String name = jsonReader.nextName(); 1207 switch (name) { 1208 case Telephony.Mms.Addr.TYPE: 1209 case Telephony.Mms.Addr.CHARSET: 1210 addrValues.put(name, jsonReader.nextInt()); 1211 break; 1212 case Telephony.Mms.Addr.ADDRESS: 1213 addrValues.put(name, jsonReader.nextString()); 1214 break; 1215 default: 1216 Log.d(TAG, "Unknown JSON Element name:" + name); 1217 jsonReader.skipValue(); 1218 break; 1219 } 1220 } 1221 jsonReader.endObject(); 1222 if (addrValues.containsKey(Telephony.Mms.Addr.ADDRESS)) { 1223 mms.addresses.add(addrValues); 1224 } 1225 } 1226 jsonReader.endArray(); 1227 } 1228 1229 private static void getMmsAttachmentsFromReader(JsonReader jsonReader, Mms mms) 1230 throws IOException { 1231 if (DEBUG) { 1232 Log.d(TAG, "Add getMmsAttachmentsFromReader"); 1233 } 1234 mms.attachments = new ArrayList<ContentValues>(); 1235 jsonReader.beginArray(); 1236 while (jsonReader.hasNext()) { 1237 jsonReader.beginObject(); 1238 ContentValues attachmentValues = new ContentValues(sDefaultValuesAttachments); 1239 while (jsonReader.hasNext()) { 1240 final String name = jsonReader.nextName(); 1241 switch (name) { 1242 case MMS_MIME_TYPE: 1243 case MMS_ATTACHMENT_FILENAME: 1244 attachmentValues.put(name, jsonReader.nextString()); 1245 break; 1246 default: 1247 Log.d(TAG, "getMmsAttachmentsFromReader Unknown name:" + name); 1248 jsonReader.skipValue(); 1249 break; 1250 } 1251 } 1252 jsonReader.endObject(); 1253 if (attachmentValues.containsKey(MMS_ATTACHMENT_FILENAME)) { 1254 mms.attachments.add(attachmentValues); 1255 } else { 1256 Log.d(TAG, "Attachment json with no filenames"); 1257 } 1258 } 1259 jsonReader.endArray(); 1260 } 1261 1262 private void addMmsMessage(Mms mms) { 1263 if (DEBUG) { 1264 Log.d(TAG, "Add mms:\n" + mms); 1265 } 1266 final long placeholderId = System.currentTimeMillis(); // Placeholder ID of the msg. 1267 final Uri partUri = Telephony.Mms.CONTENT_URI.buildUpon() 1268 .appendPath(String.valueOf(placeholderId)).appendPath("part").build(); 1269 1270 final String srcName = String.format(Locale.US, "text.%06d.txt", 0); 1271 { // Insert SMIL part. 1272 final String smilBody = String.format(sSmilTextPart, srcName); 1273 final String smil = TextUtils.isEmpty(mms.smil) ? 1274 String.format(sSmilTextOnly, smilBody) : mms.smil; 1275 final ContentValues values = new ContentValues(7); 1276 values.put(Telephony.Mms.Part.MSG_ID, placeholderId); 1277 values.put(Telephony.Mms.Part.SEQ, -1); 1278 values.put(Telephony.Mms.Part.CONTENT_TYPE, ContentType.APP_SMIL); 1279 values.put(Telephony.Mms.Part.NAME, "smil.xml"); 1280 values.put(Telephony.Mms.Part.CONTENT_ID, "<smil>"); 1281 values.put(Telephony.Mms.Part.CONTENT_LOCATION, "smil.xml"); 1282 values.put(Telephony.Mms.Part.TEXT, smil); 1283 if (mContentResolver.insert(partUri, values) == null) { 1284 Log.e(TAG, "Could not insert SMIL part"); 1285 return; 1286 } 1287 } 1288 1289 { // Insert body part. 1290 final ContentValues values = new ContentValues(8); 1291 values.put(Telephony.Mms.Part.MSG_ID, placeholderId); 1292 values.put(Telephony.Mms.Part.SEQ, 0); 1293 values.put(Telephony.Mms.Part.CONTENT_TYPE, ContentType.TEXT_PLAIN); 1294 values.put(Telephony.Mms.Part.NAME, srcName); 1295 values.put(Telephony.Mms.Part.CONTENT_ID, "<"+srcName+">"); 1296 values.put(Telephony.Mms.Part.CONTENT_LOCATION, srcName); 1297 1298 values.put( 1299 Telephony.Mms.Part.CHARSET, 1300 mms.body == null ? CharacterSets.DEFAULT_CHARSET : mms.body.charSet); 1301 values.put(Telephony.Mms.Part.TEXT, mms.body == null ? "" : mms.body.text); 1302 1303 if (mContentResolver.insert(partUri, values) == null) { 1304 Log.e(TAG, "Could not insert body part"); 1305 return; 1306 } 1307 } 1308 1309 if (mms.attachments != null) { 1310 // Insert the attachment parts. 1311 for (ContentValues mmsAttachment : mms.attachments) { 1312 final ContentValues values = new ContentValues(6); 1313 values.put(Telephony.Mms.Part.MSG_ID, placeholderId); 1314 values.put(Telephony.Mms.Part.SEQ, 0); 1315 values.put(Telephony.Mms.Part.CONTENT_TYPE, 1316 mmsAttachment.getAsString(MMS_MIME_TYPE)); 1317 String filename = mmsAttachment.getAsString(MMS_ATTACHMENT_FILENAME); 1318 values.put(Telephony.Mms.Part.CONTENT_ID, "<"+filename+">"); 1319 values.put(Telephony.Mms.Part.CONTENT_LOCATION, filename); 1320 values.put(Telephony.Mms.Part._DATA, 1321 getDataDir() + ATTACHMENT_DATA_PATH + filename); 1322 Uri newPartUri = mContentResolver.insert(partUri, values); 1323 if (newPartUri == null) { 1324 Log.e(TAG, "Could not insert attachment part"); 1325 return; 1326 } 1327 } 1328 } 1329 1330 // Insert mms. 1331 final Uri mmsUri = mContentResolver.insert(Telephony.Mms.CONTENT_URI, mms.values); 1332 if (mmsUri == null) { 1333 Log.e(TAG, "Could not insert mms"); 1334 return; 1335 } 1336 1337 final long mmsId = ContentUris.parseId(mmsUri); 1338 { // Update parts with the right mms id. 1339 ContentValues values = new ContentValues(1); 1340 values.put(Telephony.Mms.Part.MSG_ID, mmsId); 1341 mContentResolver.update(partUri, values, null, null); 1342 } 1343 1344 { // Insert addresses into "addr". 1345 final Uri addrUri = Uri.withAppendedPath(mmsUri, "addr"); 1346 for (ContentValues mmsAddress : mms.addresses) { 1347 ContentValues values = new ContentValues(mmsAddress); 1348 values.put(Telephony.Mms.Addr.MSG_ID, mmsId); 1349 mContentResolver.insert(addrUri, values); 1350 } 1351 } 1352 } 1353 1354 private static final class MmsBody { 1355 public String text; 1356 public int charSet; 1357 1358 public MmsBody(String text, int charSet) { 1359 this.text = text; 1360 this.charSet = charSet; 1361 } 1362 1363 @Override 1364 public boolean equals(Object obj) { 1365 if (obj == null || !(obj instanceof MmsBody)) { 1366 return false; 1367 } 1368 MmsBody typedObj = (MmsBody) obj; 1369 return this.text.equals(typedObj.text) && this.charSet == typedObj.charSet; 1370 } 1371 1372 @Override 1373 public String toString() { 1374 return "Text:" + text + " charSet:" + charSet; 1375 } 1376 } 1377 1378 private static final class Mms { 1379 public ContentValues values; 1380 public List<ContentValues> addresses; 1381 public List<ContentValues> attachments; 1382 public String smil; 1383 public MmsBody body; 1384 @Override 1385 public String toString() { 1386 return "Values:" + values.toString() + "\nRecipients:" + addresses.toString() 1387 + "\nAttachments:" + (attachments == null ? "none" : attachments.toString()) 1388 + "\nBody:" + body; 1389 } 1390 } 1391 1392 private JsonWriter getJsonWriter(final String fileName) throws IOException { 1393 return new JsonWriter(new BufferedWriter(new OutputStreamWriter(new DeflaterOutputStream( 1394 openFileOutput(fileName, MODE_PRIVATE)), CHARSET_UTF8), WRITER_BUFFER_SIZE)); 1395 } 1396 1397 private static JsonReader getJsonReader(final FileDescriptor fileDescriptor) 1398 throws IOException { 1399 return new JsonReader(new InputStreamReader(new InflaterInputStream( 1400 new FileInputStream(fileDescriptor)), CHARSET_UTF8)); 1401 } 1402 1403 private static void writeStringToWriter(JsonWriter jsonWriter, Cursor cursor, String name) 1404 throws IOException { 1405 final String value = cursor.getString(cursor.getColumnIndex(name)); 1406 if (value != null) { 1407 jsonWriter.name(name).value(value); 1408 } 1409 } 1410 1411 private static void writeIntToWriter(JsonWriter jsonWriter, Cursor cursor, String name) 1412 throws IOException { 1413 final int value = cursor.getInt(cursor.getColumnIndex(name)); 1414 if (value != 0) { 1415 jsonWriter.name(name).value(value); 1416 } 1417 } 1418 1419 private long getOrCreateThreadId(Set<String> recipients) { 1420 if (recipients == null) { 1421 recipients = new ArraySet<String>(); 1422 } 1423 1424 if (recipients.isEmpty()) { 1425 recipients.add(UNKNOWN_SENDER); 1426 } 1427 1428 if (mCacheGetOrCreateThreadId == null) { 1429 mCacheGetOrCreateThreadId = new HashMap<>(); 1430 } 1431 1432 if (!mCacheGetOrCreateThreadId.containsKey(recipients)) { 1433 long threadId = mUnknownSenderThreadId; 1434 try { 1435 threadId = Telephony.Threads.getOrCreateThreadId(this, recipients); 1436 } catch (RuntimeException e) { 1437 Log.e(TAG, "Problem obtaining thread.", e); 1438 } 1439 mCacheGetOrCreateThreadId.put(recipients, threadId); 1440 return threadId; 1441 } 1442 1443 return mCacheGetOrCreateThreadId.get(recipients); 1444 } 1445 1446 @VisibleForTesting 1447 static final Uri THREAD_ID_CONTENT_URI = Uri.parse("content://mms-sms/threadID"); 1448 1449 // Mostly copied from packages/apps/Messaging/src/com/android/messaging/sms/MmsUtils.java. 1450 private List<String> getRecipientsByThread(final long threadId) { 1451 if (mCacheRecipientsByThread == null) { 1452 mCacheRecipientsByThread = new HashMap<>(); 1453 } 1454 1455 if (!mCacheRecipientsByThread.containsKey(threadId)) { 1456 final String spaceSepIds = getRawRecipientIdsForThread(threadId); 1457 if (!TextUtils.isEmpty(spaceSepIds)) { 1458 mCacheRecipientsByThread.put(threadId, getAddresses(spaceSepIds)); 1459 } else { 1460 mCacheRecipientsByThread.put(threadId, new ArrayList<String>()); 1461 } 1462 } 1463 1464 return mCacheRecipientsByThread.get(threadId); 1465 } 1466 1467 @VisibleForTesting 1468 static final Uri ALL_THREADS_URI = 1469 Telephony.Threads.CONTENT_URI.buildUpon(). 1470 appendQueryParameter("simple", "true").build(); 1471 private static final int RECIPIENT_IDS = 1; 1472 1473 // Copied from packages/apps/Messaging/src/com/android/messaging/sms/MmsUtils.java. 1474 // NOTE: There are phones on which you can't get the recipients from the thread id for SMS 1475 // until you have a message in the conversation! 1476 private String getRawRecipientIdsForThread(final long threadId) { 1477 if (threadId <= 0) { 1478 return null; 1479 } 1480 final Cursor thread = mContentResolver.query( 1481 ALL_THREADS_URI, 1482 SMS_RECIPIENTS_PROJECTION, "_id=?", new String[]{String.valueOf(threadId)}, null); 1483 if (thread != null) { 1484 try { 1485 if (thread.moveToFirst()) { 1486 // recipientIds will be a space-separated list of ids into the 1487 // canonical addresses table. 1488 return thread.getString(RECIPIENT_IDS); 1489 } 1490 } finally { 1491 thread.close(); 1492 } 1493 } 1494 return null; 1495 } 1496 1497 @VisibleForTesting 1498 static final Uri SINGLE_CANONICAL_ADDRESS_URI = 1499 Uri.parse("content://mms-sms/canonical-address"); 1500 1501 // Copied from packages/apps/Messaging/src/com/android/messaging/sms/MmsUtils.java. 1502 private List<String> getAddresses(final String spaceSepIds) { 1503 final List<String> numbers = new ArrayList<String>(); 1504 final String[] ids = spaceSepIds.split(" "); 1505 for (final String id : ids) { 1506 long longId; 1507 1508 try { 1509 longId = Long.parseLong(id); 1510 if (longId < 0) { 1511 Log.e(TAG, "getAddresses: invalid id " + longId); 1512 continue; 1513 } 1514 } catch (final NumberFormatException ex) { 1515 Log.e(TAG, "getAddresses: invalid id " + ex, ex); 1516 // skip this id 1517 continue; 1518 } 1519 1520 // TODO: build a single query where we get all the addresses at once. 1521 Cursor c = null; 1522 try { 1523 c = mContentResolver.query( 1524 ContentUris.withAppendedId(SINGLE_CANONICAL_ADDRESS_URI, longId), 1525 null, null, null, null); 1526 } catch (final Exception e) { 1527 Log.e(TAG, "getAddresses: query failed for id " + longId, e); 1528 } 1529 1530 if (c != null) { 1531 try { 1532 if (c.moveToFirst()) { 1533 final String number = c.getString(0); 1534 if (!TextUtils.isEmpty(number)) { 1535 numbers.add(number); 1536 } else { 1537 Log.d(TAG, "Canonical MMS/SMS address is empty for id: " + longId); 1538 } 1539 } 1540 } finally { 1541 c.close(); 1542 } 1543 } 1544 } 1545 if (numbers.isEmpty()) { 1546 Log.d(TAG, "No MMS addresses found from ids string [" + spaceSepIds + "]"); 1547 } 1548 return numbers; 1549 } 1550 1551 @Override 1552 public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data, 1553 ParcelFileDescriptor newState) throws IOException { 1554 // Empty because is not used during full backup. 1555 } 1556 1557 @Override 1558 public void onRestore(BackupDataInput data, int appVersionCode, 1559 ParcelFileDescriptor newState) throws IOException { 1560 // Empty because is not used during full restore. 1561 } 1562 1563 public static boolean getIsRestoring() { 1564 return sIsRestoring; 1565 } 1566 1567 private static class BackupChunkInformation { 1568 // Timestamp of the recent message in the file 1569 private long timestamp; 1570 1571 // The number of messages in the backup file 1572 private int count = 0; 1573 } 1574 } 1575