• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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