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