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