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
17package com.android.providers.telephony;
18
19import com.google.android.mms.ContentType;
20import com.google.android.mms.pdu.CharacterSets;
21
22import com.android.internal.annotations.VisibleForTesting;
23
24import android.annotation.TargetApi;
25import android.app.AlarmManager;
26import android.app.IntentService;
27import android.app.backup.BackupAgent;
28import android.app.backup.BackupDataInput;
29import android.app.backup.BackupDataOutput;
30import android.app.backup.FullBackupDataOutput;
31import android.content.ContentResolver;
32import android.content.ContentUris;
33import android.content.ContentValues;
34import android.content.Context;
35import android.content.Intent;
36import android.content.SharedPreferences;
37import android.database.Cursor;
38import android.database.DatabaseUtils;
39import android.net.Uri;
40import android.os.Build;
41import android.os.ParcelFileDescriptor;
42import android.os.PowerManager;
43import android.provider.BaseColumns;
44import android.provider.Telephony;
45import android.telephony.PhoneNumberUtils;
46import android.telephony.SubscriptionInfo;
47import android.telephony.SubscriptionManager;
48import android.text.TextUtils;
49import android.util.ArrayMap;
50import android.util.ArraySet;
51import android.util.JsonReader;
52import android.util.JsonWriter;
53import android.util.Log;
54import android.util.SparseArray;
55
56import java.io.BufferedWriter;
57import java.io.File;
58import java.io.FileDescriptor;
59import java.io.FileFilter;
60import java.io.FileInputStream;
61import java.io.IOException;
62import java.io.InputStreamReader;
63import java.io.OutputStreamWriter;
64import java.util.ArrayList;
65import java.util.Arrays;
66import java.util.Comparator;
67import java.util.HashMap;
68import java.util.List;
69import java.util.Locale;
70import java.util.Map;
71import java.util.Set;
72import java.util.concurrent.TimeUnit;
73import java.util.zip.DeflaterOutputStream;
74import java.util.zip.InflaterInputStream;
75
76/***
77 * Backup agent for backup and restore SMS's and text MMS's.
78 *
79 * This backup agent stores SMS's into "sms_backup" file as a JSON array. Example below.
80 *  [{"self_phone":"+1234567891011","address":"+1234567891012","body":"Example sms",
81 *  "date":"1450893518140","date_sent":"1450893514000","status":"-1","type":"1"},
82 *  {"self_phone":"+1234567891011","address":"12345","body":"Example 2","date":"1451328022316",
83 *  "date_sent":"1451328018000","status":"-1","type":"1"}]
84 *
85 * Text MMS's are stored into "mms_backup" file as a JSON array. Example below.
86 *  [{"self_phone":"+1234567891011","date":"1451322716","date_sent":"0","m_type":"128","v":"18",
87 *  "msg_box":"2","mms_addresses":[{"type":137,"address":"+1234567891011","charset":106},
88 *  {"type":151,"address":"example@example.com","charset":106}],"mms_body":"Mms to email",
89 *  "mms_charset":106},
90 *  {"self_phone":"+1234567891011","sub":"MMS subject","date":"1451322955","date_sent":"0",
91 *  "m_type":"132","v":"17","msg_box":"1","ct_l":"http://promms/servlets/NOK5BBqgUHAqugrQNM",
92 *  "mms_addresses":[{"type":151,"address":"+1234567891011","charset":106}],
93 *  "mms_body":"Mms\nBody\r\n",
94 *  "attachments":[{"mime_type":"image/jpeg","filename":"image000000.jpg"}],
95 *  "smil":"<smil><head><layout><root-layout/><region id='Image' fit='meet' top='0' left='0'
96 *   height='100%' width='100%'/></layout></head><body><par dur='5000ms'><img src='image000000.jpg'
97 *   region='Image' /></par></body></smil>",
98 *  "mms_charset":106,"sub_cs":"106"}]
99 *
100 *   It deflates the files on the flight.
101 *   Every 1000 messages it backs up file, deletes it and creates a new one with the same name.
102 *
103 *   It stores how many bytes we are over the quota and don't backup the oldest messages.
104 *
105 *   NOTE: presently, only MMS's with text are backed up. However, MMS's with attachments are
106 *   restored. In other words, this code can restore MMS attachments if the attachment data
107 *   is in the json, but it doesn't currently backup the attachment data in the json.
108 */
109
110@TargetApi(Build.VERSION_CODES.M)
111public 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