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