1 /*
2  * Copyright (C) 2016 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License
15  */
16 
17 package com.android.providers.telephony;
18 
19 import com.google.android.mms.ContentType;
20 import com.google.android.mms.pdu.CharacterSets;
21 
22 import com.android.internal.annotations.VisibleForTesting;
23 
24 import android.annotation.TargetApi;
25 import android.app.AlarmManager;
26 import android.app.IntentService;
27 import android.app.backup.BackupAgent;
28 import android.app.backup.BackupDataInput;
29 import android.app.backup.BackupDataOutput;
30 import android.app.backup.FullBackupDataOutput;
31 import android.content.ContentResolver;
32 import android.content.ContentUris;
33 import android.content.ContentValues;
34 import android.content.Context;
35 import android.content.Intent;
36 import android.content.SharedPreferences;
37 import android.database.Cursor;
38 import android.database.DatabaseUtils;
39 import android.net.Uri;
40 import android.os.Build;
41 import android.os.ParcelFileDescriptor;
42 import android.os.PowerManager;
43 import android.provider.BaseColumns;
44 import android.provider.Telephony;
45 import android.telephony.PhoneNumberUtils;
46 import android.telephony.SubscriptionInfo;
47 import android.telephony.SubscriptionManager;
48 import android.text.TextUtils;
49 import android.util.ArrayMap;
50 import android.util.ArraySet;
51 import android.util.JsonReader;
52 import android.util.JsonWriter;
53 import android.util.Log;
54 import android.util.SparseArray;
55 
56 import java.io.BufferedWriter;
57 import java.io.File;
58 import java.io.FileDescriptor;
59 import java.io.FileFilter;
60 import java.io.FileInputStream;
61 import java.io.IOException;
62 import java.io.InputStreamReader;
63 import java.io.OutputStreamWriter;
64 import java.util.ArrayList;
65 import java.util.Arrays;
66 import java.util.Comparator;
67 import java.util.HashMap;
68 import java.util.List;
69 import java.util.Locale;
70 import java.util.Map;
71 import java.util.Set;
72 import java.util.concurrent.TimeUnit;
73 import java.util.zip.DeflaterOutputStream;
74 import java.util.zip.InflaterInputStream;
75 
76 /***
77  * Backup agent for backup and restore SMS's and text MMS's.
78  *
79  * This backup agent stores SMS's into "sms_backup" file as a JSON array. Example below.
80  *  [{"self_phone":"+1234567891011","address":"+1234567891012","body":"Example sms",
81  *  "date":"1450893518140","date_sent":"1450893514000","status":"-1","type":"1"},
82  *  {"self_phone":"+1234567891011","address":"12345","body":"Example 2","date":"1451328022316",
83  *  "date_sent":"1451328018000","status":"-1","type":"1"}]
84  *
85  * Text MMS's are stored into "mms_backup" file as a JSON array. Example below.
86  *  [{"self_phone":"+1234567891011","date":"1451322716","date_sent":"0","m_type":"128","v":"18",
87  *  "msg_box":"2","mms_addresses":[{"type":137,"address":"+1234567891011","charset":106},
88  *  {"type":151,"address":"example@example.com","charset":106}],"mms_body":"Mms to email",
89  *  "mms_charset":106},
90  *  {"self_phone":"+1234567891011","sub":"MMS subject","date":"1451322955","date_sent":"0",
91  *  "m_type":"132","v":"17","msg_box":"1","ct_l":"http://promms/servlets/NOK5BBqgUHAqugrQNM",
92  *  "mms_addresses":[{"type":151,"address":"+1234567891011","charset":106}],
93  *  "mms_body":"Mms\nBody\r\n",
94  *  "mms_charset":106,"sub_cs":"106"}]
95  *
96  *   It deflates the files on the flight.
97  *   Every 1000 messages it backs up file, deletes it and creates a new one with the same name.
98  *
99  *   It stores how many bytes we are over the quota and don't backup the oldest messages.
100  */
101 
102 @TargetApi(Build.VERSION_CODES.M)
103 public class TelephonyBackupAgent extends BackupAgent {
104     private static final String TAG = "TelephonyBackupAgent";
105     private static final boolean DEBUG = false;
106 
107 
108     // Copied from packages/apps/Messaging/src/com/android/messaging/sms/MmsUtils.java.
109     private static final int DEFAULT_DURATION = 5000; //ms
110 
111     // Copied from packages/apps/Messaging/src/com/android/messaging/sms/MmsUtils.java.
112     @VisibleForTesting
113     static final String sSmilTextOnly =
114             "<smil>" +
115                 "<head>" +
116                     "<layout>" +
117                         "<root-layout/>" +
118                         "<region id=\"Text\" top=\"0\" left=\"0\" "
119                         + "height=\"100%%\" width=\"100%%\"/>" +
120                     "</layout>" +
121                 "</head>" +
122                 "<body>" +
123                        "%s" +  // constructed body goes here
124                 "</body>" +
125             "</smil>";
126 
127     // Copied from packages/apps/Messaging/src/com/android/messaging/sms/MmsUtils.java.
128     @VisibleForTesting
129     static final String sSmilTextPart =
130             "<par dur=\"" + DEFAULT_DURATION + "ms\">" +
131                 "<text src=\"%s\" region=\"Text\" />" +
132             "</par>";
133 
134 
135     // JSON key for phone number a message was sent from or received to.
136     private static final String SELF_PHONE_KEY = "self_phone";
137     // JSON key for list of addresses of MMS message.
138     private static final String MMS_ADDRESSES_KEY = "mms_addresses";
139     // JSON key for list of recipients of the message.
140     private static final String RECIPIENTS = "recipients";
141     // JSON key for MMS body.
142     private static final String MMS_BODY_KEY = "mms_body";
143     // JSON key for MMS charset.
144     private static final String MMS_BODY_CHARSET_KEY = "mms_charset";
145 
146     // File names suffixes for backup/restore.
147     private static final String SMS_BACKUP_FILE_SUFFIX = "_sms_backup";
148     private static final String MMS_BACKUP_FILE_SUFFIX = "_mms_backup";
149 
150     // File name formats for backup. It looks like 000000_sms_backup, 000001_sms_backup, etc.
151     private static final String SMS_BACKUP_FILE_FORMAT = "%06d"+SMS_BACKUP_FILE_SUFFIX;
152     private static final String MMS_BACKUP_FILE_FORMAT = "%06d"+MMS_BACKUP_FILE_SUFFIX;
153 
154     // Charset being used for reading/writing backup files.
155     private static final String CHARSET_UTF8 = "UTF-8";
156 
157     // Order by ID entries from database.
158     private static final String ORDER_BY_ID = BaseColumns._ID + " ASC";
159 
160     // Order by Date entries from database. We start backup from the oldest.
161     private static final String ORDER_BY_DATE = "date ASC";
162 
163     // This is a hard coded string rather than a localized one because we don't want it to
164     // change when you change locale.
165     @VisibleForTesting
166     static final String UNKNOWN_SENDER = "\u02BCUNKNOWN_SENDER!\u02BC";
167 
168     // Thread id for UNKNOWN_SENDER.
169     private long mUnknownSenderThreadId;
170 
171     // Columns from SMS database for backup/restore.
172     @VisibleForTesting
173     static final String[] SMS_PROJECTION = new String[] {
174             Telephony.Sms._ID,
175             Telephony.Sms.SUBSCRIPTION_ID,
176             Telephony.Sms.ADDRESS,
177             Telephony.Sms.BODY,
178             Telephony.Sms.SUBJECT,
179             Telephony.Sms.DATE,
180             Telephony.Sms.DATE_SENT,
181             Telephony.Sms.STATUS,
182             Telephony.Sms.TYPE,
183             Telephony.Sms.THREAD_ID
184     };
185 
186     // Columns to fetch recepients of SMS.
187     private static final String[] SMS_RECIPIENTS_PROJECTION = {
188             Telephony.Threads._ID,
189             Telephony.Threads.RECIPIENT_IDS
190     };
191 
192     // Columns from MMS database for backup/restore.
193     @VisibleForTesting
194     static final String[] MMS_PROJECTION = new String[] {
195             Telephony.Mms._ID,
196             Telephony.Mms.SUBSCRIPTION_ID,
197             Telephony.Mms.SUBJECT,
198             Telephony.Mms.SUBJECT_CHARSET,
199             Telephony.Mms.DATE,
200             Telephony.Mms.DATE_SENT,
201             Telephony.Mms.MESSAGE_TYPE,
202             Telephony.Mms.MMS_VERSION,
203             Telephony.Mms.MESSAGE_BOX,
204             Telephony.Mms.CONTENT_LOCATION,
205             Telephony.Mms.THREAD_ID,
206             Telephony.Mms.TRANSACTION_ID
207     };
208 
209     // Columns from addr database for backup/restore. This database is used for fetching addresses
210     // for MMS message.
211     @VisibleForTesting
212     static final String[] MMS_ADDR_PROJECTION = new String[] {
213             Telephony.Mms.Addr.TYPE,
214             Telephony.Mms.Addr.ADDRESS,
215             Telephony.Mms.Addr.CHARSET
216     };
217 
218     // Columns from part database for backup/restore. This database is used for fetching body text
219     // and charset for MMS message.
220     @VisibleForTesting
221     static final String[] MMS_TEXT_PROJECTION = new String[] {
222             Telephony.Mms.Part.TEXT,
223             Telephony.Mms.Part.CHARSET
224     };
225     static final int MMS_TEXT_IDX = 0;
226     static final int MMS_TEXT_CHARSET_IDX = 1;
227 
228     // Buffer size for Json writer.
229     public static final int WRITER_BUFFER_SIZE = 32*1024; //32Kb
230 
231     // We increase how many bytes backup size over quota by 10%, so we will fit into quota on next
232     // backup
233     public static final double BYTES_OVER_QUOTA_MULTIPLIER = 1.1;
234 
235     // Maximum messages for one backup file. After reaching the limit the agent backs up the file,
236     // deletes it and creates a new one with the same name.
237     // Not final for the testing.
238     @VisibleForTesting
239     int mMaxMsgPerFile = 1000;
240 
241     // Default values for SMS, MMS, Addresses restore.
242     private static ContentValues sDefaultValuesSms = new ContentValues(5);
243     private static ContentValues sDefaultValuesMms = new ContentValues(6);
244     private static final ContentValues sDefaultValuesAddr = new ContentValues(2);
245 
246     // Shared preferences for the backup agent.
247     private static final String BACKUP_PREFS = "backup_shared_prefs";
248     // Key for storing quota bytes.
249     private static final String QUOTA_BYTES = "backup_quota_bytes";
250     // Key for storing backup data size.
251     private static final String BACKUP_DATA_BYTES = "backup_data_bytes";
252     // Key for storing timestamp when backup agent resets quota. It does that to get onQuotaExceeded
253     // call so it could get the new quota if it changed.
254     private static final String QUOTA_RESET_TIME = "reset_quota_time";
255     private static final long QUOTA_RESET_INTERVAL = 30 * AlarmManager.INTERVAL_DAY; // 30 days.
256 
257 
258     static {
259         // Consider restored messages read and seen.
sDefaultValuesSms.put(Telephony.Sms.READ, 1)260         sDefaultValuesSms.put(Telephony.Sms.READ, 1);
sDefaultValuesSms.put(Telephony.Sms.SEEN, 1)261         sDefaultValuesSms.put(Telephony.Sms.SEEN, 1);
sDefaultValuesSms.put(Telephony.Sms.ADDRESS, UNKNOWN_SENDER)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.
sDefaultValuesSms.put(Telephony.Sms.SUBSCRIPTION_ID, -1)264         sDefaultValuesSms.put(Telephony.Sms.SUBSCRIPTION_ID, -1);
265 
sDefaultValuesMms.put(Telephony.Mms.READ, 1)266         sDefaultValuesMms.put(Telephony.Mms.READ, 1);
sDefaultValuesMms.put(Telephony.Mms.SEEN, 1)267         sDefaultValuesMms.put(Telephony.Mms.SEEN, 1);
sDefaultValuesMms.put(Telephony.Mms.SUBSCRIPTION_ID, -1)268         sDefaultValuesMms.put(Telephony.Mms.SUBSCRIPTION_ID, -1);
sDefaultValuesMms.put(Telephony.Mms.MESSAGE_BOX, Telephony.Mms.MESSAGE_BOX_ALL)269         sDefaultValuesMms.put(Telephony.Mms.MESSAGE_BOX, Telephony.Mms.MESSAGE_BOX_ALL);
sDefaultValuesMms.put(Telephony.Mms.TEXT_ONLY, 1)270         sDefaultValuesMms.put(Telephony.Mms.TEXT_ONLY, 1);
271 
sDefaultValuesAddr.put(Telephony.Mms.Addr.TYPE, 0)272         sDefaultValuesAddr.put(Telephony.Mms.Addr.TYPE, 0);
sDefaultValuesAddr.put(Telephony.Mms.Addr.CHARSET, CharacterSets.DEFAULT_CHARSET)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
onCreate()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
setContentResolver(ContentResolver contentResolver)313     void setContentResolver(ContentResolver contentResolver) {
314         mContentResolver = contentResolver;
315     }
316     @VisibleForTesting
setSubId(SparseArray<String> subId2Phone, Map<String, Integer> phone2subId)317     void setSubId(SparseArray<String> subId2Phone, Map<String, Integer> phone2subId) {
318         mSubId2phone = subId2Phone;
319         mPhone2subId = phone2subId;
320     }
321 
322     @VisibleForTesting
initUnknownSender()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
onFullBackup(FullBackupDataOutput data)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
clearSharedPreferences()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 
getMessageDate(Cursor cursor)398     private static long getMessageDate(Cursor cursor) {
399         return cursor.getLong(cursor.getColumnIndex(Telephony.Sms.DATE));
400     }
401 
402     @Override
onQuotaExceeded(long backupDataBytes, long quotaBytes)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 
backupAll(FullBackupDataOutput data, Cursor cursor, String fileName)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
putMmsMessagesToJson(Cursor cursor, JsonWriter jsonWriter)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
putSmsMessagesToJson(Cursor cursor, JsonWriter jsonWriter)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 
backupFile(int messagesWritten, String fileName, FullBackupDataOutput data)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 
DeferredSmsMmsRestoreService()487         public DeferredSmsMmsRestoreService() {
488             super(TAG);
489             setIntentRedelivery(true);
490         }
491 
492         private TelephonyBackupAgent mTelephonyBackupAgent;
493         private PowerManager.WakeLock mWakeLock;
494 
495         @Override
onHandleIntent(Intent intent)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
onCreate()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
onDestroy()534         public void onDestroy() {
535             if (mTelephonyBackupAgent != null) {
536                 mTelephonyBackupAgent.onDestroy();
537                 mTelephonyBackupAgent = null;
538             }
539             super.onDestroy();
540         }
541 
startIfFilesExist(Context context)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 
getFilesToRestore(Context context)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