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