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