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