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