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