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