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