1 /*
2  * Copyright (C) 2007-2008 Esmertec AG.
3  * Copyright (C) 2007-2008 The Android Open Source Project
4  *
5  * Licensed under the Apache License, Version 2.0 (the "License");
6  * you may not use this file except in compliance with the License.
7  * You may obtain a copy of the License at
8  *
9  *      http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  */
17 
18 package com.android.messaging.mmslib.pdu;
19 
20 import android.content.ContentResolver;
21 import android.content.ContentUris;
22 import android.content.ContentValues;
23 import android.content.Context;
24 import android.database.Cursor;
25 import android.database.DatabaseUtils;
26 import android.database.sqlite.SQLiteException;
27 import android.net.Uri;
28 import android.provider.MediaStore;
29 import android.provider.Telephony.Mms;
30 import android.provider.Telephony.Mms.Addr;
31 import android.provider.Telephony.Mms.Part;
32 import android.provider.Telephony.MmsSms;
33 import android.provider.Telephony.MmsSms.PendingMessages;
34 import androidx.collection.ArrayMap;
35 import androidx.collection.SimpleArrayMap;
36 import android.telephony.PhoneNumberUtils;
37 import android.text.TextUtils;
38 import android.util.Log;
39 import android.util.SparseArray;
40 import android.util.SparseIntArray;
41 
42 import com.android.messaging.datamodel.data.ParticipantData;
43 import com.android.messaging.mmslib.InvalidHeaderValueException;
44 import com.android.messaging.mmslib.MmsException;
45 import com.android.messaging.mmslib.SqliteWrapper;
46 import com.android.messaging.mmslib.util.DownloadDrmHelper;
47 import com.android.messaging.mmslib.util.DrmConvertSession;
48 import com.android.messaging.mmslib.util.PduCache;
49 import com.android.messaging.mmslib.util.PduCacheEntry;
50 import com.android.messaging.sms.MmsSmsUtils;
51 import com.android.messaging.util.Assert;
52 import com.android.messaging.util.ContentType;
53 import com.android.messaging.util.LogUtil;
54 import com.android.messaging.util.OsUtil;
55 import com.android.messaging.util.UriUtil;
56 
57 import java.io.ByteArrayOutputStream;
58 import java.io.File;
59 import java.io.FileNotFoundException;
60 import java.io.IOException;
61 import java.io.InputStream;
62 import java.io.OutputStream;
63 import java.io.UnsupportedEncodingException;
64 import java.util.ArrayList;
65 import java.util.HashSet;
66 import java.util.Map;
67 
68 /**
69  * This class is the high-level manager of PDU storage.
70  */
71 public class PduPersister {
72     private static final String TAG = "PduPersister";
73     private static final boolean LOCAL_LOGV = false;
74 
75     /**
76      * The uri of temporary drm objects.
77      */
78     public static final String TEMPORARY_DRM_OBJECT_URI =
79             "content://mms/" + Long.MAX_VALUE + "/part";
80 
81     /**
82      * Indicate that we transiently failed to process a MM.
83      */
84     public static final int PROC_STATUS_TRANSIENT_FAILURE = 1;
85 
86     /**
87      * Indicate that we permanently failed to process a MM.
88      */
89     public static final int PROC_STATUS_PERMANENTLY_FAILURE = 2;
90 
91     /**
92      * Indicate that we have successfully processed a MM.
93      */
94     public static final int PROC_STATUS_COMPLETED = 3;
95 
96     public static final String BEGIN_VCARD = "BEGIN:VCARD";
97 
98     private static PduPersister sPersister;
99 
100     private static final PduCache PDU_CACHE_INSTANCE;
101 
102     private static final int[] ADDRESS_FIELDS = new int[]{
103             PduHeaders.BCC,
104             PduHeaders.CC,
105             PduHeaders.FROM,
106             PduHeaders.TO
107     };
108 
109     public static final String[] PDU_PROJECTION = new String[]{
110             Mms._ID,
111             Mms.MESSAGE_BOX,
112             Mms.THREAD_ID,
113             Mms.RETRIEVE_TEXT,
114             Mms.SUBJECT,
115             Mms.CONTENT_LOCATION,
116             Mms.CONTENT_TYPE,
117             Mms.MESSAGE_CLASS,
118             Mms.MESSAGE_ID,
119             Mms.RESPONSE_TEXT,
120             Mms.TRANSACTION_ID,
121             Mms.CONTENT_CLASS,
122             Mms.DELIVERY_REPORT,
123             Mms.MESSAGE_TYPE,
124             Mms.MMS_VERSION,
125             Mms.PRIORITY,
126             Mms.READ_REPORT,
127             Mms.READ_STATUS,
128             Mms.REPORT_ALLOWED,
129             Mms.RETRIEVE_STATUS,
130             Mms.STATUS,
131             Mms.DATE,
132             Mms.DELIVERY_TIME,
133             Mms.EXPIRY,
134             Mms.MESSAGE_SIZE,
135             Mms.SUBJECT_CHARSET,
136             Mms.RETRIEVE_TEXT_CHARSET,
137             Mms.READ,
138             Mms.SEEN,
139     };
140 
141     public static final int PDU_COLUMN_ID                    = 0;
142     public static final int PDU_COLUMN_MESSAGE_BOX           = 1;
143     public static final int PDU_COLUMN_THREAD_ID             = 2;
144     public static final int PDU_COLUMN_RETRIEVE_TEXT         = 3;
145     public static final int PDU_COLUMN_SUBJECT               = 4;
146     public static final int PDU_COLUMN_CONTENT_LOCATION      = 5;
147     public static final int PDU_COLUMN_CONTENT_TYPE          = 6;
148     public static final int PDU_COLUMN_MESSAGE_CLASS         = 7;
149     public static final int PDU_COLUMN_MESSAGE_ID            = 8;
150     public static final int PDU_COLUMN_RESPONSE_TEXT         = 9;
151     public static final int PDU_COLUMN_TRANSACTION_ID        = 10;
152     public static final int PDU_COLUMN_CONTENT_CLASS         = 11;
153     public static final int PDU_COLUMN_DELIVERY_REPORT       = 12;
154     public static final int PDU_COLUMN_MESSAGE_TYPE          = 13;
155     public static final int PDU_COLUMN_MMS_VERSION           = 14;
156     public static final int PDU_COLUMN_PRIORITY              = 15;
157     public static final int PDU_COLUMN_READ_REPORT           = 16;
158     public static final int PDU_COLUMN_READ_STATUS           = 17;
159     public static final int PDU_COLUMN_REPORT_ALLOWED        = 18;
160     public static final int PDU_COLUMN_RETRIEVE_STATUS       = 19;
161     public static final int PDU_COLUMN_STATUS                = 20;
162     public static final int PDU_COLUMN_DATE                  = 21;
163     public static final int PDU_COLUMN_DELIVERY_TIME         = 22;
164     public static final int PDU_COLUMN_EXPIRY                = 23;
165     public static final int PDU_COLUMN_MESSAGE_SIZE          = 24;
166     public static final int PDU_COLUMN_SUBJECT_CHARSET       = 25;
167     public static final int PDU_COLUMN_RETRIEVE_TEXT_CHARSET = 26;
168     public static final int PDU_COLUMN_READ                  = 27;
169     public static final int PDU_COLUMN_SEEN                  = 28;
170 
171     private static final String[] PART_PROJECTION = new String[] {
172             Part._ID,
173             Part.CHARSET,
174             Part.CONTENT_DISPOSITION,
175             Part.CONTENT_ID,
176             Part.CONTENT_LOCATION,
177             Part.CONTENT_TYPE,
178             Part.FILENAME,
179             Part.NAME,
180             Part.TEXT
181     };
182 
183     private static final int PART_COLUMN_ID                  = 0;
184     private static final int PART_COLUMN_CHARSET             = 1;
185     private static final int PART_COLUMN_CONTENT_DISPOSITION = 2;
186     private static final int PART_COLUMN_CONTENT_ID          = 3;
187     private static final int PART_COLUMN_CONTENT_LOCATION    = 4;
188     private static final int PART_COLUMN_CONTENT_TYPE        = 5;
189     private static final int PART_COLUMN_FILENAME            = 6;
190     private static final int PART_COLUMN_NAME                = 7;
191     private static final int PART_COLUMN_TEXT                = 8;
192 
193     private static final SimpleArrayMap<Uri, Integer> MESSAGE_BOX_MAP;
194 
195     // These map are used for convenience in persist() and load().
196     private static final SparseIntArray CHARSET_COLUMN_INDEX_MAP;
197 
198     private static final SparseIntArray ENCODED_STRING_COLUMN_INDEX_MAP;
199 
200     private static final SparseIntArray TEXT_STRING_COLUMN_INDEX_MAP;
201 
202     private static final SparseIntArray OCTET_COLUMN_INDEX_MAP;
203 
204     private static final SparseIntArray LONG_COLUMN_INDEX_MAP;
205 
206     private static final SparseArray<String> CHARSET_COLUMN_NAME_MAP;
207 
208     private static final SparseArray<String> ENCODED_STRING_COLUMN_NAME_MAP;
209 
210     private static final SparseArray<String> TEXT_STRING_COLUMN_NAME_MAP;
211 
212     private static final SparseArray<String> OCTET_COLUMN_NAME_MAP;
213 
214     private static final SparseArray<String> LONG_COLUMN_NAME_MAP;
215 
216     static {
217         MESSAGE_BOX_MAP = new SimpleArrayMap<Uri, Integer>();
MESSAGE_BOX_MAP.put(Mms.Inbox.CONTENT_URI, Mms.MESSAGE_BOX_INBOX)218         MESSAGE_BOX_MAP.put(Mms.Inbox.CONTENT_URI, Mms.MESSAGE_BOX_INBOX);
MESSAGE_BOX_MAP.put(Mms.Sent.CONTENT_URI, Mms.MESSAGE_BOX_SENT)219         MESSAGE_BOX_MAP.put(Mms.Sent.CONTENT_URI, Mms.MESSAGE_BOX_SENT);
MESSAGE_BOX_MAP.put(Mms.Draft.CONTENT_URI, Mms.MESSAGE_BOX_DRAFTS)220         MESSAGE_BOX_MAP.put(Mms.Draft.CONTENT_URI, Mms.MESSAGE_BOX_DRAFTS);
MESSAGE_BOX_MAP.put(Mms.Outbox.CONTENT_URI, Mms.MESSAGE_BOX_OUTBOX)221         MESSAGE_BOX_MAP.put(Mms.Outbox.CONTENT_URI, Mms.MESSAGE_BOX_OUTBOX);
222 
223         CHARSET_COLUMN_INDEX_MAP = new SparseIntArray();
CHARSET_COLUMN_INDEX_MAP.put(PduHeaders.SUBJECT, PDU_COLUMN_SUBJECT_CHARSET)224         CHARSET_COLUMN_INDEX_MAP.put(PduHeaders.SUBJECT, PDU_COLUMN_SUBJECT_CHARSET);
CHARSET_COLUMN_INDEX_MAP.put(PduHeaders.RETRIEVE_TEXT, PDU_COLUMN_RETRIEVE_TEXT_CHARSET)225         CHARSET_COLUMN_INDEX_MAP.put(PduHeaders.RETRIEVE_TEXT, PDU_COLUMN_RETRIEVE_TEXT_CHARSET);
226 
227         CHARSET_COLUMN_NAME_MAP = new SparseArray<String>();
CHARSET_COLUMN_NAME_MAP.put(PduHeaders.SUBJECT, Mms.SUBJECT_CHARSET)228         CHARSET_COLUMN_NAME_MAP.put(PduHeaders.SUBJECT, Mms.SUBJECT_CHARSET);
CHARSET_COLUMN_NAME_MAP.put(PduHeaders.RETRIEVE_TEXT, Mms.RETRIEVE_TEXT_CHARSET)229         CHARSET_COLUMN_NAME_MAP.put(PduHeaders.RETRIEVE_TEXT, Mms.RETRIEVE_TEXT_CHARSET);
230 
231         // Encoded string field code -> column index/name map.
232         ENCODED_STRING_COLUMN_INDEX_MAP = new SparseIntArray();
ENCODED_STRING_COLUMN_INDEX_MAP.put(PduHeaders.RETRIEVE_TEXT, PDU_COLUMN_RETRIEVE_TEXT)233         ENCODED_STRING_COLUMN_INDEX_MAP.put(PduHeaders.RETRIEVE_TEXT, PDU_COLUMN_RETRIEVE_TEXT);
ENCODED_STRING_COLUMN_INDEX_MAP.put(PduHeaders.SUBJECT, PDU_COLUMN_SUBJECT)234         ENCODED_STRING_COLUMN_INDEX_MAP.put(PduHeaders.SUBJECT, PDU_COLUMN_SUBJECT);
235 
236         ENCODED_STRING_COLUMN_NAME_MAP = new SparseArray<String>();
ENCODED_STRING_COLUMN_NAME_MAP.put(PduHeaders.RETRIEVE_TEXT, Mms.RETRIEVE_TEXT)237         ENCODED_STRING_COLUMN_NAME_MAP.put(PduHeaders.RETRIEVE_TEXT, Mms.RETRIEVE_TEXT);
ENCODED_STRING_COLUMN_NAME_MAP.put(PduHeaders.SUBJECT, Mms.SUBJECT)238         ENCODED_STRING_COLUMN_NAME_MAP.put(PduHeaders.SUBJECT, Mms.SUBJECT);
239 
240         // Text string field code -> column index/name map.
241         TEXT_STRING_COLUMN_INDEX_MAP = new SparseIntArray();
TEXT_STRING_COLUMN_INDEX_MAP.put(PduHeaders.CONTENT_LOCATION, PDU_COLUMN_CONTENT_LOCATION)242         TEXT_STRING_COLUMN_INDEX_MAP.put(PduHeaders.CONTENT_LOCATION, PDU_COLUMN_CONTENT_LOCATION);
TEXT_STRING_COLUMN_INDEX_MAP.put(PduHeaders.CONTENT_TYPE, PDU_COLUMN_CONTENT_TYPE)243         TEXT_STRING_COLUMN_INDEX_MAP.put(PduHeaders.CONTENT_TYPE, PDU_COLUMN_CONTENT_TYPE);
TEXT_STRING_COLUMN_INDEX_MAP.put(PduHeaders.MESSAGE_CLASS, PDU_COLUMN_MESSAGE_CLASS)244         TEXT_STRING_COLUMN_INDEX_MAP.put(PduHeaders.MESSAGE_CLASS, PDU_COLUMN_MESSAGE_CLASS);
TEXT_STRING_COLUMN_INDEX_MAP.put(PduHeaders.MESSAGE_ID, PDU_COLUMN_MESSAGE_ID)245         TEXT_STRING_COLUMN_INDEX_MAP.put(PduHeaders.MESSAGE_ID, PDU_COLUMN_MESSAGE_ID);
TEXT_STRING_COLUMN_INDEX_MAP.put(PduHeaders.RESPONSE_TEXT, PDU_COLUMN_RESPONSE_TEXT)246         TEXT_STRING_COLUMN_INDEX_MAP.put(PduHeaders.RESPONSE_TEXT, PDU_COLUMN_RESPONSE_TEXT);
TEXT_STRING_COLUMN_INDEX_MAP.put(PduHeaders.TRANSACTION_ID, PDU_COLUMN_TRANSACTION_ID)247         TEXT_STRING_COLUMN_INDEX_MAP.put(PduHeaders.TRANSACTION_ID, PDU_COLUMN_TRANSACTION_ID);
248 
249         TEXT_STRING_COLUMN_NAME_MAP = new SparseArray<String>();
TEXT_STRING_COLUMN_NAME_MAP.put(PduHeaders.CONTENT_LOCATION, Mms.CONTENT_LOCATION)250         TEXT_STRING_COLUMN_NAME_MAP.put(PduHeaders.CONTENT_LOCATION, Mms.CONTENT_LOCATION);
TEXT_STRING_COLUMN_NAME_MAP.put(PduHeaders.CONTENT_TYPE, Mms.CONTENT_TYPE)251         TEXT_STRING_COLUMN_NAME_MAP.put(PduHeaders.CONTENT_TYPE, Mms.CONTENT_TYPE);
TEXT_STRING_COLUMN_NAME_MAP.put(PduHeaders.MESSAGE_CLASS, Mms.MESSAGE_CLASS)252         TEXT_STRING_COLUMN_NAME_MAP.put(PduHeaders.MESSAGE_CLASS, Mms.MESSAGE_CLASS);
TEXT_STRING_COLUMN_NAME_MAP.put(PduHeaders.MESSAGE_ID, Mms.MESSAGE_ID)253         TEXT_STRING_COLUMN_NAME_MAP.put(PduHeaders.MESSAGE_ID, Mms.MESSAGE_ID);
TEXT_STRING_COLUMN_NAME_MAP.put(PduHeaders.RESPONSE_TEXT, Mms.RESPONSE_TEXT)254         TEXT_STRING_COLUMN_NAME_MAP.put(PduHeaders.RESPONSE_TEXT, Mms.RESPONSE_TEXT);
TEXT_STRING_COLUMN_NAME_MAP.put(PduHeaders.TRANSACTION_ID, Mms.TRANSACTION_ID)255         TEXT_STRING_COLUMN_NAME_MAP.put(PduHeaders.TRANSACTION_ID, Mms.TRANSACTION_ID);
256 
257         // Octet field code -> column index/name map.
258         OCTET_COLUMN_INDEX_MAP = new SparseIntArray();
OCTET_COLUMN_INDEX_MAP.put(PduHeaders.CONTENT_CLASS, PDU_COLUMN_CONTENT_CLASS)259         OCTET_COLUMN_INDEX_MAP.put(PduHeaders.CONTENT_CLASS, PDU_COLUMN_CONTENT_CLASS);
OCTET_COLUMN_INDEX_MAP.put(PduHeaders.DELIVERY_REPORT, PDU_COLUMN_DELIVERY_REPORT)260         OCTET_COLUMN_INDEX_MAP.put(PduHeaders.DELIVERY_REPORT, PDU_COLUMN_DELIVERY_REPORT);
OCTET_COLUMN_INDEX_MAP.put(PduHeaders.MESSAGE_TYPE, PDU_COLUMN_MESSAGE_TYPE)261         OCTET_COLUMN_INDEX_MAP.put(PduHeaders.MESSAGE_TYPE, PDU_COLUMN_MESSAGE_TYPE);
OCTET_COLUMN_INDEX_MAP.put(PduHeaders.MMS_VERSION, PDU_COLUMN_MMS_VERSION)262         OCTET_COLUMN_INDEX_MAP.put(PduHeaders.MMS_VERSION, PDU_COLUMN_MMS_VERSION);
OCTET_COLUMN_INDEX_MAP.put(PduHeaders.PRIORITY, PDU_COLUMN_PRIORITY)263         OCTET_COLUMN_INDEX_MAP.put(PduHeaders.PRIORITY, PDU_COLUMN_PRIORITY);
OCTET_COLUMN_INDEX_MAP.put(PduHeaders.READ_REPORT, PDU_COLUMN_READ_REPORT)264         OCTET_COLUMN_INDEX_MAP.put(PduHeaders.READ_REPORT, PDU_COLUMN_READ_REPORT);
OCTET_COLUMN_INDEX_MAP.put(PduHeaders.READ_STATUS, PDU_COLUMN_READ_STATUS)265         OCTET_COLUMN_INDEX_MAP.put(PduHeaders.READ_STATUS, PDU_COLUMN_READ_STATUS);
OCTET_COLUMN_INDEX_MAP.put(PduHeaders.REPORT_ALLOWED, PDU_COLUMN_REPORT_ALLOWED)266         OCTET_COLUMN_INDEX_MAP.put(PduHeaders.REPORT_ALLOWED, PDU_COLUMN_REPORT_ALLOWED);
OCTET_COLUMN_INDEX_MAP.put(PduHeaders.RETRIEVE_STATUS, PDU_COLUMN_RETRIEVE_STATUS)267         OCTET_COLUMN_INDEX_MAP.put(PduHeaders.RETRIEVE_STATUS, PDU_COLUMN_RETRIEVE_STATUS);
OCTET_COLUMN_INDEX_MAP.put(PduHeaders.STATUS, PDU_COLUMN_STATUS)268         OCTET_COLUMN_INDEX_MAP.put(PduHeaders.STATUS, PDU_COLUMN_STATUS);
269 
270         OCTET_COLUMN_NAME_MAP = new SparseArray<String>();
OCTET_COLUMN_NAME_MAP.put(PduHeaders.CONTENT_CLASS, Mms.CONTENT_CLASS)271         OCTET_COLUMN_NAME_MAP.put(PduHeaders.CONTENT_CLASS, Mms.CONTENT_CLASS);
OCTET_COLUMN_NAME_MAP.put(PduHeaders.DELIVERY_REPORT, Mms.DELIVERY_REPORT)272         OCTET_COLUMN_NAME_MAP.put(PduHeaders.DELIVERY_REPORT, Mms.DELIVERY_REPORT);
OCTET_COLUMN_NAME_MAP.put(PduHeaders.MESSAGE_TYPE, Mms.MESSAGE_TYPE)273         OCTET_COLUMN_NAME_MAP.put(PduHeaders.MESSAGE_TYPE, Mms.MESSAGE_TYPE);
OCTET_COLUMN_NAME_MAP.put(PduHeaders.MMS_VERSION, Mms.MMS_VERSION)274         OCTET_COLUMN_NAME_MAP.put(PduHeaders.MMS_VERSION, Mms.MMS_VERSION);
OCTET_COLUMN_NAME_MAP.put(PduHeaders.PRIORITY, Mms.PRIORITY)275         OCTET_COLUMN_NAME_MAP.put(PduHeaders.PRIORITY, Mms.PRIORITY);
OCTET_COLUMN_NAME_MAP.put(PduHeaders.READ_REPORT, Mms.READ_REPORT)276         OCTET_COLUMN_NAME_MAP.put(PduHeaders.READ_REPORT, Mms.READ_REPORT);
OCTET_COLUMN_NAME_MAP.put(PduHeaders.READ_STATUS, Mms.READ_STATUS)277         OCTET_COLUMN_NAME_MAP.put(PduHeaders.READ_STATUS, Mms.READ_STATUS);
OCTET_COLUMN_NAME_MAP.put(PduHeaders.REPORT_ALLOWED, Mms.REPORT_ALLOWED)278         OCTET_COLUMN_NAME_MAP.put(PduHeaders.REPORT_ALLOWED, Mms.REPORT_ALLOWED);
OCTET_COLUMN_NAME_MAP.put(PduHeaders.RETRIEVE_STATUS, Mms.RETRIEVE_STATUS)279         OCTET_COLUMN_NAME_MAP.put(PduHeaders.RETRIEVE_STATUS, Mms.RETRIEVE_STATUS);
OCTET_COLUMN_NAME_MAP.put(PduHeaders.STATUS, Mms.STATUS)280         OCTET_COLUMN_NAME_MAP.put(PduHeaders.STATUS, Mms.STATUS);
281 
282         // Long field code -> column index/name map.
283         LONG_COLUMN_INDEX_MAP = new SparseIntArray();
LONG_COLUMN_INDEX_MAP.put(PduHeaders.DATE, PDU_COLUMN_DATE)284         LONG_COLUMN_INDEX_MAP.put(PduHeaders.DATE, PDU_COLUMN_DATE);
LONG_COLUMN_INDEX_MAP.put(PduHeaders.DELIVERY_TIME, PDU_COLUMN_DELIVERY_TIME)285         LONG_COLUMN_INDEX_MAP.put(PduHeaders.DELIVERY_TIME, PDU_COLUMN_DELIVERY_TIME);
LONG_COLUMN_INDEX_MAP.put(PduHeaders.EXPIRY, PDU_COLUMN_EXPIRY)286         LONG_COLUMN_INDEX_MAP.put(PduHeaders.EXPIRY, PDU_COLUMN_EXPIRY);
LONG_COLUMN_INDEX_MAP.put(PduHeaders.MESSAGE_SIZE, PDU_COLUMN_MESSAGE_SIZE)287         LONG_COLUMN_INDEX_MAP.put(PduHeaders.MESSAGE_SIZE, PDU_COLUMN_MESSAGE_SIZE);
288 
289         LONG_COLUMN_NAME_MAP = new SparseArray<String>();
LONG_COLUMN_NAME_MAP.put(PduHeaders.DATE, Mms.DATE)290         LONG_COLUMN_NAME_MAP.put(PduHeaders.DATE, Mms.DATE);
LONG_COLUMN_NAME_MAP.put(PduHeaders.DELIVERY_TIME, Mms.DELIVERY_TIME)291         LONG_COLUMN_NAME_MAP.put(PduHeaders.DELIVERY_TIME, Mms.DELIVERY_TIME);
LONG_COLUMN_NAME_MAP.put(PduHeaders.EXPIRY, Mms.EXPIRY)292         LONG_COLUMN_NAME_MAP.put(PduHeaders.EXPIRY, Mms.EXPIRY);
LONG_COLUMN_NAME_MAP.put(PduHeaders.MESSAGE_SIZE, Mms.MESSAGE_SIZE)293         LONG_COLUMN_NAME_MAP.put(PduHeaders.MESSAGE_SIZE, Mms.MESSAGE_SIZE);
294 
295         PDU_CACHE_INSTANCE = PduCache.getInstance();
296     }
297 
298     private final Context mContext;
299 
300     private final ContentResolver mContentResolver;
301 
PduPersister(final Context context)302     private PduPersister(final Context context) {
303         mContext = context;
304         mContentResolver = context.getContentResolver();
305     }
306 
307     /** Get(or create if not exist) an instance of PduPersister */
getPduPersister(final Context context)308     public static PduPersister getPduPersister(final Context context) {
309         if ((sPersister == null) || !context.equals(sPersister.mContext)) {
310             sPersister = new PduPersister(context);
311         }
312         if (LOCAL_LOGV) {
313             LogUtil.v(TAG, "PduPersister getPduPersister");
314         }
315 
316         return sPersister;
317     }
318 
setEncodedStringValueToHeaders( final Cursor c, final int columnIndex, final PduHeaders headers, final int mapColumn)319     private void setEncodedStringValueToHeaders(
320             final Cursor c, final int columnIndex,
321             final PduHeaders headers, final int mapColumn) {
322         final String s = c.getString(columnIndex);
323         if ((s != null) && (s.length() > 0)) {
324             final int charsetColumnIndex = CHARSET_COLUMN_INDEX_MAP.get(mapColumn);
325             final int charset = c.getInt(charsetColumnIndex);
326             final EncodedStringValue value = new EncodedStringValue(
327                     charset, getBytes(s));
328             headers.setEncodedStringValue(value, mapColumn);
329         }
330     }
331 
setTextStringToHeaders( final Cursor c, final int columnIndex, final PduHeaders headers, final int mapColumn)332     private void setTextStringToHeaders(
333             final Cursor c, final int columnIndex,
334             final PduHeaders headers, final int mapColumn) {
335         final String s = c.getString(columnIndex);
336         if (s != null) {
337             headers.setTextString(getBytes(s), mapColumn);
338         }
339     }
340 
setOctetToHeaders( final Cursor c, final int columnIndex, final PduHeaders headers, final int mapColumn)341     private void setOctetToHeaders(
342             final Cursor c, final int columnIndex,
343             final PduHeaders headers, final int mapColumn) throws InvalidHeaderValueException {
344         if (!c.isNull(columnIndex)) {
345             final int b = c.getInt(columnIndex);
346             headers.setOctet(b, mapColumn);
347         }
348     }
349 
setLongToHeaders( final Cursor c, final int columnIndex, final PduHeaders headers, final int mapColumn)350     private void setLongToHeaders(
351             final Cursor c, final int columnIndex,
352             final PduHeaders headers, final int mapColumn) {
353         if (!c.isNull(columnIndex)) {
354             final long l = c.getLong(columnIndex);
355             headers.setLongInteger(l, mapColumn);
356         }
357     }
358 
getIntegerFromPartColumn(final Cursor c, final int columnIndex)359     private Integer getIntegerFromPartColumn(final Cursor c, final int columnIndex) {
360         if (!c.isNull(columnIndex)) {
361             return c.getInt(columnIndex);
362         }
363         return null;
364     }
365 
getByteArrayFromPartColumn(final Cursor c, final int columnIndex)366     private byte[] getByteArrayFromPartColumn(final Cursor c, final int columnIndex) {
367         if (!c.isNull(columnIndex)) {
368             return getBytes(c.getString(columnIndex));
369         }
370         return null;
371     }
372 
loadParts(final long msgId)373     private PduPart[] loadParts(final long msgId) throws MmsException {
374         final Cursor c = SqliteWrapper.query(mContext, mContentResolver,
375                 Uri.parse("content://mms/" + msgId + "/part"),
376                 PART_PROJECTION, null, null, null);
377 
378         PduPart[] parts = null;
379 
380         try {
381             if ((c == null) || (c.getCount() == 0)) {
382                 if (LOCAL_LOGV) {
383                     LogUtil.v(TAG, "loadParts(" + msgId + "): no part to load.");
384                 }
385                 return null;
386             }
387 
388             final int partCount = c.getCount();
389             int partIdx = 0;
390             parts = new PduPart[partCount];
391             while (c.moveToNext()) {
392                 final PduPart part = new PduPart();
393                 final Integer charset = getIntegerFromPartColumn(
394                         c, PART_COLUMN_CHARSET);
395                 if (charset != null) {
396                     part.setCharset(charset);
397                 }
398 
399                 final byte[] contentDisposition = getByteArrayFromPartColumn(
400                         c, PART_COLUMN_CONTENT_DISPOSITION);
401                 if (contentDisposition != null) {
402                     part.setContentDisposition(contentDisposition);
403                 }
404 
405                 final byte[] contentId = getByteArrayFromPartColumn(
406                         c, PART_COLUMN_CONTENT_ID);
407                 if (contentId != null) {
408                     part.setContentId(contentId);
409                 }
410 
411                 final byte[] contentLocation = getByteArrayFromPartColumn(
412                         c, PART_COLUMN_CONTENT_LOCATION);
413                 if (contentLocation != null) {
414                     part.setContentLocation(contentLocation);
415                 }
416 
417                 final byte[] contentType = getByteArrayFromPartColumn(
418                         c, PART_COLUMN_CONTENT_TYPE);
419                 if (contentType != null) {
420                     part.setContentType(contentType);
421                 } else {
422                     throw new MmsException("Content-Type must be set.");
423                 }
424 
425                 final byte[] fileName = getByteArrayFromPartColumn(
426                         c, PART_COLUMN_FILENAME);
427                 if (fileName != null) {
428                     part.setFilename(fileName);
429                 }
430 
431                 final byte[] name = getByteArrayFromPartColumn(
432                         c, PART_COLUMN_NAME);
433                 if (name != null) {
434                     part.setName(name);
435                 }
436 
437                 // Construct a Uri for this part.
438                 final long partId = c.getLong(PART_COLUMN_ID);
439                 final Uri partURI = Uri.parse("content://mms/part/" + partId);
440                 part.setDataUri(partURI);
441 
442                 // For images/audio/video, we won't keep their data in Part
443                 // because their renderer accept Uri as source.
444                 final String type = toIsoString(contentType);
445                 if (!ContentType.isImageType(type)
446                         && !ContentType.isAudioType(type)
447                         && !ContentType.isVideoType(type)) {
448                     final ByteArrayOutputStream baos = new ByteArrayOutputStream();
449                     InputStream is = null;
450 
451                     // Store simple string values directly in the database instead of an
452                     // external file.  This makes the text searchable and retrieval slightly
453                     // faster.
454                     if (ContentType.TEXT_PLAIN.equals(type) || ContentType.APP_SMIL.equals(type)
455                             || ContentType.TEXT_HTML.equals(type)) {
456                         final String text = c.getString(PART_COLUMN_TEXT);
457                         final byte[] blob = new EncodedStringValue(
458                                 charset != null ? charset : CharacterSets.DEFAULT_CHARSET,
459                                 text != null ? text : "")
460                                 .getTextString();
461                         baos.write(blob, 0, blob.length);
462                     } else {
463 
464                         try {
465                             is = mContentResolver.openInputStream(partURI);
466 
467                             final byte[] buffer = new byte[256];
468                             int len = is.read(buffer);
469                             while (len >= 0) {
470                                 baos.write(buffer, 0, len);
471                                 len = is.read(buffer);
472                             }
473                         } catch (final IOException e) {
474                             Log.e(TAG, "Failed to load part data", e);
475                             c.close();
476                             throw new MmsException(e);
477                         } finally {
478                             if (is != null) {
479                                 try {
480                                     is.close();
481                                 } catch (final IOException e) {
482                                     Log.e(TAG, "Failed to close stream", e);
483                                 } // Ignore
484                             }
485                         }
486                     }
487                     part.setData(baos.toByteArray());
488                 }
489                 parts[partIdx++] = part;
490             }
491         } finally {
492             if (c != null) {
493                 c.close();
494             }
495         }
496 
497         return parts;
498     }
499 
loadAddress(final long msgId, final PduHeaders headers)500     private void loadAddress(final long msgId, final PduHeaders headers) {
501         final Cursor c = SqliteWrapper.query(mContext, mContentResolver,
502                 Uri.parse("content://mms/" + msgId + "/addr"),
503                 new String[]{Addr.ADDRESS, Addr.CHARSET, Addr.TYPE},
504                 null, null, null);
505 
506         if (c != null) {
507             try {
508                 while (c.moveToNext()) {
509                     final String addr = c.getString(0);
510                     if (!TextUtils.isEmpty(addr)) {
511                         final int addrType = c.getInt(2);
512                         switch (addrType) {
513                             case PduHeaders.FROM:
514                                 headers.setEncodedStringValue(
515                                         new EncodedStringValue(c.getInt(1), getBytes(addr)),
516                                         addrType);
517                                 break;
518                             case PduHeaders.TO:
519                             case PduHeaders.CC:
520                             case PduHeaders.BCC:
521                                 headers.appendEncodedStringValue(
522                                         new EncodedStringValue(c.getInt(1), getBytes(addr)),
523                                         addrType);
524                                 break;
525                             default:
526                                 Log.e(TAG, "Unknown address type: " + addrType);
527                                 break;
528                         }
529                     }
530                 }
531             } finally {
532                 c.close();
533             }
534         }
535     }
536 
537     /**
538      * Load a PDU from a given cursor
539      *
540      * @param c The cursor
541      * @return A parsed PDU from the database row
542      */
load(final Cursor c)543     public GenericPdu load(final Cursor c) throws MmsException {
544         final PduHeaders headers = new PduHeaders();
545         final long msgId = c.getLong(PDU_COLUMN_ID);
546         // Fill in the headers from the PDU columns
547         loadHeadersFromCursor(c, headers);
548         // Load address information of the MM.
549         loadAddress(msgId, headers);
550         // Load parts for the PDU body
551         final int msgType = headers.getOctet(PduHeaders.MESSAGE_TYPE);
552         final PduBody body = loadBody(msgId, msgType);
553         return createPdu(msgType, headers, body);
554     }
555 
556     /**
557      * Load a PDU from storage by given Uri.
558      *
559      * @param uri            The Uri of the PDU to be loaded.
560      * @return A generic PDU object, it may be cast to dedicated PDU.
561      * @throws MmsException Failed to load some fields of a PDU.
562      */
load(final Uri uri)563     public GenericPdu load(final Uri uri) throws MmsException {
564         GenericPdu pdu = null;
565         PduCacheEntry cacheEntry = null;
566         int msgBox = 0;
567         final long threadId = -1;
568         try {
569             synchronized (PDU_CACHE_INSTANCE) {
570                 if (PDU_CACHE_INSTANCE.isUpdating(uri)) {
571                     if (LOCAL_LOGV) {
572                         LogUtil.v(TAG, "load: " + uri + " blocked by isUpdating()");
573                     }
574                     try {
575                         PDU_CACHE_INSTANCE.wait();
576                     } catch (final InterruptedException e) {
577                         Log.e(TAG, "load: ", e);
578                     }
579                 }
580 
581                 // Check if the pdu is already loaded
582                 cacheEntry = PDU_CACHE_INSTANCE.get(uri);
583                 if (cacheEntry != null) {
584                     return cacheEntry.getPdu();
585                 }
586 
587                 // Tell the cache to indicate to other callers that this item
588                 // is currently being updated.
589                 PDU_CACHE_INSTANCE.setUpdating(uri, true);
590             }
591 
592             final Cursor c = SqliteWrapper.query(mContext, mContentResolver, uri,
593                     PDU_PROJECTION, null, null, null);
594             final PduHeaders headers = new PduHeaders();
595             final long msgId = ContentUris.parseId(uri);
596 
597             try {
598                 if ((c == null) || (c.getCount() != 1) || !c.moveToFirst()) {
599                     return null;  // MMS not found
600                 }
601 
602                 msgBox = c.getInt(PDU_COLUMN_MESSAGE_BOX);
603                 //threadId = c.getLong(PDU_COLUMN_THREAD_ID);
604                 loadHeadersFromCursor(c, headers);
605             } finally {
606                 if (c != null) {
607                     c.close();
608                 }
609             }
610 
611             // Check whether 'msgId' has been assigned a valid value.
612             if (msgId == -1L) {
613                 throw new MmsException("Error! ID of the message: -1.");
614             }
615 
616             // Load address information of the MM.
617             loadAddress(msgId, headers);
618 
619             final int msgType = headers.getOctet(PduHeaders.MESSAGE_TYPE);
620             final PduBody body = loadBody(msgId, msgType);
621             pdu = createPdu(msgType, headers, body);
622         } finally {
623             synchronized (PDU_CACHE_INSTANCE) {
624                 if (pdu != null) {
625                     Assert.isNull(PDU_CACHE_INSTANCE.get(uri), "Pdu exists for " + uri);
626                     // Update the cache entry with the real info
627                     cacheEntry = new PduCacheEntry(pdu, msgBox, threadId);
628                     PDU_CACHE_INSTANCE.put(uri, cacheEntry);
629                 }
630                 PDU_CACHE_INSTANCE.setUpdating(uri, false);
631                 PDU_CACHE_INSTANCE.notifyAll(); // tell anybody waiting on this entry to go ahead
632             }
633         }
634         return pdu;
635     }
636 
loadHeadersFromCursor(final Cursor c, final PduHeaders headers)637     private void loadHeadersFromCursor(final Cursor c, final PduHeaders headers)
638             throws InvalidHeaderValueException {
639         for (int i = ENCODED_STRING_COLUMN_INDEX_MAP.size(); --i >= 0; ) {
640             setEncodedStringValueToHeaders(
641                     c, ENCODED_STRING_COLUMN_INDEX_MAP.valueAt(i), headers,
642                     ENCODED_STRING_COLUMN_INDEX_MAP.keyAt(i));
643         }
644         for (int i = TEXT_STRING_COLUMN_INDEX_MAP.size(); --i >= 0; ) {
645             setTextStringToHeaders(
646                     c, TEXT_STRING_COLUMN_INDEX_MAP.valueAt(i), headers,
647                     TEXT_STRING_COLUMN_INDEX_MAP.keyAt(i));
648         }
649         for (int i = OCTET_COLUMN_INDEX_MAP.size(); --i >= 0; ) {
650             setOctetToHeaders(
651                     c, OCTET_COLUMN_INDEX_MAP.valueAt(i), headers,
652                     OCTET_COLUMN_INDEX_MAP.keyAt(i));
653         }
654         for (int i = LONG_COLUMN_INDEX_MAP.size(); --i >= 0; ) {
655             setLongToHeaders(
656                     c, LONG_COLUMN_INDEX_MAP.valueAt(i), headers,
657                     LONG_COLUMN_INDEX_MAP.keyAt(i));
658         }
659     }
660 
createPdu(final int msgType, final PduHeaders headers, final PduBody body)661     private GenericPdu createPdu(final int msgType, final PduHeaders headers, final PduBody body)
662             throws MmsException {
663         switch (msgType) {
664             case PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND:
665                 return new NotificationInd(headers);
666             case PduHeaders.MESSAGE_TYPE_DELIVERY_IND:
667                 return new DeliveryInd(headers);
668             case PduHeaders.MESSAGE_TYPE_READ_ORIG_IND:
669                 return new ReadOrigInd(headers);
670             case PduHeaders.MESSAGE_TYPE_RETRIEVE_CONF:
671                 return new RetrieveConf(headers, body);
672             case PduHeaders.MESSAGE_TYPE_SEND_REQ:
673                 return new SendReq(headers, body);
674             case PduHeaders.MESSAGE_TYPE_ACKNOWLEDGE_IND:
675                 return new AcknowledgeInd(headers);
676             case PduHeaders.MESSAGE_TYPE_NOTIFYRESP_IND:
677                 return new NotifyRespInd(headers);
678             case PduHeaders.MESSAGE_TYPE_READ_REC_IND:
679                 return new ReadRecInd(headers);
680             case PduHeaders.MESSAGE_TYPE_SEND_CONF:
681             case PduHeaders.MESSAGE_TYPE_FORWARD_REQ:
682             case PduHeaders.MESSAGE_TYPE_FORWARD_CONF:
683             case PduHeaders.MESSAGE_TYPE_MBOX_STORE_REQ:
684             case PduHeaders.MESSAGE_TYPE_MBOX_STORE_CONF:
685             case PduHeaders.MESSAGE_TYPE_MBOX_VIEW_REQ:
686             case PduHeaders.MESSAGE_TYPE_MBOX_VIEW_CONF:
687             case PduHeaders.MESSAGE_TYPE_MBOX_UPLOAD_REQ:
688             case PduHeaders.MESSAGE_TYPE_MBOX_UPLOAD_CONF:
689             case PduHeaders.MESSAGE_TYPE_MBOX_DELETE_REQ:
690             case PduHeaders.MESSAGE_TYPE_MBOX_DELETE_CONF:
691             case PduHeaders.MESSAGE_TYPE_MBOX_DESCR:
692             case PduHeaders.MESSAGE_TYPE_DELETE_REQ:
693             case PduHeaders.MESSAGE_TYPE_DELETE_CONF:
694             case PduHeaders.MESSAGE_TYPE_CANCEL_REQ:
695             case PduHeaders.MESSAGE_TYPE_CANCEL_CONF:
696                 throw new MmsException(
697                         "Unsupported PDU type: " + Integer.toHexString(msgType));
698 
699             default:
700                 throw new MmsException(
701                         "Unrecognized PDU type: " + Integer.toHexString(msgType));
702         }
703     }
704 
loadBody(final long msgId, final int msgType)705     private PduBody loadBody(final long msgId, final int msgType) throws MmsException {
706         final PduBody body = new PduBody();
707 
708         // For PDU which type is M_retrieve.conf or Send.req, we should
709         // load multiparts and put them into the body of the PDU.
710         if ((msgType == PduHeaders.MESSAGE_TYPE_RETRIEVE_CONF)
711                 || (msgType == PduHeaders.MESSAGE_TYPE_SEND_REQ)) {
712             final PduPart[] parts = loadParts(msgId);
713             if (parts != null) {
714                 final int partsNum = parts.length;
715                 for (int i = 0; i < partsNum; i++) {
716                     body.addPart(parts[i]);
717                 }
718             }
719         }
720 
721         return body;
722     }
723 
persistAddress( final long msgId, final int type, final EncodedStringValue[] array)724     private void persistAddress(
725             final long msgId, final int type, final EncodedStringValue[] array) {
726         final ContentValues values = new ContentValues(3);
727 
728         for (final EncodedStringValue addr : array) {
729             values.clear(); // Clear all values first.
730             values.put(Addr.ADDRESS, toIsoString(addr.getTextString()));
731             values.put(Addr.CHARSET, addr.getCharacterSet());
732             values.put(Addr.TYPE, type);
733 
734             final Uri uri = Uri.parse("content://mms/" + msgId + "/addr");
735             SqliteWrapper.insert(mContext, mContentResolver, uri, values);
736         }
737     }
738 
getPartContentType(final PduPart part)739     private static String getPartContentType(final PduPart part) {
740         return part.getContentType() == null ? null : toIsoString(part.getContentType());
741     }
742 
getValues(final PduPart part, final ContentValues values)743     private static void getValues(final PduPart part, final ContentValues values) {
744         byte[] bytes = part.getFilename();
745         if (bytes != null) {
746             values.put(Part.FILENAME, new String(bytes));
747         }
748 
749         bytes = part.getName();
750         if (bytes != null) {
751             values.put(Part.NAME, new String(bytes));
752         }
753 
754         bytes = part.getContentDisposition();
755         if (bytes != null) {
756             values.put(Part.CONTENT_DISPOSITION, toIsoString(bytes));
757         }
758 
759         bytes = part.getContentId();
760         if (bytes != null) {
761             values.put(Part.CONTENT_ID, toIsoString(bytes));
762         }
763 
764         bytes = part.getContentLocation();
765         if (bytes != null) {
766             values.put(Part.CONTENT_LOCATION, toIsoString(bytes));
767         }
768     }
769 
persistPart(final PduPart part, final long msgId, final Map<Uri, InputStream> preOpenedFiles)770     public Uri persistPart(final PduPart part, final long msgId,
771             final Map<Uri, InputStream> preOpenedFiles) throws MmsException {
772         final Uri uri = Uri.parse("content://mms/" + msgId + "/part");
773         final ContentValues values = new ContentValues(8);
774 
775         final int charset = part.getCharset();
776         if (charset != 0) {
777             values.put(Part.CHARSET, charset);
778         }
779 
780         String contentType = getPartContentType(part);
781         final byte[] data = part.getData();
782 
783         if (LOCAL_LOGV) {
784             LogUtil.v(TAG, "PduPersister.persistPart part: " + uri + " contentType: " +
785                     contentType);
786         }
787 
788         if (contentType != null) {
789             // There is no "image/jpg" in Android (and it's an invalid mimetype).
790             // Change it to "image/jpeg"
791             if (ContentType.IMAGE_JPG.equals(contentType)) {
792                 contentType = ContentType.IMAGE_JPEG;
793             }
794 
795             // On somes phones, a vcard comes in as text/plain instead of text/v-card.
796             // Fix it if necessary.
797             if (ContentType.TEXT_PLAIN.equals(contentType) && data != null) {
798                 // There might be a more efficient way to just check the beginning of the string
799                 // without encoding the whole thing, but we're concerned that with various
800                 // characters sets, just comparing the byte data to BEGIN_VCARD would not be
801                 // reliable.
802                 final String encodedDataString = new EncodedStringValue(charset, data).getString();
803                 if (encodedDataString != null && encodedDataString.startsWith(BEGIN_VCARD)) {
804                     contentType = ContentType.TEXT_VCARD;
805                     part.setContentType(contentType.getBytes());
806                     if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
807                         LogUtil.d(TAG, "PduPersister.persistPart part: " + uri + " contentType: " +
808                                 contentType + " changing to vcard");
809                     }
810                 }
811             }
812 
813             values.put(Part.CONTENT_TYPE, contentType);
814             // To ensure the SMIL part is always the first part.
815             if (ContentType.APP_SMIL.equals(contentType)) {
816                 values.put(Part.SEQ, -1);
817             }
818         } else {
819             throw new MmsException("MIME type of the part must be set.");
820         }
821 
822         getValues(part, values);
823 
824         Uri res = null;
825 
826         try {
827             res = SqliteWrapper.insert(mContext, mContentResolver, uri, values);
828         } catch (IllegalStateException e) {
829             // Currently the MMS provider throws an IllegalStateException when it's out of space
830             LogUtil.e(TAG, "SqliteWrapper.insert threw: ", e);
831         }
832 
833         if (res == null) {
834             throw new MmsException("Failed to persist part, return null.");
835         }
836 
837         persistData(part, res, contentType, preOpenedFiles);
838         // After successfully store the data, we should update
839         // the dataUri of the part.
840         part.setDataUri(res);
841 
842         return res;
843     }
844 
845     /**
846      * Save data of the part into storage. The source data may be given
847      * by a byte[] or a Uri. If it's a byte[], directly save it
848      * into storage, otherwise load source data from the dataUri and then
849      * save it. If the data is an image, we may scale down it according
850      * to user preference.
851      *
852      * @param part           The PDU part which contains data to be saved.
853      * @param uri            The URI of the part.
854      * @param contentType    The MIME type of the part.
855      * @param preOpenedFiles if not null, a map of preopened InputStreams for the parts.
856      * @throws MmsException Cannot find source data or error occurred
857      *                      while saving the data.
858      */
persistData(final PduPart part, final Uri uri, final String contentType, final Map<Uri, InputStream> preOpenedFiles)859     private void persistData(final PduPart part, final Uri uri,
860             final String contentType, final Map<Uri, InputStream> preOpenedFiles)
861             throws MmsException {
862         OutputStream os = null;
863         InputStream is = null;
864         DrmConvertSession drmConvertSession = null;
865         Uri dataUri = null;
866         String path = null;
867 
868         try {
869             final byte[] data = part.getData();
870             final int charset = part.getCharset();
871             if (ContentType.TEXT_PLAIN.equals(contentType)
872                     || ContentType.APP_SMIL.equals(contentType)
873                     || ContentType.TEXT_HTML.equals(contentType)) {
874                 // Some phone could send MMS with a text part having empty data
875                 // Let's just skip those parts.
876                 // EncodedStringValue() throws NPE if data is empty
877                 if (data != null) {
878                     final ContentValues cv = new ContentValues();
879                     cv.put(Mms.Part.TEXT, new EncodedStringValue(charset, data).getString());
880                     if (mContentResolver.update(uri, cv, null, null) != 1) {
881                         throw new MmsException("unable to update " + uri.toString());
882                     }
883                 }
884             } else {
885                 final boolean isDrm = DownloadDrmHelper.isDrmConvertNeeded(contentType);
886                 if (isDrm) {
887                     if (uri != null) {
888                         try {
889                             path = convertUriToPath(mContext, uri);
890                             if (LOCAL_LOGV) {
891                                 LogUtil.v(TAG, "drm uri: " + uri + " path: " + path);
892                             }
893                             final File f = new File(path);
894                             final long len = f.length();
895                             if (LOCAL_LOGV) {
896                                 LogUtil.v(TAG, "drm path: " + path + " len: " + len);
897                             }
898                             if (len > 0) {
899                                 // we're not going to re-persist and re-encrypt an already
900                                 // converted drm file
901                                 return;
902                             }
903                         } catch (final Exception e) {
904                             Log.e(TAG, "Can't get file info for: " + part.getDataUri(), e);
905                         }
906                     }
907                     // We haven't converted the file yet, start the conversion
908                     drmConvertSession = DrmConvertSession.open(mContext, contentType);
909                     if (drmConvertSession == null) {
910                         throw new MmsException("Mimetype " + contentType +
911                                 " can not be converted.");
912                     }
913                 }
914                 // uri can look like:
915                 // content://mms/part/98
916                 os = mContentResolver.openOutputStream(uri);
917                 if (os == null) {
918                     throw new MmsException("Failed to create output stream on " + uri);
919                 }
920                 if (data == null) {
921                     dataUri = part.getDataUri();
922                     if ((dataUri == null) || (dataUri.equals(uri))) {
923                         Log.w(TAG, "Can't find data for this part.");
924                         return;
925                     }
926                     // dataUri can look like:
927                     // content://com.google.android.gallery3d.provider/picasa/item/5720646660183715
928                     if (preOpenedFiles != null && preOpenedFiles.containsKey(dataUri)) {
929                         is = preOpenedFiles.get(dataUri);
930                     }
931                     if (is == null) {
932                         is = mContentResolver.openInputStream(dataUri);
933                     }
934                     if (is == null) {
935                         throw new MmsException("Failed to create input stream on " + dataUri);
936                     }
937                     if (LOCAL_LOGV) {
938                         LogUtil.v(TAG, "Saving data to: " + uri);
939                     }
940 
941                     final byte[] buffer = new byte[8192];
942                     for (int len = 0; (len = is.read(buffer)) != -1; ) {
943                         if (!isDrm) {
944                             os.write(buffer, 0, len);
945                         } else {
946                             final byte[] convertedData = drmConvertSession.convert(buffer, len);
947                             if (convertedData != null) {
948                                 os.write(convertedData, 0, convertedData.length);
949                             } else {
950                                 throw new MmsException("Error converting drm data.");
951                             }
952                         }
953                     }
954                 } else {
955                     if (LOCAL_LOGV) {
956                         LogUtil.v(TAG, "Saving data to: " + uri);
957                     }
958                     if (!isDrm) {
959                         os.write(data);
960                     } else {
961                         dataUri = uri;
962                         final byte[] convertedData = drmConvertSession.convert(data, data.length);
963                         if (convertedData != null) {
964                             os.write(convertedData, 0, convertedData.length);
965                         } else {
966                             throw new MmsException("Error converting drm data.");
967                         }
968                     }
969                 }
970             }
971         } catch (final SQLiteException e) {
972             Log.e(TAG, "Failed with SQLiteException.", e);
973             throw new MmsException(e);
974         } catch (final FileNotFoundException e) {
975             Log.e(TAG, "Failed to open Input/Output stream.", e);
976             throw new MmsException(e);
977         } catch (final IOException e) {
978             Log.e(TAG, "Failed to read/write data.", e);
979             throw new MmsException(e);
980         } finally {
981             if (os != null) {
982                 try {
983                     os.close();
984                 } catch (final IOException e) {
985                     Log.e(TAG, "IOException while closing: " + os, e);
986                 } // Ignore
987             }
988             if (is != null) {
989                 try {
990                     is.close();
991                 } catch (final IOException e) {
992                     Log.e(TAG, "IOException while closing: " + is, e);
993                 } // Ignore
994             }
995             if (drmConvertSession != null) {
996                 drmConvertSession.close(path);
997 
998                 // Reset the permissions on the encrypted part file so everyone has only read
999                 // permission.
1000                 final File f = new File(path);
1001                 final ContentValues values = new ContentValues(0);
1002                 SqliteWrapper.update(mContext, mContentResolver,
1003                         Uri.parse("content://mms/resetFilePerm/" + f.getName()),
1004                         values, null, null);
1005             }
1006         }
1007     }
1008 
1009     /**
1010      * This method expects uri in the following format
1011      *     content://media/<table_name>/<row_index> (or)
1012      *     file://sdcard/test.mp4
1013      *     http://test.com/test.mp4
1014      *
1015      * Here <table_name> shall be "video" or "audio" or "images"
1016      * <row_index> the index of the content in given table
1017      */
convertUriToPath(final Context context, final Uri uri)1018     public static String convertUriToPath(final Context context, final Uri uri) {
1019         String path = null;
1020         if (null != uri) {
1021             final String scheme = uri.getScheme();
1022             if (null == scheme || scheme.equals("") || UriUtil.isFileUri(uri)) {
1023                 path = uri.getPath();
1024 
1025             } else if (scheme.equals("http")) {
1026                 path = uri.toString();
1027 
1028             } else if (scheme.equals(ContentResolver.SCHEME_CONTENT)) {
1029                 final String[] projection = new String[] {MediaStore.MediaColumns.DATA};
1030                 Cursor cursor = null;
1031                 try {
1032                     cursor = context.getContentResolver().query(uri, projection, null,
1033                             null, null);
1034                     if (null == cursor || 0 == cursor.getCount() || !cursor.moveToFirst()) {
1035                         throw new IllegalArgumentException("Given Uri could not be found" +
1036                                 " in media store");
1037                     }
1038                     final int pathIndex =
1039                             cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA);
1040                     path = cursor.getString(pathIndex);
1041                 } catch (final SQLiteException e) {
1042                     throw new IllegalArgumentException("Given Uri is not formatted in a way " +
1043                             "so that it can be found in media store.");
1044                 } finally {
1045                     if (null != cursor) {
1046                         cursor.close();
1047                     }
1048                 }
1049             } else {
1050                 throw new IllegalArgumentException("Given Uri scheme is not supported");
1051             }
1052         }
1053         return path;
1054     }
1055 
updateAddress( final long msgId, final int type, final EncodedStringValue[] array)1056     private void updateAddress(
1057             final long msgId, final int type, final EncodedStringValue[] array) {
1058         // Delete old address information and then insert new ones.
1059         SqliteWrapper.delete(mContext, mContentResolver,
1060                 Uri.parse("content://mms/" + msgId + "/addr"),
1061                 Addr.TYPE + "=" + type, null);
1062 
1063         persistAddress(msgId, type, array);
1064     }
1065 
1066     /**
1067      * Update headers of a SendReq.
1068      *
1069      * @param uri The PDU which need to be updated.
1070      * @param pdu New headers.
1071      * @throws MmsException Bad URI or updating failed.
1072      */
updateHeaders(final Uri uri, final SendReq sendReq)1073     public void updateHeaders(final Uri uri, final SendReq sendReq) {
1074         synchronized (PDU_CACHE_INSTANCE) {
1075             // If the cache item is getting updated, wait until it's done updating before
1076             // purging it.
1077             if (PDU_CACHE_INSTANCE.isUpdating(uri)) {
1078                 if (LOCAL_LOGV) {
1079                     LogUtil.v(TAG, "updateHeaders: " + uri + " blocked by isUpdating()");
1080                 }
1081                 try {
1082                     PDU_CACHE_INSTANCE.wait();
1083                 } catch (final InterruptedException e) {
1084                     Log.e(TAG, "updateHeaders: ", e);
1085                 }
1086             }
1087         }
1088         PDU_CACHE_INSTANCE.purge(uri);
1089 
1090         final ContentValues values = new ContentValues(10);
1091         final byte[] contentType = sendReq.getContentType();
1092         if (contentType != null) {
1093             values.put(Mms.CONTENT_TYPE, toIsoString(contentType));
1094         }
1095 
1096         final long date = sendReq.getDate();
1097         if (date != -1) {
1098             values.put(Mms.DATE, date);
1099         }
1100 
1101         final int deliveryReport = sendReq.getDeliveryReport();
1102         if (deliveryReport != 0) {
1103             values.put(Mms.DELIVERY_REPORT, deliveryReport);
1104         }
1105 
1106         final long expiry = sendReq.getExpiry();
1107         if (expiry != -1) {
1108             values.put(Mms.EXPIRY, expiry);
1109         }
1110 
1111         final byte[] msgClass = sendReq.getMessageClass();
1112         if (msgClass != null) {
1113             values.put(Mms.MESSAGE_CLASS, toIsoString(msgClass));
1114         }
1115 
1116         final int priority = sendReq.getPriority();
1117         if (priority != 0) {
1118             values.put(Mms.PRIORITY, priority);
1119         }
1120 
1121         final int readReport = sendReq.getReadReport();
1122         if (readReport != 0) {
1123             values.put(Mms.READ_REPORT, readReport);
1124         }
1125 
1126         final byte[] transId = sendReq.getTransactionId();
1127         if (transId != null) {
1128             values.put(Mms.TRANSACTION_ID, toIsoString(transId));
1129         }
1130 
1131         final EncodedStringValue subject = sendReq.getSubject();
1132         if (subject != null) {
1133             values.put(Mms.SUBJECT, toIsoString(subject.getTextString()));
1134             values.put(Mms.SUBJECT_CHARSET, subject.getCharacterSet());
1135         } else {
1136             values.put(Mms.SUBJECT, "");
1137         }
1138 
1139         final long messageSize = sendReq.getMessageSize();
1140         if (messageSize > 0) {
1141             values.put(Mms.MESSAGE_SIZE, messageSize);
1142         }
1143 
1144         final PduHeaders headers = sendReq.getPduHeaders();
1145         final HashSet<String> recipients = new HashSet<String>();
1146         for (final int addrType : ADDRESS_FIELDS) {
1147             EncodedStringValue[] array = null;
1148             if (addrType == PduHeaders.FROM) {
1149                 final EncodedStringValue v = headers.getEncodedStringValue(addrType);
1150                 if (v != null) {
1151                     array = new EncodedStringValue[1];
1152                     array[0] = v;
1153                 }
1154             } else {
1155                 array = headers.getEncodedStringValues(addrType);
1156             }
1157 
1158             if (array != null) {
1159                 final long msgId = ContentUris.parseId(uri);
1160                 updateAddress(msgId, addrType, array);
1161                 if (addrType == PduHeaders.TO) {
1162                     for (final EncodedStringValue v : array) {
1163                         if (v != null) {
1164                             recipients.add(v.getString());
1165                         }
1166                     }
1167                 }
1168             }
1169         }
1170         if (!recipients.isEmpty()) {
1171             final long threadId = MmsSmsUtils.Threads.getOrCreateThreadId(mContext, recipients);
1172             values.put(Mms.THREAD_ID, threadId);
1173         }
1174 
1175         SqliteWrapper.update(mContext, mContentResolver, uri, values, null, null);
1176     }
1177 
1178 
updatePart(final Uri uri, final PduPart part, final Map<Uri, InputStream> preOpenedFiles)1179     private void updatePart(final Uri uri, final PduPart part,
1180             final Map<Uri, InputStream> preOpenedFiles)
1181             throws MmsException {
1182         final ContentValues values = new ContentValues(7);
1183 
1184         final int charset = part.getCharset();
1185         if (charset != 0) {
1186             values.put(Part.CHARSET, charset);
1187         }
1188 
1189         String contentType = null;
1190         if (part.getContentType() != null) {
1191             contentType = toIsoString(part.getContentType());
1192             values.put(Part.CONTENT_TYPE, contentType);
1193         } else {
1194             throw new MmsException("MIME type of the part must be set.");
1195         }
1196 
1197         getValues(part, values);
1198 
1199         SqliteWrapper.update(mContext, mContentResolver, uri, values, null, null);
1200 
1201         // Only update the data when:
1202         // 1. New binary data supplied or
1203         // 2. The Uri of the part is different from the current one.
1204         if ((part.getData() != null)
1205                 || (!uri.equals(part.getDataUri()))) {
1206             persistData(part, uri, contentType, preOpenedFiles);
1207         }
1208     }
1209 
1210     /**
1211      * Update all parts of a PDU.
1212      *
1213      * @param uri            The PDU which need to be updated.
1214      * @param body           New message body of the PDU.
1215      * @param preOpenedFiles if not null, a map of preopened InputStreams for the parts.
1216      * @throws MmsException Bad URI or updating failed.
1217      */
updateParts(final Uri uri, final PduBody body, final Map<Uri, InputStream> preOpenedFiles)1218     public void updateParts(final Uri uri, final PduBody body,
1219             final Map<Uri, InputStream> preOpenedFiles)
1220             throws MmsException {
1221         try {
1222             PduCacheEntry cacheEntry;
1223             synchronized (PDU_CACHE_INSTANCE) {
1224                 if (PDU_CACHE_INSTANCE.isUpdating(uri)) {
1225                     if (LOCAL_LOGV) {
1226                         LogUtil.v(TAG, "updateParts: " + uri + " blocked by isUpdating()");
1227                     }
1228                     try {
1229                         PDU_CACHE_INSTANCE.wait();
1230                     } catch (final InterruptedException e) {
1231                         Log.e(TAG, "updateParts: ", e);
1232                     }
1233                     cacheEntry = PDU_CACHE_INSTANCE.get(uri);
1234                     if (cacheEntry != null) {
1235                         ((MultimediaMessagePdu) cacheEntry.getPdu()).setBody(body);
1236                     }
1237                 }
1238                 // Tell the cache to indicate to other callers that this item
1239                 // is currently being updated.
1240                 PDU_CACHE_INSTANCE.setUpdating(uri, true);
1241             }
1242 
1243             final ArrayList<PduPart> toBeCreated = new ArrayList<PduPart>();
1244             final ArrayMap<Uri, PduPart> toBeUpdated = new ArrayMap<Uri, PduPart>();
1245 
1246             final int partsNum = body.getPartsNum();
1247             final StringBuilder filter = new StringBuilder().append('(');
1248             for (int i = 0; i < partsNum; i++) {
1249                 final PduPart part = body.getPart(i);
1250                 final Uri partUri = part.getDataUri();
1251                 if ((partUri == null) || TextUtils.isEmpty(partUri.getAuthority())
1252                         || !partUri.getAuthority().startsWith("mms")) {
1253                     toBeCreated.add(part);
1254                 } else {
1255                     toBeUpdated.put(partUri, part);
1256 
1257                     // Don't use 'i > 0' to determine whether we should append
1258                     // 'AND' since 'i = 0' may be skipped in another branch.
1259                     if (filter.length() > 1) {
1260                         filter.append(" AND ");
1261                     }
1262 
1263                     filter.append(Part._ID);
1264                     filter.append("!=");
1265                     DatabaseUtils.appendEscapedSQLString(filter, partUri.getLastPathSegment());
1266                 }
1267             }
1268             filter.append(')');
1269 
1270             final long msgId = ContentUris.parseId(uri);
1271 
1272             // Remove the parts which doesn't exist anymore.
1273             SqliteWrapper.delete(mContext, mContentResolver,
1274                     Uri.parse(Mms.CONTENT_URI + "/" + msgId + "/part"),
1275                     filter.length() > 2 ? filter.toString() : null, null);
1276 
1277             // Create new parts which didn't exist before.
1278             for (final PduPart part : toBeCreated) {
1279                 persistPart(part, msgId, preOpenedFiles);
1280             }
1281 
1282             // Update the modified parts.
1283             for (final Map.Entry<Uri, PduPart> e : toBeUpdated.entrySet()) {
1284                 updatePart(e.getKey(), e.getValue(), preOpenedFiles);
1285             }
1286         } finally {
1287             synchronized (PDU_CACHE_INSTANCE) {
1288                 PDU_CACHE_INSTANCE.setUpdating(uri, false);
1289                 PDU_CACHE_INSTANCE.notifyAll();
1290             }
1291         }
1292     }
1293 
1294     /**
1295      * Persist a PDU object to specific location in the storage.
1296      *
1297      * @param pdu             The PDU object to be stored.
1298      * @param uri             Where to store the given PDU object.
1299      * @param subId           Subscription id associated with this message.
1300      * @param subPhoneNumber TODO
1301      * @param preOpenedFiles  if not null, a map of preopened InputStreams for the parts.
1302      * @return A Uri which can be used to access the stored PDU.
1303      */
persist(final GenericPdu pdu, final Uri uri, final int subId, final String subPhoneNumber, final Map<Uri, InputStream> preOpenedFiles)1304     public Uri persist(final GenericPdu pdu, final Uri uri, final int subId,
1305             final String subPhoneNumber, final Map<Uri, InputStream> preOpenedFiles)
1306             throws MmsException {
1307         if (uri == null) {
1308             throw new MmsException("Uri may not be null.");
1309         }
1310         long msgId = -1;
1311         try {
1312             msgId = ContentUris.parseId(uri);
1313         } catch (final NumberFormatException e) {
1314             // the uri ends with "inbox" or something else like that
1315         }
1316         final boolean existingUri = msgId != -1;
1317 
1318         if (!existingUri && MESSAGE_BOX_MAP.get(uri) == null) {
1319             throw new MmsException(
1320                     "Bad destination, must be one of "
1321                             + "content://mms/inbox, content://mms/sent, "
1322                             + "content://mms/drafts, content://mms/outbox, "
1323                             + "content://mms/temp."
1324             );
1325         }
1326         synchronized (PDU_CACHE_INSTANCE) {
1327             // If the cache item is getting updated, wait until it's done updating before
1328             // purging it.
1329             if (PDU_CACHE_INSTANCE.isUpdating(uri)) {
1330                 if (LOCAL_LOGV) {
1331                     LogUtil.v(TAG, "persist: " + uri + " blocked by isUpdating()");
1332                 }
1333                 try {
1334                     PDU_CACHE_INSTANCE.wait();
1335                 } catch (final InterruptedException e) {
1336                     Log.e(TAG, "persist1: ", e);
1337                 }
1338             }
1339         }
1340         PDU_CACHE_INSTANCE.purge(uri);
1341 
1342         final PduHeaders header = pdu.getPduHeaders();
1343         PduBody body = null;
1344         ContentValues values = new ContentValues();
1345 
1346         // Mark new messages as seen in the telephony database so that we don't have to
1347         // do a global "set all messages as seen" since that occasionally seems to be
1348         // problematic (i.e. very slow).  See bug 18189471.
1349         values.put(Mms.SEEN, 1);
1350 
1351         //Set<Entry<Integer, String>> set;
1352 
1353         for (int i = ENCODED_STRING_COLUMN_NAME_MAP.size(); --i >= 0; ) {
1354             final int field = ENCODED_STRING_COLUMN_NAME_MAP.keyAt(i);
1355             final EncodedStringValue encodedString = header.getEncodedStringValue(field);
1356             if (encodedString != null) {
1357                 final String charsetColumn = CHARSET_COLUMN_NAME_MAP.get(field);
1358                 values.put(ENCODED_STRING_COLUMN_NAME_MAP.valueAt(i),
1359                         toIsoString(encodedString.getTextString()));
1360                 values.put(charsetColumn, encodedString.getCharacterSet());
1361             }
1362         }
1363 
1364         for (int i = TEXT_STRING_COLUMN_NAME_MAP.size(); --i >= 0; ) {
1365             final byte[] text = header.getTextString(TEXT_STRING_COLUMN_NAME_MAP.keyAt(i));
1366             if (text != null) {
1367                 values.put(TEXT_STRING_COLUMN_NAME_MAP.valueAt(i), toIsoString(text));
1368             }
1369         }
1370 
1371         for (int i = OCTET_COLUMN_NAME_MAP.size(); --i >= 0; ) {
1372             final int b = header.getOctet(OCTET_COLUMN_NAME_MAP.keyAt(i));
1373             if (b != 0) {
1374                 values.put(OCTET_COLUMN_NAME_MAP.valueAt(i), b);
1375             }
1376         }
1377 
1378         for (int i = LONG_COLUMN_NAME_MAP.size(); --i >= 0; ) {
1379             final long l = header.getLongInteger(LONG_COLUMN_NAME_MAP.keyAt(i));
1380             if (l != -1L) {
1381                 values.put(LONG_COLUMN_NAME_MAP.valueAt(i), l);
1382             }
1383         }
1384 
1385         final SparseArray<EncodedStringValue[]> addressMap =
1386                 new SparseArray<EncodedStringValue[]>(ADDRESS_FIELDS.length);
1387         // Save address information.
1388         for (final int addrType : ADDRESS_FIELDS) {
1389             EncodedStringValue[] array = null;
1390             if (addrType == PduHeaders.FROM) {
1391                 final EncodedStringValue v = header.getEncodedStringValue(addrType);
1392                 if (v != null) {
1393                     array = new EncodedStringValue[1];
1394                     array[0] = v;
1395                 }
1396             } else {
1397                 array = header.getEncodedStringValues(addrType);
1398             }
1399             addressMap.put(addrType, array);
1400         }
1401 
1402         final HashSet<String> recipients = new HashSet<String>();
1403         final int msgType = pdu.getMessageType();
1404         // Here we only allocate thread ID for M-Notification.ind,
1405         // M-Retrieve.conf and M-Send.req.
1406         // Some of other PDU types may be allocated a thread ID outside
1407         // this scope.
1408         if ((msgType == PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND)
1409                 || (msgType == PduHeaders.MESSAGE_TYPE_RETRIEVE_CONF)
1410                 || (msgType == PduHeaders.MESSAGE_TYPE_SEND_REQ)) {
1411             switch (msgType) {
1412                 case PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND:
1413                 case PduHeaders.MESSAGE_TYPE_RETRIEVE_CONF:
1414                     loadRecipients(PduHeaders.FROM, recipients, addressMap);
1415 
1416                     // For received messages (whether group MMS is enabled or not) we want to
1417                     // associate this message with the thread composed of all the recipients
1418                     // EXCLUDING our own number. This includes the person who sent the message
1419                     // (the FROM field above) in addition to the other people the message was
1420                     // addressed TO (or CC fields to address group messaging compatibility issues
1421                     // with devices that place numbers in this field). Typically our own number is
1422                     // in the TO/CC field so we have to remove it in checkAndLoadToCcRecipients.
1423                     checkAndLoadToCcRecipients(recipients, addressMap, subPhoneNumber);
1424                     break;
1425                 case PduHeaders.MESSAGE_TYPE_SEND_REQ:
1426                     loadRecipients(PduHeaders.TO, recipients, addressMap);
1427                     break;
1428             }
1429             long threadId = -1L;
1430             if (!recipients.isEmpty()) {
1431                 // Given all the recipients associated with this message, find (or create) the
1432                 // correct thread.
1433                 threadId = MmsSmsUtils.Threads.getOrCreateThreadId(mContext, recipients);
1434             } else {
1435                 LogUtil.w(TAG, "PduPersister.persist No recipients; persisting PDU to thread: "
1436                         + threadId);
1437             }
1438             values.put(Mms.THREAD_ID, threadId);
1439         }
1440 
1441         // Save parts first to avoid inconsistent message is loaded
1442         // while saving the parts.
1443         final long placeholderId = System.currentTimeMillis(); // Placeholder ID of the msg.
1444 
1445         // Figure out if this PDU is a text-only message
1446         boolean textOnly = true;
1447 
1448         // Get body if the PDU is a RetrieveConf or SendReq.
1449         if (pdu instanceof MultimediaMessagePdu) {
1450             body = ((MultimediaMessagePdu) pdu).getBody();
1451             // Start saving parts if necessary.
1452             if (body != null) {
1453                 final int partsNum = body.getPartsNum();
1454                 if (LOCAL_LOGV) {
1455                     LogUtil.v(TAG, "PduPersister.persist partsNum: " + partsNum);
1456                 }
1457                 if (partsNum > 2) {
1458                     // For a text-only message there will be two parts: 1-the SMIL, 2-the text.
1459                     // Down a few lines below we're checking to make sure we've only got SMIL or
1460                     // text. We also have to check then we don't have more than two parts.
1461                     // Otherwise, a slideshow with two text slides would be marked as textOnly.
1462                     textOnly = false;
1463                 }
1464                 for (int i = 0; i < partsNum; i++) {
1465                     final PduPart part = body.getPart(i);
1466                     persistPart(part, placeholderId, preOpenedFiles);
1467 
1468                     // If we've got anything besides text/plain or SMIL part, then we've got
1469                     // an mms message with some other type of attachment.
1470                     final String contentType = getPartContentType(part);
1471                     if (LOCAL_LOGV) {
1472                         LogUtil.v(TAG, "PduPersister.persist part: " + i + " contentType: " +
1473                                 contentType);
1474                     }
1475                     if (contentType != null && !ContentType.APP_SMIL.equals(contentType)
1476                             && !ContentType.TEXT_PLAIN.equals(contentType)) {
1477                         textOnly = false;
1478                     }
1479                 }
1480             }
1481         }
1482         // Record whether this mms message is a simple plain text or not. This is a hint for the
1483         // UI.
1484         if (OsUtil.isAtLeastJB_MR1()) {
1485             values.put(Mms.TEXT_ONLY, textOnly ? 1 : 0);
1486         }
1487 
1488         if (OsUtil.isAtLeastL_MR1()) {
1489             values.put(Mms.SUBSCRIPTION_ID, subId);
1490         } else {
1491             Assert.equals(ParticipantData.DEFAULT_SELF_SUB_ID, subId);
1492         }
1493 
1494         Uri res = null;
1495         if (existingUri) {
1496             res = uri;
1497             SqliteWrapper.update(mContext, mContentResolver, res, values, null, null);
1498         } else {
1499             res = SqliteWrapper.insert(mContext, mContentResolver, uri, values);
1500             if (res == null) {
1501                 throw new MmsException("persist() failed: return null.");
1502             }
1503             // Get the real ID of the PDU and update all parts which were
1504             // saved with the placeholder ID.
1505             msgId = ContentUris.parseId(res);
1506         }
1507 
1508         values = new ContentValues(1);
1509         values.put(Part.MSG_ID, msgId);
1510         SqliteWrapper.update(mContext, mContentResolver,
1511                 Uri.parse("content://mms/" + placeholderId + "/part"),
1512                 values, null, null);
1513         // We should return the longest URI of the persisted PDU, for
1514         // example, if input URI is "content://mms/inbox" and the _ID of
1515         // persisted PDU is '8', we should return "content://mms/inbox/8"
1516         // instead of "content://mms/8".
1517         // TODO: Should the MmsProvider be responsible for this???
1518         if (!existingUri) {
1519             res = Uri.parse(uri + "/" + msgId);
1520         }
1521 
1522         // Save address information.
1523         for (final int addrType : ADDRESS_FIELDS) {
1524             final EncodedStringValue[] array = addressMap.get(addrType);
1525             if (array != null) {
1526                 persistAddress(msgId, addrType, array);
1527             }
1528         }
1529 
1530         return res;
1531     }
1532 
1533     /**
1534      * For a given address type, extract the recipients from the headers.
1535      *
1536      * @param addressType     can be PduHeaders.FROM or PduHeaders.TO
1537      * @param recipients      a HashSet that is loaded with the recipients from the FROM or TO
1538      *                        headers
1539      * @param addressMap      a HashMap of the addresses from the ADDRESS_FIELDS header
1540      */
loadRecipients(final int addressType, final HashSet<String> recipients, final SparseArray<EncodedStringValue[]> addressMap)1541     private void loadRecipients(final int addressType, final HashSet<String> recipients,
1542             final SparseArray<EncodedStringValue[]> addressMap) {
1543         final EncodedStringValue[] array = addressMap.get(addressType);
1544         if (array == null) {
1545             return;
1546         }
1547         for (final EncodedStringValue v : array) {
1548             if (v != null) {
1549                 final String number = v.getString();
1550                 if (!recipients.contains(number)) {
1551                     // Only add numbers which aren't already included.
1552                     recipients.add(number);
1553                 }
1554             }
1555         }
1556     }
1557 
1558     /**
1559      * For a given address type, extract the recipients from the headers.
1560      *
1561      * @param recipients      a HashSet that is loaded with the recipients from the FROM or TO
1562      *                        headers
1563      * @param addressMap      a HashMap of the addresses from the ADDRESS_FIELDS header
1564      * @param selfNumber      self phone number
1565      */
checkAndLoadToCcRecipients(final HashSet<String> recipients, final SparseArray<EncodedStringValue[]> addressMap, final String selfNumber)1566     private void checkAndLoadToCcRecipients(final HashSet<String> recipients,
1567             final SparseArray<EncodedStringValue[]> addressMap, final String selfNumber) {
1568         final EncodedStringValue[] arrayTo = addressMap.get(PduHeaders.TO);
1569         final EncodedStringValue[] arrayCc = addressMap.get(PduHeaders.CC);
1570         final ArrayList<String> numbers = new ArrayList<String>();
1571         if (arrayTo != null) {
1572             for (final EncodedStringValue v : arrayTo) {
1573                 if (v != null) {
1574                     numbers.add(v.getString());
1575                 }
1576             }
1577         }
1578         if (arrayCc != null) {
1579             for (final EncodedStringValue v : arrayCc) {
1580                 if (v != null) {
1581                     numbers.add(v.getString());
1582                 }
1583             }
1584         }
1585 
1586         // If selfNumber is unavailable and there is only a single address in all TO and CC, we can
1587         // skip adding it into recipients as assuming it is my own phone number.
1588         final boolean isSelfNumberUnavailable = TextUtils.isEmpty(selfNumber);
1589         if (isSelfNumberUnavailable && numbers.size() == 1) {
1590             return;
1591         }
1592 
1593         for (final String number : numbers) {
1594             // Only add numbers which aren't my own number.
1595             if (isSelfNumberUnavailable || !PhoneNumberUtils.compare(number, selfNumber)) {
1596                 if (!recipients.contains(number)) {
1597                     // Only add numbers which aren't already included.
1598                     recipients.add(number);
1599                 }
1600             }
1601         }
1602     }
1603 
1604     /**
1605      * Move a PDU object from one location to another.
1606      *
1607      * @param from Specify the PDU object to be moved.
1608      * @param to   The destination location, should be one of the following:
1609      *             "content://mms/inbox", "content://mms/sent",
1610      *             "content://mms/drafts", "content://mms/outbox",
1611      *             "content://mms/trash".
1612      * @return New Uri of the moved PDU.
1613      * @throws MmsException Error occurred while moving the message.
1614      */
move(final Uri from, final Uri to)1615     public Uri move(final Uri from, final Uri to) throws MmsException {
1616         // Check whether the 'msgId' has been assigned a valid value.
1617         final long msgId = ContentUris.parseId(from);
1618         if (msgId == -1L) {
1619             throw new MmsException("Error! ID of the message: -1.");
1620         }
1621 
1622         // Get corresponding int value of destination box.
1623         final Integer msgBox = MESSAGE_BOX_MAP.get(to);
1624         if (msgBox == null) {
1625             throw new MmsException(
1626                     "Bad destination, must be one of "
1627                             + "content://mms/inbox, content://mms/sent, "
1628                             + "content://mms/drafts, content://mms/outbox, "
1629                             + "content://mms/temp."
1630             );
1631         }
1632 
1633         final ContentValues values = new ContentValues(1);
1634         values.put(Mms.MESSAGE_BOX, msgBox);
1635         SqliteWrapper.update(mContext, mContentResolver, from, values, null, null);
1636         return ContentUris.withAppendedId(to, msgId);
1637     }
1638 
1639     /**
1640      * Wrap a byte[] into a String.
1641      */
toIsoString(final byte[] bytes)1642     public static String toIsoString(final byte[] bytes) {
1643         try {
1644             return new String(bytes, CharacterSets.MIMENAME_ISO_8859_1);
1645         } catch (final UnsupportedEncodingException e) {
1646             // Impossible to reach here!
1647             Log.e(TAG, "ISO_8859_1 must be supported!", e);
1648             return "";
1649         }
1650     }
1651 
1652     /**
1653      * Unpack a given String into a byte[].
1654      */
getBytes(final String data)1655     public static byte[] getBytes(final String data) {
1656         try {
1657             return data.getBytes(CharacterSets.MIMENAME_ISO_8859_1);
1658         } catch (final UnsupportedEncodingException e) {
1659             // Impossible to reach here!
1660             Log.e(TAG, "ISO_8859_1 must be supported!", e);
1661             return new byte[0];
1662         }
1663     }
1664 
1665     /**
1666      * Remove all objects in the temporary path.
1667      */
release()1668     public void release() {
1669         final Uri uri = Uri.parse(TEMPORARY_DRM_OBJECT_URI);
1670         SqliteWrapper.delete(mContext, mContentResolver, uri, null, null);
1671     }
1672 
1673     /**
1674      * Find all messages to be sent or downloaded before certain time.
1675      */
getPendingMessages(final long dueTime)1676     public Cursor getPendingMessages(final long dueTime) {
1677         final Uri.Builder uriBuilder = PendingMessages.CONTENT_URI.buildUpon();
1678         uriBuilder.appendQueryParameter("protocol", "mms");
1679 
1680         final String selection = PendingMessages.ERROR_TYPE + " < ?"
1681                 + " AND " + PendingMessages.DUE_TIME + " <= ?";
1682 
1683         final String[] selectionArgs = new String[] {
1684                 String.valueOf(MmsSms.ERR_TYPE_GENERIC_PERMANENT),
1685                 String.valueOf(dueTime)
1686         };
1687 
1688         return SqliteWrapper.query(mContext, mContentResolver,
1689                 uriBuilder.build(), null, selection, selectionArgs,
1690                 PendingMessages.DUE_TIME);
1691     }
1692 }
1693