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