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